mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
760 lines
35 KiB
C
760 lines
35 KiB
C
#ifndef PSX_LOBBY_CLIENT_H
|
|
#define PSX_LOBBY_CLIENT_H
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
#define PSX_LOBBY_ID_LEN 40
|
|
#define PSX_LOBBY_NAME_LEN 64
|
|
/* Title compatibility IDs may contain a prefix and a 160-bit source hash.
|
|
* Preserve the full 63-byte ID for exact list/join comparisons. */
|
|
#define PSX_LOBBY_VERSION_LEN 64
|
|
#define PSX_LOBBY_ENDPOINT_LEN 64
|
|
#define PSX_LOBBY_MAX_LIST 32
|
|
/* Player seats this framework can seat. Was what PSX_LOBBY_MAX_MEMBERS meant
|
|
* before a lobby held anything that was not a player. */
|
|
#define PSX_LOBBY_MAX_PLAYERS 8
|
|
/* Spectator seats a host may open, a separate pool on top of the players --
|
|
* so a full eight-player room can still be watched. */
|
|
#define PSX_LOBBY_MAX_SPECTATORS 4
|
|
/* Rows in the membership table: both tables land in it, tagged by role. */
|
|
#define PSX_LOBBY_MAX_MEMBERS (PSX_LOBBY_MAX_PLAYERS + PSX_LOBBY_MAX_SPECTATORS)
|
|
|
|
/* Seat indices are one namespace, matching the server: below the base is a
|
|
* player seat, at or above it is `index - base` in the gallery. The server
|
|
* republishes its own base in every lobby_update; this is the compiled-in
|
|
* default for an update that arrives without one. */
|
|
#define PSX_LOBBY_SPECTATOR_SLOT_BASE 64
|
|
#define PSX_LOBBY_MAX_LAN_EPS 4
|
|
#define PSX_LOBBY_LANG_LEN 16
|
|
#define PSX_LOBBY_MAX_MODS 12
|
|
#define PSX_LOBBY_MOD_ID_LEN 96
|
|
#define PSX_LOBBY_MOD_VER_LEN 32
|
|
#define PSX_LOBBY_MOD_NAME_LEN 64
|
|
#define PSX_LOBBY_MOD_FEATS_LEN 384
|
|
|
|
#ifndef PSX_GAME_VERSION
|
|
#define PSX_GAME_VERSION "dev"
|
|
#endif
|
|
|
|
typedef struct PsxLobbyRow {
|
|
char lobby_id[PSX_LOBBY_ID_LEN];
|
|
char name[PSX_LOBBY_NAME_LEN];
|
|
char game_name[PSX_LOBBY_NAME_LEN];
|
|
char game_version[PSX_LOBBY_VERSION_LEN];
|
|
int player_count;
|
|
int max_slots;
|
|
int has_password;
|
|
/* Host UDP endpoint from the server list (for one-shot latency probes). */
|
|
char host_endpoint[PSX_LOBBY_ENDPOINT_LEN];
|
|
/* Legacy hub lan_endpoints (compat). Prefer local UDP beacon by lobby_id. */
|
|
char lan_endpoints[PSX_LOBBY_MAX_LAN_EPS][PSX_LOBBY_ENDPOINT_LEN];
|
|
int lan_count;
|
|
/* Round-trip ms to a reachable candidate; -1 unknown / timed out. */
|
|
int latency_ms;
|
|
/* Host's country (alpha-2) from the server's GeoIP; "" unknown. */
|
|
char host_country[4];
|
|
/* Gallery: 0 = none; else how many watch out of how many seats. */
|
|
int allow_spectators;
|
|
int max_spectators;
|
|
int spectator_count;
|
|
} PsxLobbyRow;
|
|
|
|
/* One player connected to the hub (the lobby_list `players` array). */
|
|
typedef struct PsxLobbyOnlinePlayer {
|
|
char player_id[PSX_LOBBY_ID_LEN];
|
|
char display_name[PSX_LOBBY_NAME_LEN];
|
|
char country[4];
|
|
char lobby_id[PSX_LOBBY_ID_LEN];
|
|
char lobby_name[PSX_LOBBY_NAME_LEN];
|
|
int hosting;
|
|
/* First 8 characters of the player's connection id: enough to tell
|
|
* "which row is me" without publishing whole ids to browsers. */
|
|
char tag[12];
|
|
/* The title that player is browsing for; rows of other titles are
|
|
* dropped at parse time when this client has a game identity. */
|
|
char game_name[PSX_LOBBY_NAME_LEN];
|
|
/* Opaque, stable id for the ACCOUNT behind this connection; "" for a
|
|
* guest. The server's own row key, published so a client-side block
|
|
* list can survive the other player reconnecting or renaming. A key,
|
|
* never a label: nothing renders it. */
|
|
char account[PSX_LOBBY_ID_LEN];
|
|
} PsxLobbyOnlinePlayer;
|
|
#define PSX_LOBBY_MAX_ONLINE 64
|
|
|
|
typedef struct PsxLobbyModPkg {
|
|
char id[PSX_LOBBY_MOD_ID_LEN];
|
|
char ver[PSX_LOBBY_MOD_VER_LEN];
|
|
char name[PSX_LOBBY_MOD_NAME_LEN];
|
|
char feats[PSX_LOBBY_MOD_FEATS_LEN];
|
|
int builtin;
|
|
uint32_t size;
|
|
} PsxLobbyModPkg;
|
|
|
|
typedef struct PsxLobbyModOffer {
|
|
int valid;
|
|
int count;
|
|
PsxLobbyModPkg pkgs[PSX_LOBBY_MAX_MODS];
|
|
} PsxLobbyModOffer;
|
|
|
|
typedef struct PsxLobbyMember {
|
|
/* Seat index in the shared namespace: a player seat, or
|
|
* spectator_slot_base + gallery index. Pass it back to kick / move as-is. */
|
|
int slot;
|
|
char player_id[PSX_LOBBY_ID_LEN];
|
|
char display_name[PSX_LOBBY_NAME_LEN];
|
|
int ready;
|
|
/* 1 when this row is in the gallery. Read this rather than comparing
|
|
* `slot` against a base: the base is the server's to choose. */
|
|
int is_spectator;
|
|
/* Peer BIOS capability from set_ready bios_offer (0 if legacy/missing). */
|
|
int bios_offer_valid;
|
|
int bios_can_openbios; /* linked OpenBIOS backend */
|
|
int bios_can_scph1001; /* linked retail + validated dump available */
|
|
int bios_prefer_openbios; /* explicit OpenBIOS pick (not retail) */
|
|
/* CRC-32 of the retail image this seat would boot (bios_offer.retail_crc);
|
|
* 0 = not sent (older client). See netplay_bios_settle.h. */
|
|
uint32_t bios_retail_crc;
|
|
/* Peer memory-card offer from set_ready memcard_offer (0 if legacy). */
|
|
int memcard_offer_valid;
|
|
int memcard_has_card; /* a slot-1 card is enabled locally */
|
|
int memcard_share; /* peer opted in to bring it to the match */
|
|
/* Country (alpha-2) from the server's GeoIP; "" unknown. */
|
|
char country[4];
|
|
/* 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;
|
|
/* Peer mod catalog offer from join/set_ready (0 if legacy/missing). */
|
|
int mod_offer_valid;
|
|
int mod_count;
|
|
PsxLobbyModPkg mods[PSX_LOBBY_MAX_MODS];
|
|
} PsxLobbyMember;
|
|
|
|
/*
|
|
* Local BIOS capability advertised on set_ready (see docs/BIOS_SELECTION.md
|
|
* netplay settle rule). Updated by the host runtime via psx_lobby_set_bios_offer.
|
|
*/
|
|
typedef struct PsxLobbyBiosOffer {
|
|
int valid;
|
|
int can_openbios;
|
|
int can_scph1001;
|
|
int prefer_openbios; /* 1 = OpenBIOS selected; 0 = retail / willing SCPH */
|
|
/* CRC-32 of the retail file this peer would boot for a retail match;
|
|
* 0 when can_scph1001 is 0. The settle compares these, not the token. */
|
|
uint32_t retail_crc;
|
|
} PsxLobbyBiosOffer;
|
|
|
|
/*
|
|
* Local memory-card offer advertised on set_ready. `has_card` says whether a
|
|
* slot-1 card is enabled here; `share` is the player's opt-in to bring it.
|
|
* Meaningful for seat 1 (whose card becomes the match's slot-2 card); other
|
|
* seats may advertise it harmlessly.
|
|
*/
|
|
typedef struct PsxLobbyMemcardOffer {
|
|
int valid;
|
|
int has_card;
|
|
int share;
|
|
} PsxLobbyMemcardOffer;
|
|
|
|
/* One lobby chat line (WS op:chat, echoed by the server to everyone seated,
|
|
* sender included — so the ring is the room's order, not ours). */
|
|
#define PSX_LOBBY_CHAT_TEXT_LEN 256
|
|
#define PSX_LOBBY_CHAT_RING 64
|
|
typedef struct PsxLobbyChatMsg {
|
|
char player_id[PSX_LOBBY_ID_LEN];
|
|
char from[PSX_LOBBY_NAME_LEN];
|
|
char text[PSX_LOBBY_CHAT_TEXT_LEN];
|
|
/* The SERVER's id for this line, and what psx_lobby_report_chat names.
|
|
* Empty for a system line, a locally generated one, and anything from a
|
|
* server too old to assign one -- none of which can be reported, because
|
|
* there is no referent both sides agree on.
|
|
*
|
|
* A report carries this and NOT the text: the server writes down what it
|
|
* relayed under this id. Sending the words would let a client fabricate a
|
|
* message and have somebody sanctioned for it. */
|
|
char mid[40];
|
|
/* Account key of the sender; "" for a guest or a system line. What an
|
|
* ignore/block keys on, so it outlives a rename. */
|
|
char account[PSX_LOBBY_ID_LEN];
|
|
int is_local;
|
|
int is_system;
|
|
uint32_t seq;
|
|
} PsxLobbyChatMsg;
|
|
|
|
/*
|
|
* Host-authoritative sim settings negotiated over the lobby.
|
|
* Guests apply these on launch so both peers boot with matching caps.
|
|
*/
|
|
typedef struct PsxLobbyMatchCaps {
|
|
int valid; /* 1 when a host blob was received / set */
|
|
int aspect_num; /* e.g. 4, 16, 21 */
|
|
int aspect_den; /* e.g. 3, 9 */
|
|
int turbo_loads; /* 0/1 */
|
|
int bios_hle; /* 0/1 */
|
|
int fast_boot; /* 0/1 */
|
|
int auto_skip_fmv; /* 0/1 */
|
|
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;
|
|
/* 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. */
|
|
int multitap_analog;
|
|
/* Host allows seat 1 to bring its own memory card (default 1; absent
|
|
* field = 1). The lobby icon is lit only when this AND seat 1's offer
|
|
* (has_card && share) hold. */
|
|
int guest_memcard;
|
|
/* Settled at start by the host: 1 = seat 1's card IS used this match.
|
|
* Peers launch from this, never from their own view of the seat table,
|
|
* so a toggle racing the start cannot split the room. */
|
|
int guest_memcard_active;
|
|
char language[PSX_LOBBY_LANG_LEN];
|
|
/* Settled match BIOS: "openbios" | "scph1001" | "" (unset / legacy).
|
|
* "scph1001" means "the retail image named by session_bios_crc". */
|
|
char session_bios[16];
|
|
/* Retail match: CRC-32 of the image every peer boots. 0 = not sent (an
|
|
* older host); peers then accept any retail dump, as before. */
|
|
uint32_t session_bios_crc;
|
|
/* Host-authoritative online mod plan. Empty mod_plan_fp/mod_count means vanilla. */
|
|
char mod_plan_fp[72];
|
|
int mod_count;
|
|
PsxLobbyModPkg mods[PSX_LOBBY_MAX_MODS];
|
|
} PsxLobbyMatchCaps;
|
|
|
|
/* The same portable plan is used by WebSocket and LAN lobbies. No media paths
|
|
* or bytes are serialized. A failed/truncated message must never mean vanilla. */
|
|
int psx_lobby_encode_match_caps(char *out, size_t cap, const PsxLobbyMatchCaps *caps);
|
|
int psx_lobby_decode_match_caps(const char *json, PsxLobbyMatchCaps *out);
|
|
void psx_lobby_set_local_match_caps(const PsxLobbyMatchCaps *caps);
|
|
|
|
typedef struct PsxLobbyJoinInfo {
|
|
int ok;
|
|
char lobby_id[PSX_LOBBY_ID_LEN];
|
|
uint32_t session_id;
|
|
int local_slot;
|
|
char host_endpoint[PSX_LOBBY_ENDPOINT_LEN];
|
|
char guest_endpoint[PSX_LOBBY_ENDPOINT_LEN];
|
|
char bind_hostport[PSX_LOBBY_ENDPOINT_LEN];
|
|
char peer_hostport[PSX_LOBBY_ENDPOINT_LEN];
|
|
int player_count;
|
|
int max_slots;
|
|
/* 1 when this client holds a gallery seat: it runs the match and shows it,
|
|
* and contributes no input to anybody. */
|
|
int local_is_spectator;
|
|
/* Host opt-in, echoed by the server. 0 when the server predates
|
|
* spectators, which is why every spectator control is gated on it. */
|
|
int allow_spectators;
|
|
int max_spectators;
|
|
int spectator_count;
|
|
int spectator_slot_base;
|
|
/* Where the gallery starts in the RELAY's slot space -- a different
|
|
* namespace from the lobby seat index above, and the one the engine needs.
|
|
* The relay forwards nothing from a slot at or beyond its player count, so
|
|
* this is the number that makes a spectator unable to send. */
|
|
int spectator_relay_base;
|
|
/* 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;
|
|
/* Launch: transport "ice" -- a 2-player room started on ICE
|
|
* (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];
|
|
} PsxLobbyJoinInfo;
|
|
|
|
/* Default URL when PSX_NET_LOBBY_URL unset. Order:
|
|
* 1) env PSX_NET_LOBBY_URL
|
|
* 2) compile-time PSX_NET_LOBBY_DEFAULT_URL (title CMake / scaffold)
|
|
* 3) ws://netplay.retcomm.net:8765 */
|
|
const char *psx_lobby_default_url(void);
|
|
|
|
int psx_lobby_connect(const char *ws_url); /* 0 = connected or connect started */
|
|
void psx_lobby_disconnect(void);
|
|
int psx_lobby_connected(void);
|
|
/* 1 while DNS/TCP/WebSocket upgrade runs off the UI thread. */
|
|
int psx_lobby_connecting(void);
|
|
|
|
void psx_lobby_set_display_name(const char *name);
|
|
const char *psx_lobby_display_name(void);
|
|
const char *psx_lobby_player_id(void);
|
|
|
|
/* Non-blocking pump — call every frame from the launcher. */
|
|
void psx_lobby_pump(void);
|
|
|
|
/*
|
|
* Title + release pin used for create/join matching and list filters.
|
|
* Defaults: game_name empty (no name filter), game_version PSX_GAME_VERSION.
|
|
* List: Release pins filter by exact game_version; "dev" lists all versions
|
|
* of the title (join still requires an exact version match).
|
|
*/
|
|
void psx_lobby_set_game_identity(const char *game_name, const char *game_version);
|
|
const char *psx_lobby_game_version(void);
|
|
|
|
/*
|
|
* TOC fingerprint (lowercase hex SHA-256 from DiscIdentity::disc_fp).
|
|
* Sent on create/join so the lobby server can reject mismatched mounts
|
|
* (e.g. Track-01-only vs full multi-track cue). Empty disables the check
|
|
* on the server for that peer (legacy clients); modern clients always set it
|
|
* after a successful disc verify.
|
|
*/
|
|
void psx_lobby_set_disc_fp(const char *disc_fp);
|
|
const char *psx_lobby_disc_fp(void);
|
|
|
|
/* Default max_slots for create (clamped 2..8, default 2). */
|
|
void psx_lobby_set_max_slots(int max_slots);
|
|
|
|
void psx_lobby_request_list(void);
|
|
int psx_lobby_list_count(void);
|
|
int psx_lobby_list_get(int index, PsxLobbyRow *out);
|
|
/* Players connected to the hub, refreshed with every lobby_list. */
|
|
int psx_lobby_online_count(void);
|
|
int psx_lobby_online_get(int index, PsxLobbyOnlinePlayer *out);
|
|
|
|
/*
|
|
* Create lobby. host_bind e.g. "0.0.0.0:7777". password may be NULL/empty.
|
|
* game_version NULL/empty → identity / PSX_GAME_VERSION / "dev".
|
|
* match_caps may be NULL (legacy); when non-NULL and valid, sent to the server
|
|
* so guests join with the host's sim settings.
|
|
* Returns 0 if request sent; poll psx_lobby_join_info() / in_lobby().
|
|
*/
|
|
int psx_lobby_create(const char *name, const char *game_name,
|
|
const char *game_version,
|
|
const char *password, const char *host_bind,
|
|
const PsxLobbyMatchCaps *match_caps);
|
|
|
|
int psx_lobby_join(const char *lobby_id, const char *password,
|
|
const char *guest_bind);
|
|
|
|
int psx_lobby_leave(void);
|
|
|
|
/* ---- spectators --------------------------------------------------------
|
|
*
|
|
* A gallery seat runs the match locally, in sync, and contributes nothing.
|
|
* The lobby server enforces the "contributes nothing" half at its UDP relay;
|
|
* this side enforces the rest -- no Ready vote, no controller reaching the sim.
|
|
*
|
|
* Every one of these reads 0 against a server that predates spectators, so a
|
|
* caller that gates its UI on allow_spectators degrades to the old lobby. */
|
|
|
|
/* Host: open a gallery on the NEXT create. Sticky, like the max_slots
|
|
* default -- create carries whatever this was last set to. */
|
|
void psx_lobby_set_allow_spectators(int allow);
|
|
/* What the toggle is set to, for the UI to render: what the host ASKED for. */
|
|
int psx_lobby_allow_spectators_pref(void);
|
|
/* What the current lobby actually has (server-echoed), not what was asked.
|
|
* These disagree whenever the server predates spectators -- exactly the case
|
|
* the UI must not offer a gallery in. */
|
|
int psx_lobby_allow_spectators(void);
|
|
int psx_lobby_max_spectators(void);
|
|
int psx_lobby_spectator_count(void);
|
|
/* 1 when THIS client is in the gallery. The one call the engine needs. */
|
|
int psx_lobby_local_is_spectator(void);
|
|
/* Base of the gallery half of the seat namespace, as the server reports it. */
|
|
int psx_lobby_spectator_slot_base(void);
|
|
/* Seat index for gallery position `index`, for move / kick; <0 out of range. */
|
|
int psx_lobby_spectator_slot(int index);
|
|
/* This client's slot in the RELAY's namespace, for RNetConfig.wire_slot.
|
|
* -1 when not a spectator or the server published no relay base -- and a
|
|
* spectator without one must not launch, because sending as a player slot is
|
|
* exactly what it must not do. */
|
|
int psx_lobby_local_wire_slot(void);
|
|
/* 1 when `slot` addresses a seat in either table -- the shared guard the
|
|
* kick / move entry points use, so a gallery index is not rejected as if it
|
|
* were out of range. */
|
|
int psx_lobby_seat_valid(int slot);
|
|
|
|
/* Host-only: remove the player in `slot` (not the host / self). */
|
|
int psx_lobby_kick(int slot);
|
|
|
|
/* Host-only: swap/move a seated player between slots (server broadcasts update). */
|
|
int psx_lobby_move_member(int from_slot, int to_slot);
|
|
|
|
int psx_lobby_in_lobby(void);
|
|
int psx_lobby_is_host(void);
|
|
/* Host's player_id from the last create/lobby_update (empty if unknown). */
|
|
const char *psx_lobby_host_player_id(void);
|
|
/* Filled after create/join/lobby_update; peer endpoints for PsxNetplayConfig. */
|
|
const PsxLobbyJoinInfo *psx_lobby_join_info(void);
|
|
|
|
/* Latest host match_caps (valid==0 until create/join/launch delivers one). */
|
|
const PsxLobbyMatchCaps *psx_lobby_match_caps(void);
|
|
|
|
/* Local mod package/version catalog advertised to the lobby on join. */
|
|
void psx_lobby_set_mod_offer(const PsxLobbyModOffer *offer);
|
|
const PsxLobbyModOffer *psx_lobby_mod_offer(void);
|
|
int psx_lobby_need_mods_count(void);
|
|
int psx_lobby_need_mods_get(int index, PsxLobbyModPkg *out);
|
|
int psx_lobby_need_mods_can_transfer(void);
|
|
int psx_lobby_mod_xfer_start(void);
|
|
void psx_lobby_mod_xfer_cancel(void);
|
|
int psx_lobby_mod_xfer_progress(void);
|
|
int psx_lobby_mod_xfer_failed(char *err, size_t err_cap);
|
|
|
|
/* 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);
|
|
|
|
/* 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);
|
|
|
|
/* Waiting-room RTT in ms *to* `slot`, or -1 if unknown / local seat.
|
|
* Prefers ICE/TURN data-channel RTT when the lobby probe completes
|
|
* (CGNAT / Force TURN); else direct UDP rnet_rtt_probe. Stored as
|
|
* max(local, peer REPORT) so an optimistic guest sample cannot undercut
|
|
* delay. Not the lobby WebSocket RTT. */
|
|
int psx_lobby_member_latency_ms(int slot);
|
|
|
|
/* True when member.player_id matches psx_lobby_host_player_id().
|
|
* Prefer this over `slot == 0` — seats can move. */
|
|
int psx_lobby_member_is_host(const PsxLobbyMember *member);
|
|
|
|
/*
|
|
* ICE signaling relay (MotK WS op:signal). text is SDP/candidate (max 2047).
|
|
* send returns 0 if queued/written; poll returns 1 when an inbound signal was
|
|
* copied out (LOCAL_* types as emitted by the peer — remap to REMOTE_* before
|
|
* rnet_session_push_signal).
|
|
*/
|
|
int psx_lobby_send_signal(int type, int flag, const char *text);
|
|
int psx_lobby_poll_signal(int *type, int *flag, char *text, size_t text_cap);
|
|
/* Drop queued ICE SDP/candidates (soft-return / rematch hygiene). */
|
|
void psx_lobby_clear_signals(void);
|
|
/* When 0, inbound ICE op:signal is discarded (lobby / post-match). Launch
|
|
* re-enables so early peer offers are kept until netplay drains them. */
|
|
void psx_lobby_set_ice_signal_accept(int accept);
|
|
|
|
/*
|
|
* Coturn / ICE credentials minted by the WS lobby
|
|
* (`get_turn_credentials` → `turn_credentials`). Valid until disconnect or TTL.
|
|
* Strings are stable until the next successful mint or disconnect — safe to
|
|
* pass into RNetIceConfig for juice_create.
|
|
*/
|
|
typedef struct PsxLobbyTurnCredentials {
|
|
int valid; /* 1 when ok mint cached and not expired */
|
|
char stun_host[128];
|
|
int stun_port;
|
|
char turn_host[128];
|
|
int turn_port;
|
|
char username[192];
|
|
char password[128];
|
|
uint32_t ttl_secs;
|
|
} PsxLobbyTurnCredentials;
|
|
|
|
/* Queue WS get_turn_credentials. Returns 0 if sent/queued. */
|
|
int psx_lobby_request_turn_credentials(void);
|
|
/* Non-NULL; valid==0 when unavailable / expired / STUN-only. */
|
|
const PsxLobbyTurnCredentials *psx_lobby_turn_credentials(void);
|
|
|
|
/* Local ready flag (from last lobby_update matching our player_id). */
|
|
int psx_lobby_local_ready(void);
|
|
/* True when every seated player is ready and player_count >= 2. */
|
|
int psx_lobby_all_ready(void);
|
|
|
|
/* Toggle ready in the current lobby (attaches current bios_offer). */
|
|
int psx_lobby_set_ready(int ready);
|
|
/* The set_ready / start messages exactly as this client sends them (into msg,
|
|
* 0 = ok). The Retro hub forwards them verbatim for a seat it holds. */
|
|
int psx_lobby_ready_message(int ready, char *msg, size_t cap);
|
|
int psx_lobby_start_message(const PsxLobbyMatchCaps *caps, char *msg, size_t cap);
|
|
/* A match the Retro hub negotiated (retcomm-launcher docs/NETPLAY_HANDOFF.md).
|
|
* replay_room adopts the record's identity, seat and room messages as if they
|
|
* had arrived on this client's socket; ingest_record also adopts its launch,
|
|
* after which launch_pending, join_info and match_caps answer as for an
|
|
* in-game match. 0 = adopted; else `why` says why. handoff_active: 1 once a
|
|
* record was ingested (the match ends the process, not back into a lobby). */
|
|
int psx_lobby_replay_room(const char *record, char *why, size_t why_cap);
|
|
int psx_lobby_ingest_record(const char *record, char *why, size_t why_cap);
|
|
int psx_lobby_handoff_active(void);
|
|
/* A string field of a JSON object (the client's own reader); 1 = found. */
|
|
int psx_lobby_json_str(const char *json, const char *key, char *out, size_t cap);
|
|
|
|
/* Local BIOS offer used on the next set_ready (and included in settle). */
|
|
void psx_lobby_set_bios_offer(const PsxLobbyBiosOffer *offer);
|
|
const PsxLobbyBiosOffer *psx_lobby_bios_offer(void);
|
|
/* Local memory-card offer (attached to set_ready alongside bios_offer). */
|
|
void psx_lobby_set_memcard_offer(const PsxLobbyMemcardOffer *offer);
|
|
const PsxLobbyMemcardOffer *psx_lobby_memcard_offer(void);
|
|
|
|
/* Seat self-service (server ops seat_move / seat_swap_request /
|
|
* seat_swap_answer). A player may move itself to a FREE player seat; taking
|
|
* an occupied one asks its occupant, who answers from the prompt.
|
|
* seat_move_self(to) : 0 queued; a refusal comes back as a lobby error
|
|
* seat_swap_request(to) : 0 queued; outgoing() then reports the answer
|
|
* seat_swap_incoming : 1 while somebody is asking THIS player
|
|
* seat_swap_respond(ok) : answer (and drop) the incoming ask
|
|
* seat_swap_outgoing : 0 idle, 1 waiting, 2 accepted, -1 declined
|
|
* seat_swap_clear : back to idle after showing a finished result */
|
|
int psx_lobby_seat_move_self(int to_slot);
|
|
int psx_lobby_seat_swap_request(int target_slot);
|
|
int psx_lobby_seat_swap_incoming(char *who, size_t who_cap, int *from_slot);
|
|
int psx_lobby_seat_swap_respond(int accept);
|
|
int psx_lobby_seat_swap_outgoing(void);
|
|
void psx_lobby_seat_swap_clear(void);
|
|
|
|
/* Lobby chat. send: 0 when queued (the line appears via the server echo).
|
|
* count/get read the ring, oldest first; cleared on create/join/leave. */
|
|
/* Report chat lines for moderation. Same contract on every console -- the
|
|
* frame is built by recomp-net (recomp_net/chat_report.h), which is also where
|
|
* the RNET_REPORT_* categories live; this only says where we are and sends it.
|
|
*
|
|
* `mids` are PsxLobbyChatMsg.mid values, several at once because harassment is
|
|
* usually a burst rather than a line. Entries with an empty id are skipped.
|
|
* The text is never sent: see PsxLobbyChatMsg.mid.
|
|
*
|
|
* 0 when handed to the server, which is not the same as accepted -- it refuses
|
|
* an expired line, a guest's line, your own, and anything over the rate
|
|
* limit. */
|
|
int psx_lobby_report_chat(const char *const *mids, int mid_count,
|
|
const char *reason, const char *note);
|
|
|
|
int psx_lobby_send_chat(const char *text);
|
|
|
|
/* Server chat: per-game, outside any room (op server_chat). Its own ring,
|
|
* cleared on disconnect; lines are masked on arrival like lobby chat. */
|
|
int psx_lobby_send_server_chat(const char *text);
|
|
int psx_lobby_server_chat_count(void);
|
|
int psx_lobby_server_chat_get(int index, PsxLobbyChatMsg *out);
|
|
int psx_lobby_chat_count(void);
|
|
int psx_lobby_chat_get(int index, PsxLobbyChatMsg *out);
|
|
void psx_lobby_chat_clear(void);
|
|
|
|
/*
|
|
* Settle session BIOS from seated peers' bios_offer (+ local offer), by the
|
|
* rule in netplay_bios_settle.h: retail only when every seat offers the same
|
|
* retail image CRC; else OpenBIOS when every seat links it.
|
|
* Returns 0 and writes "openbios" or "scph1001" into out (and the retail CRC
|
|
* into *out_crc, 0 for OpenBIOS). Returns 1 when no BIOS suits every seat:
|
|
* out is "", and why (when non-NULL) says which images differ; the host must
|
|
* not start. Returns -1 on bad arguments.
|
|
*/
|
|
int psx_lobby_settle_session_bios(char *out, size_t out_cap, uint32_t *out_crc,
|
|
char *why, size_t why_cap);
|
|
|
|
/*
|
|
* Put one sentence on the launcher's status line (through last_error, which
|
|
* the launcher shows as "Lobby error: <text>"). NULL or "" clears it.
|
|
*/
|
|
void psx_lobby_set_last_error(const char *text);
|
|
|
|
/*
|
|
* Host: ask server to broadcast launch. When match_caps is non-NULL and valid,
|
|
* it is attached to start so launch freezes the latest host settings.
|
|
*/
|
|
int psx_lobby_request_start(const PsxLobbyMatchCaps *match_caps);
|
|
|
|
/*
|
|
* Set when server sends op:launch. Both host and guests should boot netplay.
|
|
* Cleared by psx_lobby_clear_launch_pending() after consuming.
|
|
*/
|
|
int psx_lobby_launch_pending(void);
|
|
void psx_lobby_clear_launch_pending(void);
|
|
|
|
/* After soft-return / rematch: allow waiting-room ICE/UDP RTT probes again.
|
|
* Launch suspends them so they cannot steal match ICE signaling. */
|
|
void psx_lobby_resume_waiting_room_rtt(void);
|
|
|
|
/*
|
|
* Tell the server which accounts this player has blocked.
|
|
*
|
|
* ';'-separated opaque account ids, replacing the whole set -- the client's
|
|
* own file (recomp-ui moderation.ini) is the authority, so an unblock needs
|
|
* no separate op. NULL/"" clears.
|
|
*
|
|
* Sent to the SERVER rather than applied only here, because two of the three
|
|
* things a block has to do cannot be done from this side: the matchmaker must
|
|
* not pair the two, and the blocked player must not see or be able to join
|
|
* the blocker's room. A client can hide what it was sent; it cannot know it
|
|
* was blocked by somebody else, and that is the direction that matters.
|
|
*
|
|
* The server holds the list for the life of a connection and never persists
|
|
* it; recomp-ui re-sends it on every reconnect.
|
|
*/
|
|
int psx_lobby_set_blocks(const char *accounts);
|
|
|
|
/* -- Automatch --------------------------------------------------------------
|
|
*
|
|
* Server-run pairing: the player asks for a match and the SERVER creates the
|
|
* room, is its host, and owns its match_caps from a named ruleset. Protocol:
|
|
* recomp-net-server docs/AUTOMATCH.md. Everything downstream of the pairing
|
|
* is the ordinary `joined` / `lobby_update` / `launch` path already in this
|
|
* file, so this adds a queue and an accept gate and nothing else.
|
|
*
|
|
* Requires a signed-in account: the accept gate charges a dodge cooldown, and
|
|
* a cost that reconnecting erases is not a cost.
|
|
*
|
|
* Same shape as the SNES client (snes_lobby_client.h), on purpose: the host
|
|
* adapter in main.cpp is then a one-line translation per entry point.
|
|
*/
|
|
#define PSX_LOBBY_MAX_RULESETS 8
|
|
#define PSX_LOBBY_RULESET_ID_LEN 48
|
|
#define PSX_LOBBY_RULESET_LABEL_LEN 64
|
|
#define PSX_LOBBY_CAPS_SUMMARY_LEN 128
|
|
|
|
/* One queue type this server offers for this title. */
|
|
typedef struct PsxLobbyRuleset {
|
|
char id[PSX_LOBBY_RULESET_ID_LEN];
|
|
char label[PSX_LOBBY_RULESET_LABEL_LEN];
|
|
/* Server-derived one-liner ("Delay 2 - Rollback on"). Derived from the
|
|
* caps rather than authored, so it cannot drift from what the match
|
|
* runs. The caps themselves arrive with `joined` like any host's. */
|
|
char caps_summary[PSX_LOBBY_CAPS_SUMMARY_LEN];
|
|
/* Empty = any release may queue (still only pooling with its own). */
|
|
char game_version[PSX_LOBBY_VERSION_LEN];
|
|
int max_slots;
|
|
} PsxLobbyRuleset;
|
|
|
|
/* Matches RECOMP_LAUNCHER_AUTOMATCH_* so the host layer can hand these
|
|
* straight to the launcher without a translation table that could drift. */
|
|
enum {
|
|
PSX_LOBBY_AUTOMATCH_IDLE = 0,
|
|
PSX_LOBBY_AUTOMATCH_QUEUED = 1,
|
|
PSX_LOBBY_AUTOMATCH_FOUND = 2,
|
|
PSX_LOBBY_AUTOMATCH_ACCEPTED = 3,
|
|
PSX_LOBBY_AUTOMATCH_FAILED = 4
|
|
};
|
|
|
|
/* The offer on the table while state is FOUND. */
|
|
typedef struct PsxLobbyAutomatchFound {
|
|
char opponent[PSX_LOBBY_NAME_LEN];
|
|
/* Discord @username: display names are not unique, so this is the
|
|
* disambiguator. "" when the server did not say. */
|
|
char opponent_username[PSX_LOBBY_NAME_LEN];
|
|
char opponent_country[4];
|
|
char ruleset_id[PSX_LOBBY_RULESET_ID_LEN];
|
|
char ruleset_label[PSX_LOBBY_RULESET_LABEL_LEN];
|
|
int est_rtt_ms; /* the pair's estimate: rtt_a + rtt_b; <0 unknown */
|
|
int accept_secs; /* seconds left to answer, recomputed per read */
|
|
} PsxLobbyAutomatchFound;
|
|
|
|
/* Ask the server what queues it offers for this title. Answered into
|
|
* psx_lobby_automatch_ruleset_count/get; 0 of them means automatch is off
|
|
* here. Refuses to re-send while one is outstanding. */
|
|
int psx_lobby_automatch_request_rulesets(void);
|
|
/* 1 once the server has answered with at least one ruleset. Not having
|
|
* asked yet is "no": the button must not be offered on an assumption. */
|
|
int psx_lobby_automatch_available(void);
|
|
int psx_lobby_automatch_ruleset_count(void);
|
|
int psx_lobby_automatch_ruleset_get(int index, PsxLobbyRuleset *out);
|
|
|
|
/*
|
|
* Join the queue. `ruleset_id` NULL/"" takes the first one.
|
|
*
|
|
* `mods_enabled` is the caller's assertion that a SIM-AFFECTING mod feature is
|
|
* on locally, beyond whatever the ruleset itself imposes. The server refuses a
|
|
* true with mods_not_pooled and cannot check it -- the assertion exists so a
|
|
* modified client makes a deliberate false statement rather than exploiting
|
|
* an omission (AUTOMATCH.md 5). The host layer decides it; this file only
|
|
* carries it. `mod_exempt` is ';'-separated evidence for cosmetic exemptions
|
|
* (NULL or "" = none), sent as a JSON array for the server to check.
|
|
*
|
|
* 0 = sent. <0 = refused locally (not connected, no fingerprint, already
|
|
* queued). A server refusal arrives asynchronously as state FAILED with a
|
|
* reason in psx_lobby_automatch_error().
|
|
*/
|
|
int psx_lobby_automatch_queue(const char *ruleset_id, int mods_enabled,
|
|
const char *mod_exempt);
|
|
int psx_lobby_automatch_cancel(void);
|
|
int psx_lobby_automatch_state(void);
|
|
int psx_lobby_automatch_queued_secs(void);
|
|
int psx_lobby_automatch_pool(void);
|
|
int psx_lobby_automatch_found_get(PsxLobbyAutomatchFound *out);
|
|
/* accept != 0 accepts; 0 declines and takes the dodge strike. */
|
|
int psx_lobby_automatch_accept(int accept);
|
|
/*
|
|
* Non-zero while the room this client is seated in was created by AUTOMATCH.
|
|
*
|
|
* A human-hosted room is somebody's room: it survives the match, and staying
|
|
* seated is how a rematch happens. An automatch room is the server's, it is
|
|
* created at both-accept and is not joinable by anyone, and there is no host
|
|
* to rematch with -- so staying in it leaves the player parked in a room that
|
|
* can never fill, and the server refuses their next ticket with
|
|
* `already_in_lobby`. Cleared by leaving, and by anything else that ends the
|
|
* seating.
|
|
*/
|
|
int psx_lobby_automatch_room(void);
|
|
/* Refuse a queue locally, with the reason the player sees. For when the HOST
|
|
* already knows the server would bounce the ticket. */
|
|
void psx_lobby_automatch_refuse_local(const char *why);
|
|
const char *psx_lobby_automatch_error(void);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PSX_LOBBY_CLIENT_H */
|