mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
netplay: block list, account keys, list scope and automatch for the launcher
Brings the PSX runtime level with what recomp-ui's netplay pages now ask a host for, and with the SNES runner that already answers them (snes_lobby_client.c / snes_host_lobby.c): - Chat lines, seat rows and players-online rows carry the server's opaque account key, so the launcher's ignore/block list survives a rename. - psx_lobby_set_blocks pushes that list to the server as `set_blocks`, since refusing to pair or seat the two together is the server's job; recomp-ui re-sends it on every reconnect. - list_scope_set: a LAN player no longer sees online rooms they have no connection for, and an online player no longer sees their own LAN rows. - Automatch: rulesets discovery, the relay latency probe (14-byte LE frame, type 200 -> 201, nonce-matched), queue with the disc fingerprint, the accept gate with a locally counted deadline, requeue/cancel/cooldown, and claiming automatch's `error` codes only while a ticket is in flight. Leaving an automatch room after the match: it is the server's room, has no host to rematch with, and holding the seat blocks the next ticket. - Account: init the auth client once per URL rather than every pump (the login worker reads the host), anchor the device key to the executable directory, and publish the signed-in handle as the lobby display name. mods_enabled is asserted false on PSX: every netplay launch clears the mod plan or refuses to start, and the caps a match runs are the ruleset's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FPXfGGhBBBxQPcEzaMsauF
This commit is contained in:
@@ -71,6 +71,11 @@ typedef struct PsxLobbyOnlinePlayer {
|
||||
/* 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
|
||||
|
||||
@@ -95,6 +100,9 @@ typedef struct PsxLobbyMember {
|
||||
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];
|
||||
} PsxLobbyMember;
|
||||
|
||||
/*
|
||||
@@ -137,6 +145,9 @@ typedef struct PsxLobbyChatMsg {
|
||||
* 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;
|
||||
@@ -461,6 +472,130 @@ void psx_lobby_clear_launch_pending(void);
|
||||
* 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
|
||||
|
||||
+211
-6
@@ -10037,6 +10037,139 @@ namespace {
|
||||
int ae_np_account_sign_out(void*) { return rnet_account_sign_out(); }
|
||||
int ae_np_account_set_handle(void*, const char* h) { return rnet_account_set_handle(h); }
|
||||
|
||||
/* Point the account client at the lobby host -- once per URL, not once
|
||||
* per pump: the login worker thread reads the host while a sign-in is in
|
||||
* flight, and re-initialising it 60 times a second under that read is a
|
||||
* data race for no gain. The secret is anchored to the EXECUTABLE
|
||||
* directory before the first init: its default is the bare relative name
|
||||
* "netplay_secret", resolved against the working directory, so the same
|
||||
* install signed itself out depending on where it was launched from.
|
||||
* rnet_auth migrates an old CWD-relative file into this path on first
|
||||
* load, so nobody is signed out by the move. Same shape as the SNES
|
||||
* host (snes_host_lobby.c cb_pump). */
|
||||
void ae_np_account_sync(void) {
|
||||
static std::string s_auth_url;
|
||||
const std::string& url = g_lnch_lobby_url;
|
||||
if (url.empty() || url == s_auth_url) return;
|
||||
if (s_auth_url.empty()) {
|
||||
const std::string secret =
|
||||
(exe_dir_from_argv(g_lnch_argv0 ? g_lnch_argv0 : "") /
|
||||
"netplay_secret").string();
|
||||
rnet_account_set_secret_path(secret.c_str());
|
||||
}
|
||||
s_auth_url = url;
|
||||
rnet_account_init(url.c_str());
|
||||
}
|
||||
|
||||
/* ---- list scope --------------------------------------------------------
|
||||
* The launcher forks LAN / Direct IP from online before the browser, and
|
||||
* only it knows which fork the player took. Without the scope a player
|
||||
* who chose LAN was shown online rooms they had no connection for, and
|
||||
* one who chose online was shown LAN rooms from their own machine. The
|
||||
* values are RECOMP_LAUNCHER_LIST_SCOPE_*; 0 (any) is the historical
|
||||
* merge, and what a recomp-ui without the callback leaves us in. */
|
||||
int g_lnch_list_scope = 0;
|
||||
int ae_np_list_scope_set(void*, int scope) {
|
||||
g_lnch_list_scope = scope;
|
||||
return 0;
|
||||
}
|
||||
bool ae_np_list_want_online(void) { return g_lnch_list_scope != 1; }
|
||||
bool ae_np_list_want_lan(void) { return g_lnch_list_scope != 2; }
|
||||
|
||||
/* ---- moderation ------------------------------------------------------
|
||||
* Both go to the lobby server; a LAN room has none, and the launcher
|
||||
* only offers them online. What a report contains is recomp-net's
|
||||
* (chat_report.h); the block list is the launcher's own file, pushed
|
||||
* here so the server can refuse to pair or seat the two together. */
|
||||
int ae_np_chat_report(void*, const char* const* mids, int mid_count,
|
||||
const char* reason, const char* note) {
|
||||
if (g_lnch_hosting_lan || g_lnch_joined_lan) return -1;
|
||||
return psx_lobby_report_chat(mids, mid_count, reason, note);
|
||||
}
|
||||
int ae_np_set_blocks(void*, const char* accounts) {
|
||||
if (g_lnch_hosting_lan || g_lnch_joined_lan) return -1;
|
||||
return psx_lobby_set_blocks(accounts);
|
||||
}
|
||||
|
||||
/* ---- automatch -------------------------------------------------------
|
||||
* Thin: the lobby client owns the protocol and the state machine, and
|
||||
* this only translates its vocabulary into the launcher's. The one piece
|
||||
* of POLICY here is mods_enabled -- see ae_np_automatch_queue. */
|
||||
bool ae_np_online_mode(void) {
|
||||
return !g_lnch_hosting_lan && !g_lnch_joined_lan && psx_lobby_connected();
|
||||
}
|
||||
int ae_np_automatch_available(void*) {
|
||||
if (!ae_np_online_mode()) return 0;
|
||||
/* Ask once the answer could exist. The launcher polls this every
|
||||
* frame while the netplay page is up, which is exactly when a reply
|
||||
* is useful, and the client refuses to re-send while one is
|
||||
* outstanding. */
|
||||
if (!psx_lobby_automatch_available())
|
||||
(void)psx_lobby_automatch_request_rulesets();
|
||||
return psx_lobby_automatch_available();
|
||||
}
|
||||
int ae_np_automatch_ruleset_count(void*) {
|
||||
return psx_lobby_automatch_ruleset_count();
|
||||
}
|
||||
#if defined(RECOMP_LAUNCHER_HAS_AUTOMATCH)
|
||||
int ae_np_automatch_ruleset_get(void*, int index, RecompLauncherCNetplayRuleset* out) {
|
||||
PsxLobbyRuleset r{};
|
||||
if (!out || !psx_lobby_automatch_ruleset_get(index, &r)) return 0;
|
||||
std::memset(out, 0, sizeof(*out));
|
||||
std::snprintf(out->id, sizeof(out->id), "%s", r.id);
|
||||
std::snprintf(out->label, sizeof(out->label), "%s", r.label);
|
||||
std::snprintf(out->caps_summary, sizeof(out->caps_summary), "%s", r.caps_summary);
|
||||
std::snprintf(out->game_version, sizeof(out->game_version), "%s", r.game_version);
|
||||
out->max_slots = r.max_slots > 0 ? r.max_slots : 2;
|
||||
return 1;
|
||||
}
|
||||
int ae_np_automatch_found_get(void*, RecompLauncherCNetplayFound* out) {
|
||||
PsxLobbyAutomatchFound f{};
|
||||
if (!out || !psx_lobby_automatch_found_get(&f)) return 0;
|
||||
std::memset(out, 0, sizeof(*out));
|
||||
std::snprintf(out->handle, sizeof(out->handle), "%s", f.opponent);
|
||||
std::snprintf(out->username, sizeof(out->username), "%s", f.opponent_username);
|
||||
std::snprintf(out->country, sizeof(out->country), "%s", f.opponent_country);
|
||||
std::snprintf(out->ruleset_label, sizeof(out->ruleset_label), "%s", f.ruleset_label);
|
||||
out->est_rtt_ms = f.est_rtt_ms;
|
||||
out->accept_secs_left = f.accept_secs;
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
int ae_np_automatch_queue(void*, const char* ruleset_id) {
|
||||
if (!ae_np_online_mode()) return -1;
|
||||
/* mods_enabled asserts that a SIM-AFFECTING mod feature is on locally
|
||||
* beyond what the ruleset imposes. On PSX that is never true for a
|
||||
* netplay session: every netplay launch, rematch included, goes
|
||||
* through mod_runtime_clear_for_netplay and refuses to start if the
|
||||
* plan cannot be cleared, and the caps a match runs (aspect, turbo
|
||||
* loads, BIOS, FMV skip) are the server's ruleset, settled through
|
||||
* match_caps like any host's. There is no cosmetic-exemption
|
||||
* mechanism on this runtime either, so the evidence list is empty. */
|
||||
return psx_lobby_automatch_queue(ruleset_id, 0, "") == 0 ? 0 : -1;
|
||||
}
|
||||
int ae_np_automatch_cancel(void*) { return psx_lobby_automatch_cancel(); }
|
||||
int ae_np_automatch_state(void*) { return psx_lobby_automatch_state(); }
|
||||
int ae_np_automatch_queued_secs(void*) { return psx_lobby_automatch_queued_secs(); }
|
||||
int ae_np_automatch_pool(void*) { return psx_lobby_automatch_pool(); }
|
||||
int ae_np_automatch_accept(void*, int accept) { return psx_lobby_automatch_accept(accept); }
|
||||
const char* ae_np_automatch_error(void*) { return psx_lobby_automatch_error(); }
|
||||
|
||||
/* After a match: an automatch room is the server's, not a host's. It is
|
||||
* created at both-accept, nobody can join it, and there is no host to
|
||||
* rematch with -- staying seated parks the player in a room that can
|
||||
* never fill, and the server refuses their next ticket with
|
||||
* already_in_lobby. So leave it, and tell the caller not to reopen the
|
||||
* launcher on the room. 1 when a room was left. */
|
||||
int ae_np_leave_automatch_room_after_match(void) {
|
||||
if (g_lnch_hosting_lan || g_lnch_joined_lan) return 0;
|
||||
if (!psx_lobby_automatch_room()) return 0;
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: leaving the automatch room (no host to rematch with)\n");
|
||||
(void)psx_lobby_leave();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ae_np_chat_count(void*) {
|
||||
ae_np_chat_track_room();
|
||||
if (g_lnch_hosting_lan || g_lnch_joined_lan) return g_lnch_lan_chat_count;
|
||||
@@ -10060,6 +10193,14 @@ namespace {
|
||||
if (!psx_lobby_chat_get(index, &msg)) return 0;
|
||||
std::snprintf(out->from, sizeof(out->from), "%s", msg.from);
|
||||
std::snprintf(out->text, sizeof(out->text), "%s", msg.text);
|
||||
#if defined(RECOMP_LAUNCHER_HAS_CHAT_REPORT)
|
||||
/* The server's id for the line -- what a report names. Empty for a
|
||||
* system line or an older server, and then it cannot be reported. */
|
||||
std::snprintf(out->mid, sizeof(out->mid), "%s", msg.mid);
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_PLAYER_ACCOUNT)
|
||||
std::snprintf(out->account, sizeof(out->account), "%s", msg.account);
|
||||
#endif
|
||||
out->is_local = msg.is_local;
|
||||
out->is_system = msg.is_system;
|
||||
out->seq = msg.seq;
|
||||
@@ -10085,6 +10226,14 @@ namespace {
|
||||
if (!psx_lobby_server_chat_get(index, &msg)) return 0;
|
||||
std::snprintf(out->from, sizeof(out->from), "%s", msg.from);
|
||||
std::snprintf(out->text, sizeof(out->text), "%s", msg.text);
|
||||
#if defined(RECOMP_LAUNCHER_HAS_CHAT_REPORT)
|
||||
/* The server's id for the line -- what a report names. Empty for a
|
||||
* system line or an older server, and then it cannot be reported. */
|
||||
std::snprintf(out->mid, sizeof(out->mid), "%s", msg.mid);
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_PLAYER_ACCOUNT)
|
||||
std::snprintf(out->account, sizeof(out->account), "%s", msg.account);
|
||||
#endif
|
||||
out->is_local = msg.is_local;
|
||||
out->is_system = msg.is_system;
|
||||
out->seq = msg.seq;
|
||||
@@ -10244,7 +10393,7 @@ namespace {
|
||||
ae_np_save_identity(nullptr, g_lnch_lobby_url.c_str());
|
||||
/* The auth endpoints live on the same host and port as the lobby
|
||||
* socket, so the sign-in follows whatever server the player points at. */
|
||||
rnet_account_init(g_lnch_lobby_url.c_str());
|
||||
ae_np_account_sync();
|
||||
}
|
||||
|
||||
int ae_np_connect(void*) {
|
||||
@@ -10822,11 +10971,27 @@ namespace {
|
||||
}
|
||||
|
||||
void ae_np_pump(void*) {
|
||||
psx_lobby_pump();
|
||||
/* Redeems a stored device key on the first pump, so a machine that has
|
||||
* signed in once comes up signed in with no player action. */
|
||||
rnet_account_init(g_lnch_lobby_url.c_str());
|
||||
ae_np_account_sync();
|
||||
rnet_account_pump();
|
||||
/* Publish the account name to the lobby. The lobby's display name is
|
||||
* what seats and the players-online list show, and nothing else
|
||||
* pushed the handle into it: signing in -- including the automatic
|
||||
* sign-in from a stored secret -- only updated the ACCOUNT, so a
|
||||
* signed-in player created a room and appeared under the LAN name.
|
||||
* Done here rather than at a sign-in edge because there is no single
|
||||
* such edge: interactive login, stored-secret redemption and a
|
||||
* server-side handle change all land asynchronously in the pump.
|
||||
* Comparing against the live name makes this idempotent --
|
||||
* set_display_name only re-sends hello when the value changed. */
|
||||
if (rnet_account_state() == RNET_ACCOUNT_SIGNED_IN) {
|
||||
const char* handle = rnet_account_handle();
|
||||
const char* shown = psx_lobby_display_name();
|
||||
if (handle && handle[0] && (!shown || std::strcmp(shown, handle) != 0))
|
||||
psx_lobby_set_display_name(handle);
|
||||
}
|
||||
psx_lobby_pump();
|
||||
ae_np_lan_browse_pump();
|
||||
ae_np_lan_udp_pump();
|
||||
/* Lobby UI has no Ready toggle; production WS still requires every
|
||||
@@ -10870,14 +11035,16 @@ namespace {
|
||||
}
|
||||
|
||||
int ae_np_list_count(void*) {
|
||||
return psx_lobby_list_count() + ae_np_lan_list_extra_count();
|
||||
return (ae_np_list_want_online() ? psx_lobby_list_count() : 0) +
|
||||
(ae_np_list_want_lan() ? ae_np_lan_list_extra_count() : 0);
|
||||
}
|
||||
|
||||
int ae_np_list_get(void*, int index, RecompLauncherCNetplayLobby* out) {
|
||||
if (!out) return 0;
|
||||
const int remote_count = psx_lobby_list_count();
|
||||
const int remote_count = ae_np_list_want_online() ? psx_lobby_list_count() : 0;
|
||||
if (index >= remote_count) {
|
||||
const int lan_i = index - remote_count;
|
||||
if (!ae_np_list_want_lan()) return 0;
|
||||
ae_np_lan_prune_discovered();
|
||||
if (g_lnch_lan_discovered_n > 0)
|
||||
return ae_np_lan_fill_lobby_from_discovered(lan_i, out);
|
||||
@@ -10914,6 +11081,9 @@ namespace {
|
||||
std::memset(out, 0, sizeof(*out));
|
||||
std::snprintf(out->display_name, sizeof(out->display_name), "%s", p.display_name);
|
||||
std::snprintf(out->country, sizeof(out->country), "%s", p.country);
|
||||
#if defined(RECOMP_LAUNCHER_HAS_PLAYER_ACCOUNT)
|
||||
std::snprintf(out->account, sizeof(out->account), "%s", p.account);
|
||||
#endif
|
||||
std::snprintf(out->lobby_name, sizeof(out->lobby_name), "%s", p.lobby_name);
|
||||
out->in_lobby = p.lobby_id[0] != '\0';
|
||||
out->hosting = p.hosting;
|
||||
@@ -11472,6 +11642,9 @@ namespace {
|
||||
out->memcard_has_card = mem.memcard_has_card;
|
||||
out->memcard_share = mem.memcard_share;
|
||||
std::snprintf(out->country, sizeof(out->country), "%s", mem.country);
|
||||
#if defined(RECOMP_LAUNCHER_HAS_PLAYER_ACCOUNT)
|
||||
std::snprintf(out->account, sizeof(out->account), "%s", mem.account);
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
if (ae_np_use_lan_members()) {
|
||||
@@ -11533,6 +11706,9 @@ namespace {
|
||||
out->memcard_has_card = mem.memcard_has_card;
|
||||
out->memcard_share = mem.memcard_share;
|
||||
std::snprintf(out->country, sizeof(out->country), "%s", mem.country);
|
||||
#if defined(RECOMP_LAUNCHER_HAS_PLAYER_ACCOUNT)
|
||||
std::snprintf(out->account, sizeof(out->account), "%s", mem.account);
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -12094,6 +12270,30 @@ namespace {
|
||||
g_lnch_netplay_callbacks.account_error = ae_np_account_error;
|
||||
g_lnch_netplay_callbacks.account_sign_out = ae_np_account_sign_out;
|
||||
g_lnch_netplay_callbacks.account_set_handle = ae_np_account_set_handle;
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_LIST_SCOPE)
|
||||
g_lnch_netplay_callbacks.list_scope_set = ae_np_list_scope_set;
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_CHAT_REPORT)
|
||||
g_lnch_netplay_callbacks.chat_report = ae_np_chat_report;
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_SET_BLOCKS)
|
||||
g_lnch_netplay_callbacks.set_blocks = ae_np_set_blocks;
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_AUTOMATCH)
|
||||
/* Server-run pairing. Same guard discipline as the account block:
|
||||
* the runner and the UI land in either order. */
|
||||
g_lnch_netplay_callbacks.automatch_available = ae_np_automatch_available;
|
||||
g_lnch_netplay_callbacks.automatch_ruleset_count = ae_np_automatch_ruleset_count;
|
||||
g_lnch_netplay_callbacks.automatch_ruleset_get = ae_np_automatch_ruleset_get;
|
||||
g_lnch_netplay_callbacks.automatch_queue = ae_np_automatch_queue;
|
||||
g_lnch_netplay_callbacks.automatch_cancel = ae_np_automatch_cancel;
|
||||
g_lnch_netplay_callbacks.automatch_state = ae_np_automatch_state;
|
||||
g_lnch_netplay_callbacks.automatch_queued_secs = ae_np_automatch_queued_secs;
|
||||
g_lnch_netplay_callbacks.automatch_pool = ae_np_automatch_pool;
|
||||
g_lnch_netplay_callbacks.automatch_found_get = ae_np_automatch_found_get;
|
||||
g_lnch_netplay_callbacks.automatch_accept = ae_np_automatch_accept;
|
||||
g_lnch_netplay_callbacks.automatch_error = ae_np_automatch_error;
|
||||
#endif
|
||||
g_lnch_netplay_callbacks.seat_move_self = ae_np_seat_move_self;
|
||||
g_lnch_netplay_callbacks.seat_swap_request = ae_np_seat_swap_request;
|
||||
@@ -15645,6 +15845,11 @@ soft_return_lobby:
|
||||
std::string rui_title = (game_name.empty() ? std::string("PSX") : game_name)
|
||||
+ " - Launcher";
|
||||
std::string rui_initial_disc = disc_path_str;
|
||||
/* A human-hosted room survives the match and is where a rematch
|
||||
* happens; an automatch room is the server's and cannot. Leave it
|
||||
* here, and reopen the launcher on the browser instead. */
|
||||
const int rui_resume_room =
|
||||
ae_np_leave_automatch_room_after_match() ? 0 : 1;
|
||||
|
||||
ae_rui_set_sidecar_paths(argv[0]);
|
||||
g_lnch_expected_serial = game_id;
|
||||
@@ -15822,7 +16027,7 @@ soft_return_lobby:
|
||||
ctrl_locked_mode[0],
|
||||
rui_lang_labels.empty() ? nullptr : rui_lang_labels.data(),
|
||||
(int)rui_lang_labels.size(),
|
||||
/*resume_netplay_room=*/1);
|
||||
/*resume_netplay_room=*/rui_resume_room);
|
||||
gi.discs = rui_discs.empty() ? nullptr : rui_discs.data();
|
||||
gi.num_discs = (int)rui_discs.size();
|
||||
#if defined(PSX_HAS_SETUP_WIZARD) && defined(PSX_HAS_CODEGEN_SETUP_HOST)
|
||||
|
||||
@@ -126,6 +126,22 @@ int psx_lobby_send_chat(const char *text) { (void)text; return -1; }
|
||||
int psx_lobby_report_chat(const char *const *m, int c, const char *r,
|
||||
const char *n)
|
||||
{ (void)m; (void)c; (void)r; (void)n; return -1; }
|
||||
int psx_lobby_set_blocks(const char *accounts) { (void)accounts; return -1; }
|
||||
int psx_lobby_automatch_request_rulesets(void) { return -1; }
|
||||
int psx_lobby_automatch_available(void) { return 0; }
|
||||
int psx_lobby_automatch_ruleset_count(void) { return 0; }
|
||||
int psx_lobby_automatch_ruleset_get(int i, PsxLobbyRuleset *o) { (void)i; (void)o; return 0; }
|
||||
int psx_lobby_automatch_queue(const char *r, int m, const char *e)
|
||||
{ (void)r; (void)m; (void)e; return -1; }
|
||||
int psx_lobby_automatch_cancel(void) { return -1; }
|
||||
int psx_lobby_automatch_state(void) { return PSX_LOBBY_AUTOMATCH_IDLE; }
|
||||
int psx_lobby_automatch_queued_secs(void) { return 0; }
|
||||
int psx_lobby_automatch_pool(void) { return 0; }
|
||||
int psx_lobby_automatch_found_get(PsxLobbyAutomatchFound *o) { (void)o; return 0; }
|
||||
int psx_lobby_automatch_accept(int a) { (void)a; return -1; }
|
||||
int psx_lobby_automatch_room(void) { return 0; }
|
||||
void psx_lobby_automatch_refuse_local(const char *w) { (void)w; }
|
||||
const char *psx_lobby_automatch_error(void) { return ""; }
|
||||
int psx_lobby_seat_move_self(int to_slot) { (void)to_slot; return -1; }
|
||||
int psx_lobby_seat_swap_request(int target_slot) { (void)target_slot; return -1; }
|
||||
int psx_lobby_seat_swap_incoming(char *who, size_t who_cap, int *from_slot)
|
||||
@@ -1351,6 +1367,8 @@ static void lobby_list_parse_players(const char *json)
|
||||
sizeof(g_lc.online[n].lobby_name));
|
||||
g_lc.online[n].hosting = json_get_bool(chunk, "hosting", 0);
|
||||
json_get_str(chunk, "tag", g_lc.online[n].tag, sizeof(g_lc.online[n].tag));
|
||||
json_get_str(chunk, "account", g_lc.online[n].account,
|
||||
sizeof(g_lc.online[n].account));
|
||||
json_get_str(chunk, "game_name", g_lc.online[n].game_name,
|
||||
sizeof(g_lc.online[n].game_name));
|
||||
/* Players of another title are not "online" for this one. A row
|
||||
@@ -1695,6 +1713,8 @@ static int parse_seat_array(const char *json, const char *key, int is_spectator,
|
||||
}
|
||||
json_get_str(chunk, "country", g_lc.members[n].country,
|
||||
sizeof(g_lc.members[n].country));
|
||||
json_get_str(chunk, "account", g_lc.members[n].account,
|
||||
sizeof(g_lc.members[n].account));
|
||||
if (json_extract_object(chunk, "memcard_offer", offer, sizeof(offer))) {
|
||||
g_lc.members[n].memcard_offer_valid = 1;
|
||||
g_lc.members[n].memcard_has_card =
|
||||
@@ -1761,6 +1781,699 @@ static void ingest_host_player_id(const char *json)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ── Block list ────────────────────────────────────────────────────────────── */
|
||||
|
||||
int psx_lobby_set_blocks(const char *accounts)
|
||||
{
|
||||
/* Room for the server's cap (256 ids) at 40 characters each, plus the
|
||||
* separators and the envelope. A list that would not fit is truncated at
|
||||
* a separator rather than sent malformed -- a half-written id at the end
|
||||
* would name nobody, and a malformed op would leave the server enforcing
|
||||
* nothing at all. */
|
||||
static char msg[256 * 41 + 64];
|
||||
static char list[256 * 41];
|
||||
size_t n = 0;
|
||||
int first = 1;
|
||||
const char *p = accounts ? accounts : "";
|
||||
|
||||
if (!g_lc.connected) return -1;
|
||||
list[0] = '\0';
|
||||
while (*p) {
|
||||
const char *sep = strchr(p, ';');
|
||||
size_t len = sep ? (size_t)(sep - p) : strlen(p);
|
||||
if (len && len < 40 && n + len + 8 < sizeof(list)) {
|
||||
if (!first) { list[n++] = ','; }
|
||||
list[n++] = '"';
|
||||
memcpy(list + n, p, len);
|
||||
n += len;
|
||||
list[n++] = '"';
|
||||
list[n] = '\0';
|
||||
first = 0;
|
||||
}
|
||||
if (!sep) break;
|
||||
p = sep + 1;
|
||||
}
|
||||
snprintf(msg, sizeof(msg), "{\"op\":\"set_blocks\",\"accounts\":[%s]}", list);
|
||||
queue_send(msg);
|
||||
flush_pending();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Automatch ─────────────────────────────────────────────────────────────
|
||||
*
|
||||
* State machine and wire for recomp-net-server docs/AUTOMATCH.md. The room a
|
||||
* pairing produces arrives as an ordinary `joined` and is handled by the
|
||||
* existing code; this owns the ticket, the accept gate, and the latency
|
||||
* probe the server uses to pick an input delay.
|
||||
*/
|
||||
#define AUTOMATCH_PROBE_LEN 14
|
||||
|
||||
static int set_nonblock(int fd); /* defined with the socket code below */
|
||||
|
||||
typedef struct {
|
||||
int have_rulesets; /* the server has answered once this connection */
|
||||
int rulesets_in_flight;
|
||||
int ruleset_count;
|
||||
PsxLobbyRuleset rulesets[PSX_LOBBY_MAX_RULESETS];
|
||||
|
||||
int state; /* PSX_LOBBY_AUTOMATCH_* */
|
||||
char ticket_id[PSX_LOBBY_ID_LEN];
|
||||
char match_id[PSX_LOBBY_ID_LEN];
|
||||
int queued_secs;
|
||||
int pool;
|
||||
char error[160];
|
||||
|
||||
PsxLobbyAutomatchFound found;
|
||||
/* When the offer lapses, as a monotonic timestamp rather than the count
|
||||
* the server sent. The server states the deadline once; a client that
|
||||
* stored the number and showed it unchanged would display "15s" for the
|
||||
* whole fifteen seconds, which reads as a frozen dialog. */
|
||||
uint64_t found_deadline_ms;
|
||||
|
||||
/* Where to send the latency probe, published on rulesets_ok and again on
|
||||
* automatch_queued. Kept from whichever arrived last. */
|
||||
char probe_host[128];
|
||||
int probe_port;
|
||||
unsigned probe_magic;
|
||||
int probe_type;
|
||||
/* The measurement, and the nonce that identifies our outstanding probe. */
|
||||
int rtt_ms; /* <0 = not measured yet */
|
||||
uint32_t probe_nonce;
|
||||
uint64_t probe_sent_ms; /* 0 = none outstanding */
|
||||
int probe_socket; /* -1 = not open */
|
||||
int rtt_reported; /* the server has our number */
|
||||
/* A queue op is out and its answer -- automatch_queued, or an error --
|
||||
* has not arrived. Without it a refusal of the FIRST queue attempt
|
||||
* (need_account is the common one) would arrive while state is still
|
||||
* IDLE and be filed as somebody else's error. */
|
||||
int queue_in_flight;
|
||||
/* This seat came from a pairing, not from a room somebody hosts. */
|
||||
int in_automatch_room;
|
||||
} LobbyAutomatch;
|
||||
|
||||
static LobbyAutomatch g_am = { .rtt_ms = -1, .probe_socket = -1 };
|
||||
|
||||
static void automatch_reset_queue_state(void)
|
||||
{
|
||||
g_am.state = PSX_LOBBY_AUTOMATCH_IDLE;
|
||||
g_am.queue_in_flight = 0;
|
||||
g_am.ticket_id[0] = '\0';
|
||||
g_am.match_id[0] = '\0';
|
||||
g_am.queued_secs = 0;
|
||||
g_am.pool = 0;
|
||||
g_am.rtt_reported = 0;
|
||||
g_am.found_deadline_ms = 0;
|
||||
memset(&g_am.found, 0, sizeof(g_am.found));
|
||||
}
|
||||
|
||||
static void automatch_fail(const char *why)
|
||||
{
|
||||
g_am.state = PSX_LOBBY_AUTOMATCH_FAILED;
|
||||
g_am.queue_in_flight = 0;
|
||||
snprintf(g_am.error, sizeof(g_am.error), "%s", why ? why : "automatch failed");
|
||||
fprintf(stderr, "psx_lobby: automatch failed: %s\n", g_am.error);
|
||||
}
|
||||
|
||||
static void automatch_probe_close(void)
|
||||
{
|
||||
if (g_am.probe_socket >= 0) {
|
||||
close(g_am.probe_socket);
|
||||
g_am.probe_socket = -1;
|
||||
}
|
||||
g_am.probe_sent_ms = 0;
|
||||
}
|
||||
|
||||
/* A new connection has no ticket and has not been told what queues exist:
|
||||
* the server holds both per socket. Called from `welcome` and on disconnect
|
||||
* so the availability question is re-asked, not answered from a stale yes. */
|
||||
static void automatch_on_connection_reset(void)
|
||||
{
|
||||
automatch_reset_queue_state();
|
||||
automatch_probe_close();
|
||||
g_am.have_rulesets = 0;
|
||||
g_am.rulesets_in_flight = 0;
|
||||
g_am.ruleset_count = 0;
|
||||
g_am.rtt_ms = -1;
|
||||
g_am.in_automatch_room = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The 14-byte probe: magic, type, then a nonce.
|
||||
*
|
||||
* LITTLE-ENDIAN, because that is what the relay's header is (`read_u32_le` /
|
||||
* `read_u16_le` in input_relay.rs) -- a big-endian magic is simply not this
|
||||
* protocol's magic, so the packet is dropped on the `magic` counter with no
|
||||
* reply. The nonce sits at offset 6, where an ordinary packet carries its
|
||||
* session id; the relay echoes those bytes untouched and only rewrites the
|
||||
* type (200 -> 201), so it comes back as sent.
|
||||
*/
|
||||
static void automatch_probe_pack(unsigned char *out, uint32_t nonce)
|
||||
{
|
||||
const unsigned magic = g_am.probe_magic;
|
||||
const int type = g_am.probe_type ? g_am.probe_type : 200;
|
||||
memset(out, 0, AUTOMATCH_PROBE_LEN);
|
||||
out[0] = (unsigned char)(magic & 0xFF);
|
||||
out[1] = (unsigned char)((magic >> 8) & 0xFF);
|
||||
out[2] = (unsigned char)((magic >> 16) & 0xFF);
|
||||
out[3] = (unsigned char)((magic >> 24) & 0xFF);
|
||||
out[4] = (unsigned char)(type & 0xFF);
|
||||
out[5] = (unsigned char)((type >> 8) & 0xFF);
|
||||
out[6] = (unsigned char)(nonce & 0xFF);
|
||||
out[7] = (unsigned char)((nonce >> 8) & 0xFF);
|
||||
out[8] = (unsigned char)((nonce >> 16) & 0xFF);
|
||||
out[9] = (unsigned char)((nonce >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
static int automatch_probe_send(void)
|
||||
{
|
||||
struct addrinfo hints, *res = NULL;
|
||||
unsigned char pkt[AUTOMATCH_PROBE_LEN];
|
||||
char portstr[16];
|
||||
int fd;
|
||||
|
||||
if (!g_am.probe_host[0] || g_am.probe_port <= 0) return -1;
|
||||
if (g_am.probe_sent_ms) return 0; /* one outstanding at a time */
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
snprintf(portstr, sizeof(portstr), "%d", g_am.probe_port);
|
||||
if (getaddrinfo(g_am.probe_host, portstr, &hints, &res) != 0 || !res)
|
||||
return -1;
|
||||
|
||||
fd = (int)socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
||||
if (fd < 0) { freeaddrinfo(res); return -1; }
|
||||
set_nonblock(fd);
|
||||
|
||||
/* A fresh nonce per attempt, so a late reply to a previous probe cannot
|
||||
* be timed against this one's clock and report an absurdly low number. */
|
||||
g_am.probe_nonce = (uint32_t)(lobby_mono_ms() * 2654435761u) ^ 0x9E3779B9u;
|
||||
automatch_probe_pack(pkt, g_am.probe_nonce);
|
||||
|
||||
if (sendto(fd, (const char *)pkt, (int)sizeof(pkt), 0,
|
||||
res->ai_addr, (int)res->ai_addrlen) < 0) {
|
||||
close(fd);
|
||||
freeaddrinfo(res);
|
||||
return -1;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
|
||||
automatch_probe_close();
|
||||
g_am.probe_socket = fd;
|
||||
g_am.probe_sent_ms = lobby_mono_ms();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void automatch_send_rtt(void)
|
||||
{
|
||||
char msg[128];
|
||||
if (g_am.rtt_ms < 0 || g_am.rtt_reported) return;
|
||||
snprintf(msg, sizeof(msg),
|
||||
"{\"op\":\"automatch_rtt\",\"rtt_ms\":%d}", g_am.rtt_ms);
|
||||
queue_send(msg);
|
||||
g_am.rtt_reported = 1;
|
||||
}
|
||||
|
||||
/* Polled every pump. Times the 201 reply, or gives up after 2 s. */
|
||||
static void automatch_probe_poll(void)
|
||||
{
|
||||
unsigned char buf[64];
|
||||
uint64_t now;
|
||||
|
||||
if (g_am.probe_socket < 0 || !g_am.probe_sent_ms) return;
|
||||
now = lobby_mono_ms();
|
||||
|
||||
for (;;) {
|
||||
int n = (int)recv(g_am.probe_socket, (char *)buf, (int)sizeof(buf), 0);
|
||||
if (n < 0) break;
|
||||
if (n < 10) continue;
|
||||
/* Match the nonce: the socket is unconnected and anything can arrive
|
||||
* on it, and an unrelated packet timed as our reply is a wrong number
|
||||
* reported as fact. */
|
||||
if (((uint32_t)buf[6] | (uint32_t)buf[7] << 8 |
|
||||
(uint32_t)buf[8] << 16 | (uint32_t)buf[9] << 24) != g_am.probe_nonce)
|
||||
continue;
|
||||
g_am.rtt_ms = (int)(now - g_am.probe_sent_ms);
|
||||
if (g_am.rtt_ms < 0) g_am.rtt_ms = 0;
|
||||
if (g_am.rtt_ms > 2000) g_am.rtt_ms = 2000; /* the server clamps here too */
|
||||
fprintf(stderr, "psx_lobby: automatch probe %s:%d rtt=%d ms\n",
|
||||
g_am.probe_host, g_am.probe_port, g_am.rtt_ms);
|
||||
automatch_probe_close();
|
||||
/* Queued already? Then the ticket was enqueued on an unknown latency
|
||||
* and the server is holding it out of pairing for the probe grace --
|
||||
* tell it now rather than letting the grace lapse. */
|
||||
if (g_am.state == PSX_LOBBY_AUTOMATCH_QUEUED) automatch_send_rtt();
|
||||
return;
|
||||
}
|
||||
if (now - g_am.probe_sent_ms > 2000) {
|
||||
fprintf(stderr, "psx_lobby: automatch probe timed out (%s:%d) -- "
|
||||
"queueing without a latency estimate\n",
|
||||
g_am.probe_host, g_am.probe_port);
|
||||
automatch_probe_close();
|
||||
}
|
||||
}
|
||||
|
||||
/* Walk to the next {...} in an array, copying it out. Returns 0 at ']'. */
|
||||
static int automatch_next_object(const char **pp, char *out, size_t cap)
|
||||
{
|
||||
const char *p = *pp;
|
||||
const char *start;
|
||||
int depth = 0;
|
||||
size_t n;
|
||||
|
||||
while (*p && *p != '{') {
|
||||
if (*p == ']') { *pp = p; return 0; }
|
||||
++p;
|
||||
}
|
||||
if (*p != '{') { *pp = p; return 0; }
|
||||
start = p;
|
||||
do {
|
||||
if (*p == '{') ++depth;
|
||||
else if (*p == '}') --depth;
|
||||
++p;
|
||||
} while (*p && depth > 0);
|
||||
n = (size_t)(p - start);
|
||||
if (n >= cap) n = cap - 1;
|
||||
memcpy(out, start, n);
|
||||
out[n] = '\0';
|
||||
*pp = p;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* `probe: { endpoint, magic, type }` -- where to measure the path. Published
|
||||
* on both rulesets_ok and queued, and taken from whichever arrived last. */
|
||||
static void automatch_ingest_probe(const char *obj)
|
||||
{
|
||||
char endpoint[160];
|
||||
char *colon;
|
||||
endpoint[0] = '\0';
|
||||
json_get_str(obj, "endpoint", endpoint, sizeof(endpoint));
|
||||
/* Rightmost colon: an IPv6 literal has several, and the port is last. */
|
||||
colon = strrchr(endpoint, ':');
|
||||
if (!colon || !colon[1]) return;
|
||||
*colon = '\0';
|
||||
snprintf(g_am.probe_host, sizeof(g_am.probe_host), "%s", endpoint);
|
||||
g_am.probe_port = atoi(colon + 1);
|
||||
g_am.probe_magic = (unsigned)json_get_int(obj, "magic", 0);
|
||||
g_am.probe_type = json_get_int(obj, "type", 200);
|
||||
}
|
||||
|
||||
/* `titles: [ { ..., pool: N } ]` -- this host queues one title, so the first
|
||||
* row is the one the player is waiting in. */
|
||||
static int automatch_first_pool(const char *json)
|
||||
{
|
||||
const char *p = strstr(json, "\"titles\"");
|
||||
char obj[512];
|
||||
if (!p) return g_am.pool;
|
||||
p = strchr(p, '[');
|
||||
if (!p) return g_am.pool;
|
||||
++p;
|
||||
if (!automatch_next_object(&p, obj, sizeof(obj))) return 0;
|
||||
return json_get_int(obj, "pool", 0);
|
||||
}
|
||||
|
||||
int psx_lobby_automatch_request_rulesets(void)
|
||||
{
|
||||
char msg[256];
|
||||
char gn_esc[JSON_ESC_CAP(PSX_LOBBY_NAME_LEN)];
|
||||
const char *gn = g_lc.filter_game_name;
|
||||
if (!g_lc.connected) return -1;
|
||||
if (!gn || !gn[0]) return -1;
|
||||
if (g_am.rulesets_in_flight) return 0;
|
||||
json_escape(gn, gn_esc, sizeof(gn_esc));
|
||||
snprintf(msg, sizeof(msg),
|
||||
"{\"op\":\"automatch_rulesets\",\"game_name\":\"%s\"}", gn_esc);
|
||||
queue_send(msg);
|
||||
g_am.rulesets_in_flight = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int psx_lobby_automatch_available(void)
|
||||
{
|
||||
/* Zero rulesets is a real answer and means the same as "no": this
|
||||
* deployment has none loaded for this title. Not having ASKED yet is also
|
||||
* no -- the button must not be offered on an assumption. */
|
||||
return g_am.have_rulesets && g_am.ruleset_count > 0;
|
||||
}
|
||||
|
||||
int psx_lobby_automatch_ruleset_count(void) { return g_am.ruleset_count; }
|
||||
|
||||
int psx_lobby_automatch_ruleset_get(int index, PsxLobbyRuleset *out)
|
||||
{
|
||||
if (!out || index < 0 || index >= g_am.ruleset_count) return 0;
|
||||
*out = g_am.rulesets[index];
|
||||
return 1;
|
||||
}
|
||||
|
||||
int psx_lobby_automatch_queue(const char *ruleset_id, int mods_enabled,
|
||||
const char *mod_exempt)
|
||||
{
|
||||
char msg[2048];
|
||||
char exempt_json[768];
|
||||
char gn_esc[JSON_ESC_CAP(PSX_LOBBY_NAME_LEN)];
|
||||
char gv_esc[JSON_ESC_CAP(PSX_LOBBY_VERSION_LEN)];
|
||||
char rid_esc[JSON_ESC_CAP(PSX_LOBBY_RULESET_ID_LEN)];
|
||||
const char *rid = (ruleset_id && ruleset_id[0]) ? ruleset_id
|
||||
: (g_am.ruleset_count > 0 ? g_am.rulesets[0].id : "");
|
||||
const char *disc_fp = psx_lobby_disc_fp();
|
||||
char rtt[48];
|
||||
|
||||
if (!g_lc.connected) return -1;
|
||||
if (!rid[0]) return -1;
|
||||
if (g_am.state == PSX_LOBBY_AUTOMATCH_QUEUED ||
|
||||
g_am.state == PSX_LOBBY_AUTOMATCH_FOUND ||
|
||||
g_am.state == PSX_LOBBY_AUTOMATCH_ACCEPTED ||
|
||||
g_am.queue_in_flight)
|
||||
return -1;
|
||||
/* The queue key REQUIRES a fingerprint: in `join` an empty one means
|
||||
* "legacy host, no check", and a wildcard in a queue silently pairs a
|
||||
* different dump against this one. Refuse here rather than let the server
|
||||
* answer need_disc_fp, so the reason is available before the round trip. */
|
||||
if (!disc_fp || strlen(disc_fp) != 64) {
|
||||
automatch_fail("this build cannot fingerprint its disc, so it cannot queue");
|
||||
return -1;
|
||||
}
|
||||
|
||||
json_escape(g_lc.filter_game_name, gn_esc, sizeof(gn_esc));
|
||||
json_escape(psx_lobby_game_version(), gv_esc, sizeof(gv_esc));
|
||||
json_escape(rid, rid_esc, sizeof(rid_esc));
|
||||
|
||||
/* The exemption evidence, as a JSON ARRAY of strings -- never a
|
||||
* ';'-joined string, which is valid JSON that every reader using
|
||||
* as_array() sees as empty and so fails open (recomp-net's
|
||||
* chat_report.h records the instance this repo already paid for). */
|
||||
{
|
||||
size_t o = 0;
|
||||
const char *p = mod_exempt;
|
||||
int first = 1;
|
||||
exempt_json[o++] = '[';
|
||||
while (p && *p) {
|
||||
const char *end = p;
|
||||
char entry[160];
|
||||
char esc[JSON_ESC_CAP(sizeof(entry))];
|
||||
size_t len;
|
||||
while (*end && *end != ';' && *end != '\n') ++end;
|
||||
len = (size_t)(end - p);
|
||||
if (len && len < sizeof(entry)) {
|
||||
memcpy(entry, p, len);
|
||||
entry[len] = '\0';
|
||||
json_escape(entry, esc, sizeof(esc));
|
||||
if (o + strlen(esc) + 4 < sizeof(exempt_json)) {
|
||||
if (!first) exempt_json[o++] = ',';
|
||||
exempt_json[o++] = '"';
|
||||
memcpy(exempt_json + o, esc, strlen(esc));
|
||||
o += strlen(esc);
|
||||
exempt_json[o++] = '"';
|
||||
first = 0;
|
||||
} else {
|
||||
/* Refuse rather than send a SHORT list: a truncated list
|
||||
* of exemptions is one the server approves in full while
|
||||
* the client relies on more than it declared. */
|
||||
automatch_fail("too many mod exemptions to declare");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
p = *end ? end + 1 : end;
|
||||
}
|
||||
exempt_json[o++] = ']';
|
||||
exempt_json[o] = '\0';
|
||||
}
|
||||
|
||||
/* Measure before queueing when we can: a client that probes first never
|
||||
* waits out the server's probe grace at all. */
|
||||
if (g_am.rtt_ms < 0) automatch_probe_send();
|
||||
rtt[0] = '\0';
|
||||
if (g_am.rtt_ms >= 0)
|
||||
snprintf(rtt, sizeof(rtt), ",\"rtt_ms\":%d", g_am.rtt_ms);
|
||||
|
||||
/* One title: this host runs one game. A multi-title launcher sends
|
||||
* several here, in preference order; the wire has always allowed it. */
|
||||
snprintf(msg, sizeof(msg),
|
||||
"{\"op\":\"automatch_queue\",\"titles\":[{"
|
||||
"\"game_name\":\"%s\",\"game_version\":\"%s\","
|
||||
"\"disc_fp\":\"%s\",\"ruleset_id\":\"%s\",\"max_slots\":2}],"
|
||||
"\"mods_enabled\":%s,\"mod_exempt\":%s%s}",
|
||||
gn_esc, gv_esc, disc_fp, rid_esc,
|
||||
mods_enabled ? "true" : "false", exempt_json, rtt);
|
||||
queue_send(msg);
|
||||
flush_pending();
|
||||
g_am.error[0] = '\0';
|
||||
g_am.queue_in_flight = 1;
|
||||
g_am.rtt_reported = (g_am.rtt_ms >= 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int psx_lobby_automatch_cancel(void)
|
||||
{
|
||||
if (!g_lc.connected) return -1;
|
||||
queue_send("{\"op\":\"automatch_cancel\"}");
|
||||
flush_pending();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int psx_lobby_automatch_state(void) { return g_am.state; }
|
||||
int psx_lobby_automatch_queued_secs(void) { return g_am.queued_secs; }
|
||||
int psx_lobby_automatch_pool(void) { return g_am.pool; }
|
||||
|
||||
int psx_lobby_automatch_found_get(PsxLobbyAutomatchFound *out)
|
||||
{
|
||||
if (!out || g_am.state != PSX_LOBBY_AUTOMATCH_FOUND) return 0;
|
||||
*out = g_am.found;
|
||||
/* Recomputed on every read, so the caller can poll it each frame and get
|
||||
* a live count. Never below 0: the offer is about to lapse, and a
|
||||
* negative would draw as one. */
|
||||
if (g_am.found_deadline_ms) {
|
||||
uint64_t now = lobby_mono_ms();
|
||||
out->accept_secs = now >= g_am.found_deadline_ms
|
||||
? 0
|
||||
: (int)((g_am.found_deadline_ms - now + 999ull) / 1000ull);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int psx_lobby_automatch_accept(int accept)
|
||||
{
|
||||
char msg[192];
|
||||
char mid_esc[JSON_ESC_CAP(PSX_LOBBY_ID_LEN)];
|
||||
if (!g_lc.connected) return -1;
|
||||
if (g_am.state != PSX_LOBBY_AUTOMATCH_FOUND) return -1;
|
||||
/* The match id is echoed so a late answer to a LAPSED offer is discarded
|
||||
* rather than applied to whatever offer is current by then. */
|
||||
json_escape(g_am.match_id, mid_esc, sizeof(mid_esc));
|
||||
snprintf(msg, sizeof(msg),
|
||||
"{\"op\":\"automatch_accept\",\"match_id\":\"%s\","
|
||||
"\"accept\":%s}", mid_esc, accept ? "true" : "false");
|
||||
queue_send(msg);
|
||||
flush_pending();
|
||||
if (accept) {
|
||||
g_am.state = PSX_LOBBY_AUTOMATCH_ACCEPTED;
|
||||
} else {
|
||||
/* Declining ends the ticket. The strike is the server's to record. */
|
||||
automatch_reset_queue_state();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int psx_lobby_automatch_room(void) { return g_am.in_automatch_room; }
|
||||
|
||||
void psx_lobby_automatch_refuse_local(const char *why)
|
||||
{
|
||||
automatch_fail(why);
|
||||
}
|
||||
|
||||
const char *psx_lobby_automatch_error(void) { return g_am.error; }
|
||||
|
||||
/* Inbound automatch ops. Returns 1 when `op` was one of ours. */
|
||||
static int automatch_handle_op(const char *op, const char *json)
|
||||
{
|
||||
if (strcmp(op, "automatch_rulesets_ok") == 0) {
|
||||
const char *p2 = strstr(json, "\"rulesets\"");
|
||||
char probe[256];
|
||||
int n = 0;
|
||||
g_am.have_rulesets = 1;
|
||||
g_am.rulesets_in_flight = 0;
|
||||
g_am.ruleset_count = 0;
|
||||
if (json_extract_object(json, "probe", probe, sizeof(probe)))
|
||||
automatch_ingest_probe(probe);
|
||||
if (p2) {
|
||||
p2 = strchr(p2, '[');
|
||||
if (p2) {
|
||||
++p2;
|
||||
while (*p2 && n < PSX_LOBBY_MAX_RULESETS) {
|
||||
char obj[1024];
|
||||
PsxLobbyRuleset *r = &g_am.rulesets[n];
|
||||
if (!automatch_next_object(&p2, obj, sizeof(obj))) break;
|
||||
memset(r, 0, sizeof(*r));
|
||||
json_get_str(obj, "id", r->id, sizeof(r->id));
|
||||
json_get_str(obj, "label", r->label, sizeof(r->label));
|
||||
json_get_str(obj, "caps_summary", r->caps_summary,
|
||||
sizeof(r->caps_summary));
|
||||
json_get_str(obj, "game_version", r->game_version,
|
||||
sizeof(r->game_version));
|
||||
r->max_slots = json_get_int(obj, "max_slots", 2);
|
||||
if (r->id[0]) ++n;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_am.ruleset_count = n;
|
||||
fprintf(stderr, "psx_lobby: automatch %d ruleset(s) for \"%s\"\n",
|
||||
n, g_lc.filter_game_name);
|
||||
/* Measure now rather than at queue time: a client that has already
|
||||
* probed never waits out the server's probe grace. */
|
||||
if (n > 0 && g_am.rtt_ms < 0) automatch_probe_send();
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(op, "automatch_queued") == 0) {
|
||||
char probe[256];
|
||||
g_am.state = PSX_LOBBY_AUTOMATCH_QUEUED;
|
||||
g_am.error[0] = '\0';
|
||||
g_am.queue_in_flight = 0;
|
||||
json_get_str(json, "ticket_id", g_am.ticket_id, sizeof(g_am.ticket_id));
|
||||
g_am.queued_secs = 0;
|
||||
g_am.pool = automatch_first_pool(json);
|
||||
if (json_extract_object(json, "probe", probe, sizeof(probe)))
|
||||
automatch_ingest_probe(probe);
|
||||
if (g_am.rtt_ms < 0) automatch_probe_send();
|
||||
else automatch_send_rtt();
|
||||
fprintf(stderr, "psx_lobby: automatch queued (pool=%d)\n", g_am.pool);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(op, "automatch_status") == 0) {
|
||||
/* Pushed at most 1 Hz while queued. Not a state change: a status for
|
||||
* a ticket we already gave up on must not resurrect the queue. */
|
||||
if (g_am.state != PSX_LOBBY_AUTOMATCH_QUEUED) return 1;
|
||||
g_am.queued_secs = json_get_int(json, "queued_secs", g_am.queued_secs);
|
||||
g_am.pool = automatch_first_pool(json);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(op, "automatch_found") == 0) {
|
||||
char opp[512];
|
||||
memset(&g_am.found, 0, sizeof(g_am.found));
|
||||
json_get_str(json, "match_id", g_am.match_id, sizeof(g_am.match_id));
|
||||
/* The opponent is an OBJECT: { handle, discord_username, country }.
|
||||
* Read from the extracted object, not the whole frame -- "country"
|
||||
* at top level would be found inside it either way, but "handle" is
|
||||
* theirs and nothing else's. */
|
||||
if (json_extract_object(json, "opponent", opp, sizeof(opp))) {
|
||||
json_get_str(opp, "handle", g_am.found.opponent,
|
||||
sizeof(g_am.found.opponent));
|
||||
json_get_str(opp, "discord_username", g_am.found.opponent_username,
|
||||
sizeof(g_am.found.opponent_username));
|
||||
json_get_str(opp, "country", g_am.found.opponent_country,
|
||||
sizeof(g_am.found.opponent_country));
|
||||
}
|
||||
json_get_str(json, "ruleset_id", g_am.found.ruleset_id,
|
||||
sizeof(g_am.found.ruleset_id));
|
||||
json_get_str(json, "ruleset_label", g_am.found.ruleset_label,
|
||||
sizeof(g_am.found.ruleset_label));
|
||||
g_am.found.est_rtt_ms = json_get_int(json, "est_rtt_ms", -1);
|
||||
g_am.found.accept_secs = json_get_int(json, "accept_secs", 15);
|
||||
g_am.found_deadline_ms =
|
||||
lobby_mono_ms() + (uint64_t)(g_am.found.accept_secs > 0
|
||||
? g_am.found.accept_secs : 0) * 1000ull;
|
||||
g_am.state = PSX_LOBBY_AUTOMATCH_FOUND;
|
||||
fprintf(stderr, "psx_lobby: automatch found opponent=\"%s\" "
|
||||
"est_rtt=%d ms, %d s to answer\n",
|
||||
g_am.found.opponent, g_am.found.est_rtt_ms,
|
||||
g_am.found.accept_secs);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(op, "automatch_accept_ok") == 0) {
|
||||
/* The echo carries WHICH answer was acknowledged, and honouring it is
|
||||
* not optional: setting ACCEPTED unconditionally reopened the gate on
|
||||
* a player who had just declined. A declined ack ends the ticket
|
||||
* here; the server's automatch_cancelled follows and is idempotent
|
||||
* with this. */
|
||||
if (json_get_bool(json, "accept", 1)) {
|
||||
if (g_am.state == PSX_LOBBY_AUTOMATCH_FOUND ||
|
||||
g_am.state == PSX_LOBBY_AUTOMATCH_ACCEPTED)
|
||||
g_am.state = PSX_LOBBY_AUTOMATCH_ACCEPTED;
|
||||
} else {
|
||||
automatch_reset_queue_state();
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(op, "automatch_requeue") == 0) {
|
||||
/* The other side declined or let it lapse. Back to waiting, with the
|
||||
* ticket intact -- this is not a failure and must not read as one.
|
||||
* `queued:false` (lobby_limit) is the one case the ticket is gone. */
|
||||
if (json_get_bool(json, "queued", 1)) {
|
||||
g_am.state = PSX_LOBBY_AUTOMATCH_QUEUED;
|
||||
memset(&g_am.found, 0, sizeof(g_am.found));
|
||||
g_am.found_deadline_ms = 0;
|
||||
g_am.pool = automatch_first_pool(json);
|
||||
fprintf(stderr, "psx_lobby: automatch re-queued (the offer lapsed)\n");
|
||||
} else {
|
||||
automatch_fail("The server is out of rooms -- try again shortly");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(op, "automatch_cancelled") == 0) {
|
||||
char reason[32];
|
||||
int cooldown;
|
||||
reason[0] = '\0';
|
||||
json_get_str(json, "reason", reason, sizeof(reason));
|
||||
cooldown = json_get_int(json, "cooldown_secs", 0);
|
||||
automatch_reset_queue_state();
|
||||
/* A lapsed offer the player never answered is worth a line: the gate
|
||||
* simply vanishes otherwise, and the cooldown it cost is invisible. */
|
||||
if (strcmp(reason, "timeout") == 0 || strcmp(reason, "declined") == 0) {
|
||||
if (cooldown > 0) {
|
||||
char line[96];
|
||||
snprintf(line, sizeof(line),
|
||||
"%d Second Cooldown For Declining", cooldown);
|
||||
automatch_fail(line);
|
||||
} else if (strcmp(reason, "timeout") == 0) {
|
||||
automatch_fail("The offer lapsed before you answered");
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(op, "automatch_rtt_ok") == 0) {
|
||||
return 1; /* acknowledgement only */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* An automatch refusal arrives as a plain `error`, so it has to be claimed
|
||||
* or it would be filed as a join failure and the queue would sit waiting for
|
||||
* a pairing that was never going to come. Only while an attempt is actually
|
||||
* in flight: these codes are automatch's, but `error` is everyone's.
|
||||
* Returns 1 when claimed. */
|
||||
static int automatch_claim_error(const char *code, const char *json)
|
||||
{
|
||||
const char *why = NULL;
|
||||
char line[160];
|
||||
if (!(g_am.queue_in_flight ||
|
||||
g_am.state == PSX_LOBBY_AUTOMATCH_QUEUED ||
|
||||
g_am.state == PSX_LOBBY_AUTOMATCH_FOUND ||
|
||||
g_am.state == PSX_LOBBY_AUTOMATCH_ACCEPTED))
|
||||
return 0;
|
||||
if (!strcmp(code, "need_account")) why = "Sign in to use automatch";
|
||||
else if (!strcmp(code, "automatch_off")) why = "This server has no automatch queues";
|
||||
else if (!strcmp(code, "already_queued")) why = "This account is already in a queue";
|
||||
else if (!strcmp(code, "already_in_lobby")) why = "Leave the room first";
|
||||
else if (!strcmp(code, "unknown_ruleset")) why = "That queue type is gone -- refresh";
|
||||
else if (!strcmp(code, "need_disc_fp")) why = "The server needs a disc fingerprint this build did not send";
|
||||
else if (!strcmp(code, "version_not_pooled")) why = "This release is not the one this queue pools";
|
||||
else if (!strcmp(code, "mods_not_pooled")) why = "Turn off sim-affecting mods to queue";
|
||||
else if (!strcmp(code, "mod_not_approved")) why = "A mod exemption is not on this queue's approved list";
|
||||
else if (!strcmp(code, "slots_not_pooled")) why = "Automatch is two-player only";
|
||||
else if (!strcmp(code, "queue_full")) why = "The queue is full -- try again shortly";
|
||||
else if (!strcmp(code, "cooldown")) {
|
||||
int retry = json_get_int(json, "retry_secs", 0);
|
||||
if (retry > 0)
|
||||
snprintf(line, sizeof(line), "%d Second Cooldown For Declining", retry);
|
||||
else
|
||||
snprintf(line, sizeof(line), "Cooldown For Declining");
|
||||
why = line;
|
||||
}
|
||||
if (!why) return 0;
|
||||
automatch_fail(why);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void handle_server_json(const char *json);
|
||||
|
||||
/* Parse complete unmasked server text frames from ws_pending; leave remainder. */
|
||||
@@ -1807,7 +2520,7 @@ static void drain_ws_pending(void)
|
||||
}
|
||||
|
||||
static void chat_push(const char *player_id, const char *from, const char *text,
|
||||
const char *mid, int is_system)
|
||||
const char *mid, const char *account, int is_system)
|
||||
{
|
||||
PsxLobbyChatMsg *m;
|
||||
int idx;
|
||||
@@ -1826,6 +2539,8 @@ static void chat_push(const char *player_id, const char *from, const char *text,
|
||||
snprintf(m->text, sizeof(m->text), "%s", text);
|
||||
snprintf(m->mid, sizeof(m->mid), "%s",
|
||||
(mid && !is_system) ? mid : "");
|
||||
snprintf(m->account, sizeof(m->account), "%s",
|
||||
(account && !is_system) ? account : "");
|
||||
/* Masked on arrival, whatever relayed it: the server already did this,
|
||||
* an older server did not, and the rule is that nothing unmasked is
|
||||
* ever shown. */
|
||||
@@ -1837,7 +2552,7 @@ static void chat_push(const char *player_id, const char *from, const char *text,
|
||||
}
|
||||
|
||||
static void schat_push(const char *player_id, const char *from, const char *text,
|
||||
const char *mid)
|
||||
const char *mid, const char *account)
|
||||
{
|
||||
PsxLobbyChatMsg *m;
|
||||
int idx;
|
||||
@@ -1855,6 +2570,7 @@ static void schat_push(const char *player_id, const char *from, const char *text
|
||||
snprintf(m->from, sizeof(m->from), "%s", from ? from : "");
|
||||
snprintf(m->text, sizeof(m->text), "%s", text);
|
||||
snprintf(m->mid, sizeof(m->mid), "%s", mid ? mid : "");
|
||||
snprintf(m->account, sizeof(m->account), "%s", account ? account : "");
|
||||
(void)rnet_chat_filter_apply(m->text, sizeof(m->text));
|
||||
m->is_local = (g_lc.player_id[0] && player_id &&
|
||||
strcmp(player_id, g_lc.player_id) == 0) ? 1 : 0;
|
||||
@@ -1906,6 +2622,9 @@ static void handle_server_json(const char *json)
|
||||
json_get_str(json, "op", op, sizeof(op));
|
||||
if (strcmp(op, "welcome") == 0) {
|
||||
json_get_str(json, "player_id", g_lc.player_id, sizeof(g_lc.player_id));
|
||||
/* A fresh socket: the server holds the ticket and the ruleset answer
|
||||
* per connection, so neither survives from the last one. */
|
||||
automatch_on_connection_reset();
|
||||
{
|
||||
/* Say who we are AND what we are playing in the first message:
|
||||
* the server scopes players-online and server chat by title, and
|
||||
@@ -2158,6 +2877,15 @@ static void handle_server_json(const char *json)
|
||||
}
|
||||
if (strcmp(op, "joined") == 0) {
|
||||
psx_lobby_chat_clear();
|
||||
/* A ticket that reached a room is spent: `joined` is that moment for
|
||||
* automatch (AUTOMATCH.md 8). Recorded before the reset below, which
|
||||
* erases the evidence -- ACCEPTED (or FOUND, if the pair resolved in
|
||||
* the same breath) is the only signal that this seat came from a
|
||||
* queue rather than from somebody's room. */
|
||||
g_am.in_automatch_room =
|
||||
(g_am.state == PSX_LOBBY_AUTOMATCH_ACCEPTED ||
|
||||
g_am.state == PSX_LOBBY_AUTOMATCH_FOUND);
|
||||
automatch_reset_queue_state();
|
||||
g_lc.in_lobby = 1;
|
||||
g_lc.is_host = 0;
|
||||
g_lc.join.ok = 1;
|
||||
@@ -2308,15 +3036,18 @@ static void handle_server_json(const char *json)
|
||||
char from_id[PSX_LOBBY_ID_LEN];
|
||||
char from[PSX_LOBBY_NAME_LEN];
|
||||
char mid[40];
|
||||
char account[PSX_LOBBY_ID_LEN];
|
||||
text[0] = '\0';
|
||||
from_id[0] = '\0';
|
||||
from[0] = '\0';
|
||||
mid[0] = '\0';
|
||||
account[0] = '\0';
|
||||
json_get_str(json, "text", text, sizeof(text));
|
||||
json_get_str(json, "from_player_id", from_id, sizeof(from_id));
|
||||
json_get_str(json, "from", from, sizeof(from));
|
||||
json_get_str(json, "mid", mid, sizeof(mid));
|
||||
schat_push(from_id, from, text, mid);
|
||||
json_get_str(json, "from_account", account, sizeof(account));
|
||||
schat_push(from_id, from, text, mid, account);
|
||||
return;
|
||||
}
|
||||
if (strcmp(op, "chat") == 0) {
|
||||
@@ -2324,15 +3055,19 @@ static void handle_server_json(const char *json)
|
||||
char from_id[PSX_LOBBY_ID_LEN];
|
||||
char from[PSX_LOBBY_NAME_LEN];
|
||||
char mid[40];
|
||||
char account[PSX_LOBBY_ID_LEN];
|
||||
text[0] = '\0';
|
||||
from_id[0] = '\0';
|
||||
from[0] = '\0';
|
||||
mid[0] = '\0';
|
||||
account[0] = '\0';
|
||||
json_get_str(json, "text", text, sizeof(text));
|
||||
json_get_str(json, "from_player_id", from_id, sizeof(from_id));
|
||||
json_get_str(json, "from", from, sizeof(from));
|
||||
json_get_str(json, "mid", mid, sizeof(mid));
|
||||
chat_push(from_id, from, text, mid, json_get_bool(json, "system", 0));
|
||||
json_get_str(json, "from_account", account, sizeof(account));
|
||||
chat_push(from_id, from, text, mid, account,
|
||||
json_get_bool(json, "system", 0));
|
||||
return;
|
||||
}
|
||||
if (strcmp(op, "signal") == 0) {
|
||||
@@ -2364,11 +3099,17 @@ static void handle_server_json(const char *json)
|
||||
(void)flag;
|
||||
return;
|
||||
}
|
||||
if (automatch_handle_op(op, json)) {
|
||||
return;
|
||||
}
|
||||
if (strcmp(op, "error") == 0) {
|
||||
char code[64];
|
||||
json_get_str(json, "code", code, sizeof(code));
|
||||
strncpy(g_lc.join.last_error, code, sizeof(g_lc.join.last_error) - 1);
|
||||
g_lc.join.last_error[sizeof(g_lc.join.last_error) - 1] = '\0';
|
||||
if (automatch_claim_error(code, json)) {
|
||||
return;
|
||||
}
|
||||
/* Create/join failures are fatal to the seat. In-lobby ops (kick/move
|
||||
* on an older server, not_host, …) must not clear join.ok or the room
|
||||
* looks abandoned after a rejected host action. */
|
||||
@@ -2388,6 +3129,10 @@ 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();
|
||||
/* Leaving a room must never leave a queue ticket looking live, and
|
||||
* no seat means no automatch room either. */
|
||||
automatch_reset_queue_state();
|
||||
g_am.in_automatch_room = 0;
|
||||
g_lc.swap_in_valid = 0;
|
||||
g_lc.swap_out = 0;
|
||||
g_lc.ice_rtt_suspended = 0;
|
||||
@@ -2805,6 +3550,7 @@ void psx_lobby_disconnect(void)
|
||||
{
|
||||
/* Never block the UI on DNS/connect — cancel and let pump reap. */
|
||||
lobby_cancel_connect_async();
|
||||
automatch_on_connection_reset();
|
||||
|
||||
g_lc.ice_rtt_suspended = 0;
|
||||
lobby_ice_rtt_close();
|
||||
@@ -3153,6 +3899,7 @@ static void lobby_ice_rtt_tick(void)
|
||||
void psx_lobby_pump(void)
|
||||
{
|
||||
char buf[4096];
|
||||
automatch_probe_poll();
|
||||
#if defined(_WIN32)
|
||||
int n;
|
||||
#else
|
||||
|
||||
Reference in New Issue
Block a user