mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
Merge branch 'review/alex-pr445-head-20260930' into review/alex-netplay-bundle-20260930
This commit is contained in:
+26
-14
@@ -135,25 +135,37 @@ netplay guest sandbox remains `<memcard_dir>/netplay/` (unscoped).
|
||||
## Netplay lobby settle
|
||||
|
||||
Online and LAN lobbies advertise a per-peer BIOS offer and freeze a single
|
||||
session BIOS at host Start (`openbios` or `scph1001`):
|
||||
session BIOS at host Start (`openbios` or `scph1001`). The token `scph1001`
|
||||
means "retail", not SCPH-1001: each offer also names the retail image the peer
|
||||
would boot by its CRC-32 (`retail_crc`), and a retail session carries the CRC
|
||||
every peer must boot (`session_bios_crc`). The rule lives in
|
||||
`runtime/src/netplay_bios_settle.c`.
|
||||
|
||||
- **Online:** `bios_offer` on `set_ready` → host publishes
|
||||
`match_caps.session_bios`.
|
||||
- **LAN:** peers append offer fields on `MOTK3 JOIN`; host broadcasts them on
|
||||
`MOTK4 UPDATE` and includes the settled token on `MOTK1 START`.
|
||||
- **Online:** `bios_offer` (with `retail_crc`) on `set_ready` → host publishes
|
||||
`match_caps.session_bios` and `match_caps.session_bios_crc`.
|
||||
- **LAN:** peers append offer fields on `MOTK3 JOIN` (the CRC is line 6 of the
|
||||
tail); host broadcasts the flags on `MOTK4 UPDATE` and puts the settled
|
||||
token and CRC on `MOTK1 START` (lines 5 and 7).
|
||||
|
||||
Settle rule (same for both):
|
||||
|
||||
- **OpenBIOS** if any seated peer prefers OpenBIOS, or any peer cannot run
|
||||
SCPH-1001 (no linked retail backend and/or no validated dump), or a peer
|
||||
sends no offer (legacy client).
|
||||
- **SCPH-1001** only when every seated peer can run it and nobody selected
|
||||
OpenBIOS.
|
||||
- **Retail** only when every seated peer offers a retail image and all the
|
||||
CRCs are equal, and either the host prefers retail or nobody selected
|
||||
OpenBIOS. A peer with no offer, no dump or no CRC (an older client) rules
|
||||
retail out.
|
||||
- Otherwise **OpenBIOS**, when every peer that sent an offer links it.
|
||||
- Otherwise retail, if it is possible.
|
||||
- Otherwise the host **refuses to start** and the launcher says which images
|
||||
differ ("Players use different BIOS images (SCPH-5552 and SCPH-1001) …").
|
||||
|
||||
Every peer applies that session BIOS before boot. Mixed BIOSes are invalid for
|
||||
rollback (kernel RAM layout differs). If the session settles to SCPH-1001 but a
|
||||
peer has no validated dump, that peer **aborts the launch** rather than silently
|
||||
falling back to OpenBIOS (which would desync immediately).
|
||||
Every peer applies that session BIOS before boot, and for retail it boots only
|
||||
a dump whose CRC is the settled one. Mixed BIOSes are invalid for rollback
|
||||
(kernel RAM layout differs): before this rule an SCPH-5552 peer and an SCPH-1001
|
||||
peer both offered `scph1001`, each booted its own image, and the boot digest
|
||||
never matched. If the session settles to retail but a peer has no matching
|
||||
dump, that peer **aborts the launch** rather than silently falling back
|
||||
(which would desync immediately). A session from an older host carries no CRC;
|
||||
peers then accept any retail dump, as before.
|
||||
|
||||
Session BIOS is **ephemeral**: it affects only that match’s runtime boot. It
|
||||
does **not** rewrite `bios.cfg`, `settings.toml`, or the launcher Settings BIOS
|
||||
|
||||
+19
-6
@@ -211,15 +211,28 @@ device.
|
||||
|
||||
Rollback and delay-sync both carry multitap pad bytes.
|
||||
|
||||
**BIOS settle:** each peer advertises a BIOS offer (can run OpenBIOS /
|
||||
SCPH-1001, and whether OpenBIOS is selected) — online on ready, LAN on JOIN.
|
||||
At Start the host freezes one session BIOS (`openbios` or `scph1001`) via
|
||||
`match_caps.session_bios` (online) or the `MOTK1 START` line (LAN). The session
|
||||
uses OpenBIOS unless every seated peer can run SCPH-1001 and nobody selected
|
||||
OpenBIOS. Peers that cannot apply a settled SCPH-1001 abort instead of falling
|
||||
**BIOS settle:** each peer advertises a BIOS offer (can run OpenBIOS, has a
|
||||
retail dump and that dump's CRC-32, and whether OpenBIOS is selected) — online
|
||||
on ready, LAN on JOIN. At Start the host freezes one session BIOS (`openbios`,
|
||||
or `scph1001` = retail with the image's CRC) via `match_caps.session_bios` /
|
||||
`session_bios_crc` (online) or the `MOTK1 START` lines (LAN). Retail is used
|
||||
only when every seated peer has the same retail image; otherwise OpenBIOS if
|
||||
everyone links it; otherwise the host refuses to start and says which images
|
||||
differ. Peers that cannot apply the settled BIOS abort instead of falling
|
||||
back. That choice boots the match only — it does not change each peer’s saved
|
||||
BIOS preference. See `docs/BIOS_SELECTION.md` (Netplay lobby settle).
|
||||
|
||||
**Why a match ended:** a match that ends early returns every peer to the lobby
|
||||
(`netplay_soft_exit`), and the launcher's status line says why, from
|
||||
`runtime/src/netplay_exit_reason.c`: the other player left, the shared save
|
||||
state failed or timed out, the link timed out, the games stopped running in
|
||||
step, or the consoles started differently. The last one is the rollback boot
|
||||
digest: when both peers' tick-0 digests are known and differ, and the same pair
|
||||
holds for 3 s (`NETPLAY_BOOT_MISMATCH_GRACE_MS`), the match ends at once instead
|
||||
of waiting out the 20 s admit-stall watchdog. A peer that receives the other
|
||||
side's BYE while it sees the same mismatch reports the mismatch, not a
|
||||
disconnect. Window close and Escape end the match with no message.
|
||||
|
||||
---
|
||||
|
||||
## Disc identity for multi-track titles
|
||||
|
||||
+14
-1
@@ -23,6 +23,18 @@ psxrecomp_add_runtime_target(psx-runtime
|
||||
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
|
||||
|
||||
if(BUILD_TESTING)
|
||||
# Netplay: which BIOS a match boots, and its wire helpers.
|
||||
add_executable(netplay_bios_settle_test
|
||||
tests/test_netplay_bios_settle.c
|
||||
src/netplay_bios_settle.c)
|
||||
target_include_directories(netplay_bios_settle_test PRIVATE include)
|
||||
add_test(NAME netplay_bios_settle_test COMMAND netplay_bios_settle_test)
|
||||
# Netplay: why a match ended, and when a boot-digest mismatch is final.
|
||||
add_executable(netplay_exit_reason_test
|
||||
tests/test_netplay_exit_reason.c
|
||||
src/netplay_exit_reason.c)
|
||||
target_include_directories(netplay_exit_reason_test PRIVATE include)
|
||||
add_test(NAME netplay_exit_reason_test COMMAND netplay_exit_reason_test)
|
||||
add_executable(ws_scene_hold_test tests/test_ws_scene_hold.c)
|
||||
target_include_directories(ws_scene_hold_test PRIVATE include)
|
||||
add_test(NAME ws_scene_hold_test COMMAND ws_scene_hold_test)
|
||||
@@ -762,7 +774,8 @@ if(BUILD_TESTING)
|
||||
# is needed and none can drift from what the runtime links.
|
||||
target_sources(lobby_chat_ring_test PRIVATE
|
||||
"${PSXRECOMP_ROOT}/runtime/src/lobby_ws/rnet_ws.c"
|
||||
"${PSXRECOMP_ROOT}/runtime/src/lobby_ws/rnet_sha1.c")
|
||||
"${PSXRECOMP_ROOT}/runtime/src/lobby_ws/rnet_sha1.c"
|
||||
"${PSXRECOMP_ROOT}/runtime/src/netplay_bios_settle.c")
|
||||
target_link_libraries(lobby_chat_ring_test PRIVATE recomp_net)
|
||||
add_test(NAME lobby_chat_ring_test COMMAND lobby_chat_ring_test)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/* netplay_bios_settle.h — which BIOS every peer boots for a netplay match.
|
||||
*
|
||||
* Each seat offers what it can boot: the bundled OpenBIOS and/or one retail
|
||||
* image, named by the CRC-32 of the file it would actually load. The host
|
||||
* settles one BIOS for the room from those offers and sends the result to
|
||||
* every peer, which then boots only that image.
|
||||
*
|
||||
* The wire token for retail is still "scph1001" so older peers read it, but it
|
||||
* means "the retail image named by retail_crc", not SCPH-1001. Before this rule
|
||||
* the token was the whole identity: an SCPH-5552 peer and an SCPH-1001 peer
|
||||
* both offered "scph1001", each booted its own dump, and the rollback boot
|
||||
* digest never matched.
|
||||
*
|
||||
* Rule (netplay_bios_settle):
|
||||
* 1. Retail is possible only when every seat offered a retail image and all
|
||||
* the CRCs are equal. A seat with no offer, no dump, or no CRC (an older
|
||||
* client) makes retail impossible.
|
||||
* 2. Retail wins when it is possible and the host prefers it, or when nobody
|
||||
* prefers OpenBIOS.
|
||||
* 3. Otherwise OpenBIOS, when every seat that sent an offer links it. A seat
|
||||
* with no offer is an older client and is assumed to link it, as before.
|
||||
* 4. Otherwise retail, if it is possible.
|
||||
* 5. Otherwise there is no BIOS every seat can boot: the host must not start.
|
||||
*/
|
||||
#ifndef NETPLAY_BIOS_SETTLE_H
|
||||
#define NETPLAY_BIOS_SETTLE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct NetplayBiosSeat {
|
||||
int offered; /* 0 = no offer received (older client / not ready) */
|
||||
int can_openbios; /* links the bundled OpenBIOS backend */
|
||||
int can_retail; /* has a retail dump a linked backend accepts */
|
||||
int prefer_openbios; /* the player picked OpenBIOS */
|
||||
uint32_t retail_crc; /* CRC-32 of the retail file it would boot; 0 = not sent */
|
||||
int is_host;
|
||||
} NetplayBiosSeat;
|
||||
|
||||
enum {
|
||||
NETPLAY_BIOS_OPENBIOS = 0,
|
||||
NETPLAY_BIOS_RETAIL = 1,
|
||||
NETPLAY_BIOS_NONE = 2 /* no BIOS every seat can boot */
|
||||
};
|
||||
|
||||
typedef struct NetplayBiosSettle {
|
||||
int kind; /* NETPLAY_BIOS_* */
|
||||
uint32_t retail_crc; /* RETAIL: the image every seat boots */
|
||||
/* NONE: two seats that cannot share a retail image (-1 when unused).
|
||||
* seat_b is -1 when seat_a simply has no usable retail offer. */
|
||||
int seat_a;
|
||||
int seat_b;
|
||||
} NetplayBiosSettle;
|
||||
|
||||
NetplayBiosSettle netplay_bios_settle(const NetplayBiosSeat *seats, int count);
|
||||
|
||||
/* Wire token for a settled kind: "openbios", "scph1001" (retail), or "". */
|
||||
const char *netplay_bios_token(int kind);
|
||||
|
||||
/* retail_crc on the wire: eight lowercase hex digits, "" for 0. */
|
||||
void netplay_bios_format_crc(uint32_t crc, char *out, size_t cap);
|
||||
/* Parses eight hex digits (surrounding spaces allowed). Returns 0 for
|
||||
* anything else, which callers treat as "not sent". */
|
||||
uint32_t netplay_bios_parse_crc(const char *text);
|
||||
|
||||
/* Short player-facing name for a retail image CRC ("SCPH-5552"), or the CRC
|
||||
* in hex ("BIOS d786f0b9") when the image is not in psx_bios_known_images.h. */
|
||||
void netplay_bios_describe_crc(uint32_t crc, char *out, size_t cap);
|
||||
|
||||
/* One line explaining a NONE result, for the launcher's status line. */
|
||||
void netplay_bios_describe_refusal(const NetplayBiosSettle *s,
|
||||
const NetplayBiosSeat *seats, int count,
|
||||
char *out, size_t cap);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NETPLAY_BIOS_SETTLE_H */
|
||||
@@ -0,0 +1,37 @@
|
||||
/* netplay_exit_reason.h — why a netplay match ended, in words.
|
||||
*
|
||||
* A match that ends early returns every player to the lobby. The runtime knew
|
||||
* why (netplay_soft_exit's origin) but only printed it to stdout, so players
|
||||
* saw the lobby again with no reason. These map an origin to one sentence for
|
||||
* the launcher's status line, and decide when a boot-digest mismatch is final.
|
||||
*/
|
||||
#ifndef NETPLAY_EXIT_REASON_H
|
||||
#define NETPLAY_EXIT_REASON_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* The player-facing sentence for a netplay_soft_exit origin, or NULL when the
|
||||
* player ended the match themselves (window close, Escape) or the origin is
|
||||
* unknown. */
|
||||
const char *netplay_exit_reason_text(const char *origin);
|
||||
|
||||
/* The rollback boot digests of the two peers are both known and different.
|
||||
* The peers booted differently, and waiting does not fix that; before this change
|
||||
* only the generic 20 s admit-stall watchdog ended it. The mismatch is final
|
||||
* once the same pair of digests has held for grace_ms (the short wait lets a
|
||||
* stale peer digest be replaced, and the peer reach the same verdict).
|
||||
* mismatch_since_ms is 0 when there is no mismatch. */
|
||||
int netplay_boot_mismatch_final(uint32_t mismatch_since_ms, uint32_t now_ms,
|
||||
uint32_t grace_ms);
|
||||
|
||||
#define NETPLAY_BOOT_MISMATCH_GRACE_MS 3000u
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NETPLAY_EXIT_REASON_H */
|
||||
@@ -94,6 +94,9 @@ typedef struct PsxLobbyMember {
|
||||
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 */
|
||||
@@ -114,6 +117,9 @@ typedef struct PsxLobbyBiosOffer {
|
||||
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;
|
||||
|
||||
/*
|
||||
@@ -181,8 +187,12 @@ typedef struct PsxLobbyMatchCaps {
|
||||
* 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). */
|
||||
/* 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;
|
||||
} PsxLobbyMatchCaps;
|
||||
|
||||
typedef struct PsxLobbyJoinInfo {
|
||||
@@ -213,7 +223,9 @@ typedef struct PsxLobbyJoinInfo {
|
||||
/* Launch: the host watches from the gallery but runs the match from
|
||||
* session slot 0 (pad muted); player seats sit at lobby seat + 1. */
|
||||
int host_spectates;
|
||||
char last_error[64]; /* need_password | bad_password | … */
|
||||
/* 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:
|
||||
@@ -446,14 +458,22 @@ 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):
|
||||
* OpenBIOS if anyone cannot run SCPH-1001 (missing offer ⇒ cannot);
|
||||
* else SCPH-1001 when the host prefers retail and every peer can;
|
||||
* else OpenBIOS if anyone prefers OpenBIOS; else SCPH-1001.
|
||||
* Host preference wins over guest OpenBIOS picks when all can SCPH.
|
||||
* Writes "openbios" or "scph1001" into out. Returns 0 on success.
|
||||
* 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);
|
||||
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,
|
||||
|
||||
@@ -152,6 +152,11 @@ void psx_netplay_rb_pump(void);
|
||||
* dig0 not received. Latches open once both digests match (sticky latches —
|
||||
* HC ring may age tick-0 out; must not re-stall after sync). */
|
||||
int psx_netplay_rb_boot_dig0_gate(void);
|
||||
/* psx_host_mono_ms() since when the peers' boot digests have been known and
|
||||
* different (the same pair throughout), or 0. The watchdog ends the match once
|
||||
* that has lasted NETPLAY_BOOT_MISMATCH_GRACE_MS: the peers booted different
|
||||
* BIOS images or boot settings, and waiting cannot fix it. */
|
||||
uint32_t psx_netplay_rb_boot_dig0_mismatch_since_ms(void);
|
||||
/* Record dig0 cores outside the HC ring (emit / peer FC drain). */
|
||||
void psx_netplay_rb_boot_dig0_note_local(uint32_t core);
|
||||
void psx_netplay_rb_boot_dig0_note_peer(uint32_t core);
|
||||
|
||||
@@ -429,6 +429,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_bios_backend.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_netplay.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_lobby_client.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/netplay_bios_settle.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/netplay_exit_reason.c
|
||||
${PSXRECOMP_ROOT}/recompiler/src/config_loader.cpp
|
||||
${PSXRECOMP_ROOT}/recompiler/src/ps1_exe_parser.cpp
|
||||
# (sljit Tier-2 in-process JIT backend removed 2026-07-15 — was disabled by
|
||||
|
||||
+213
-75
@@ -61,6 +61,9 @@ extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "psx_netplay_rb.h"
|
||||
#include "psx_selfcheck.h"
|
||||
#include "psx_lobby_client.h"
|
||||
#include "netplay_bios_settle.h"
|
||||
#include "netplay_exit_reason.h"
|
||||
#include "host_time.h"
|
||||
#if defined(PSX_HAS_RECOMP_NET)
|
||||
#include "recomp_net/auth.h"
|
||||
#include "recomp_net/chat_filter.h" /* chat profanity mask, LAN rooms too */
|
||||
@@ -2982,13 +2985,19 @@ static std::filesystem::path discover_retail_bios_near(const char* argv0) {
|
||||
|
||||
/* Match-only BIOS from lobby `session_bios`. Never writes bios.cfg / settings.
|
||||
* Returns true when session_bios is a known settle token.
|
||||
* *out_path empty ⇒ OpenBIOS for this match; otherwise a validated retail dump. */
|
||||
* *out_path empty ⇒ OpenBIOS for this match; otherwise a validated retail dump.
|
||||
* A retail match boots only the image named by retail_crc, so every
|
||||
* peer runs the same BIOS; retail_crc 0 (an older host) accepts any retail
|
||||
* dump, as before. *out_image_id, when given, names the image found. */
|
||||
static bool resolve_match_session_bios_path(
|
||||
const char* session_bios,
|
||||
uint32_t retail_crc,
|
||||
const std::filesystem::path& preferred_hint,
|
||||
const char* launcher_bios_path,
|
||||
const char* argv0,
|
||||
std::filesystem::path* out_path) {
|
||||
std::filesystem::path* out_path,
|
||||
const char** out_image_id = nullptr) {
|
||||
if (out_image_id) *out_image_id = nullptr;
|
||||
if (!out_path || !session_bios || !session_bios[0])
|
||||
return false;
|
||||
if (std::strcmp(session_bios, "openbios") == 0) {
|
||||
@@ -3004,8 +3013,11 @@ static bool resolve_match_session_bios_path(
|
||||
if (p.empty() || !std::filesystem::exists(p, ec))
|
||||
return;
|
||||
const PsxBiosBackend* b = bios_backend_for_file(p, nullptr, nullptr);
|
||||
if (b && b->image && !b->image->image_bundled)
|
||||
if (b && b->image && !b->image->image_bundled &&
|
||||
(!retail_crc || b->image->image_crc32 == retail_crc)) {
|
||||
retail = p;
|
||||
if (out_image_id) *out_image_id = b->image->image_id;
|
||||
}
|
||||
};
|
||||
try_retail(preferred_hint);
|
||||
if (retail.empty() && launcher_bios_path && launcher_bios_path[0])
|
||||
@@ -3313,7 +3325,18 @@ static void netplay_host_present_restore(void) {
|
||||
apply_present_cadence();
|
||||
}
|
||||
|
||||
/* Why the last match ended, for the launcher's status line once the lobby is
|
||||
* back. Set by netplay_soft_exit; NULL when the player ended it. */
|
||||
static const char *g_netplay_exit_reason_text = nullptr;
|
||||
|
||||
static void netplay_soft_exit(const char *origin) {
|
||||
/* A peer that gave up on a boot-digest mismatch sends BYE, which reaches
|
||||
* this side as a disconnect. This side saw the same mismatch: say that,
|
||||
* not "the other player left". Read it before shutdown clears the latch. */
|
||||
if (origin && std::strcmp(origin, "netplay_peer_disconnect") == 0 &&
|
||||
psx_netplay_rb_boot_dig0_mismatch_since_ms() != 0u)
|
||||
origin = "netplay_boot_mismatch";
|
||||
g_netplay_exit_reason_text = netplay_exit_reason_text(origin);
|
||||
psx_crash_trace_set_exit_origin(origin);
|
||||
netplay_host_present_restore();
|
||||
psx_netplay_shutdown(); /* sends BYE so the peer soft-exits too */
|
||||
@@ -3324,6 +3347,8 @@ static void netplay_soft_exit(const char *origin) {
|
||||
psx_request_return_to_lobby();
|
||||
return;
|
||||
}
|
||||
if (g_netplay_exit_reason_text)
|
||||
std::fprintf(stderr, "psxrecomp: %s\n", g_netplay_exit_reason_text);
|
||||
shutdown_runtime();
|
||||
std::exit(0);
|
||||
}
|
||||
@@ -5717,6 +5742,20 @@ static void netplay_barrier_admit(int override) {
|
||||
netplay_soft_exit("netplay_peer_disconnect");
|
||||
if (psx_return_to_lobby_requested()) goto done;
|
||||
}
|
||||
/* Both boot digests known and different: the peers booted differently
|
||||
* (BIOS image, boot settings) and the dig0 gate would hold until the
|
||||
* 20 s admit-stall watchdog. End it after a short grace, with its own
|
||||
* reason. */
|
||||
if (netplay_boot_mismatch_final(psx_netplay_rb_boot_dig0_mismatch_since_ms(),
|
||||
(uint32_t)psx_host_mono_ms(),
|
||||
NETPLAY_BOOT_MISMATCH_GRACE_MS)) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: netplay boot digest mismatch held %u ms — "
|
||||
"returning to lobby\n",
|
||||
(unsigned)NETPLAY_BOOT_MISMATCH_GRACE_MS);
|
||||
netplay_soft_exit("netplay_boot_mismatch");
|
||||
if (psx_return_to_lobby_requested()) goto done;
|
||||
}
|
||||
/* Staged .pst rejected (stale codegen / BIOS / missing) — do not wait
|
||||
* out the 90s load barrier with stall=load_apply_done. */
|
||||
if (psx_netplay_consume_load_apply_failed()) {
|
||||
@@ -8364,10 +8403,14 @@ namespace {
|
||||
int prefer_openbios = 1;
|
||||
int can_openbios = 1;
|
||||
int can_scph1001 = 0;
|
||||
uint32_t retail_crc = 0; /* image this seat would boot; 0 = not sent */
|
||||
};
|
||||
AeLanSlotBios g_lnch_lan_slot_bios[kAeLanMaxSlots]{};
|
||||
/* Match-only BIOS token from lobby settle or LAN START ("openbios"|"scph1001"). */
|
||||
char g_lnch_session_bios[16]{};
|
||||
/* With "scph1001": CRC-32 of the retail image every peer boots (0 = any,
|
||||
* from an older host). See netplay_bios_settle.h. */
|
||||
uint32_t g_lnch_session_bios_crc = 0;
|
||||
|
||||
/* Bring-your-own memory card (seat 1 / P2). Per-seat offers for LAN
|
||||
* (mirrors online memcard_offer); the local offer as last published by
|
||||
@@ -8458,13 +8501,14 @@ namespace {
|
||||
static int ae_np_lan_occupied(const AeLanLobbyState& state);
|
||||
static int ae_np_lan_endpoint_port(const std::string& endpoint);
|
||||
static bool ae_np_read_lan_file_state(AeLanLobbyState* state);
|
||||
static void ae_np_set_session_bios_token(const char* token);
|
||||
static void ae_np_set_session_bios_token(const char* token, uint32_t retail_crc = 0);
|
||||
static void ae_np_clear_session_bios_token(void);
|
||||
static void ae_np_lan_clear_slot_bios(int slot);
|
||||
static void ae_np_lan_store_slot_bios(int slot, int prefer_open, int can_open,
|
||||
int can_scph);
|
||||
int can_scph, uint32_t retail_crc = 0);
|
||||
static void ae_np_lan_sync_local_slot_bios(void);
|
||||
static int ae_np_lan_settle_session_bios(char* out, size_t out_cap);
|
||||
static int ae_np_lan_settle_session_bios(char* out, size_t out_cap, uint32_t* out_crc,
|
||||
char* why, size_t why_cap);
|
||||
static void ae_np_append_lan_bios_join(char* msg, size_t msg_cap, int* io_off);
|
||||
static int ae_np_parse_lan_bios_tail(char* p, int* prefer_open, int* can_open,
|
||||
int* can_scph);
|
||||
@@ -9748,21 +9792,27 @@ namespace {
|
||||
offer.can_openbios =
|
||||
(s_openbios_allowed && psx_bios_bundled() != nullptr) ? 1 : 0;
|
||||
const char* argv0 = g_lnch_argv0 ? g_lnch_argv0 : "";
|
||||
auto path_is_retail = [&](const std::filesystem::path& p) -> bool {
|
||||
/* CRC-32 of the linked retail image a file matches, 0 if none. */
|
||||
auto retail_crc_of = [&](const std::filesystem::path& p) -> uint32_t {
|
||||
std::error_code ec;
|
||||
if (p.empty() || !std::filesystem::exists(p, ec)) return false;
|
||||
if (p.empty() || !std::filesystem::exists(p, ec)) return 0;
|
||||
const PsxBiosBackend* b = bios_backend_for_file(p, nullptr, nullptr);
|
||||
return b && b->image && !b->image->image_bundled;
|
||||
return (b && b->image && !b->image->image_bundled) ? b->image->image_crc32 : 0;
|
||||
};
|
||||
bool has_dump = false;
|
||||
if (launcher_bios_path && launcher_bios_path[0]) {
|
||||
const auto p = resolve_bios_path(launcher_bios_path, argv0);
|
||||
has_dump = path_is_retail(p);
|
||||
}
|
||||
if (!has_dump) has_dump = path_is_retail(read_cached_path(argv0, "bios.cfg"));
|
||||
if (!has_dump) has_dump = path_is_retail(discover_retail_bios_near(argv0));
|
||||
auto path_is_retail = [&](const std::filesystem::path& p) -> bool {
|
||||
return retail_crc_of(p) != 0;
|
||||
};
|
||||
/* The first dump in the order resolve_match_session_bios_path tries
|
||||
* them: that is the image this peer boots for a retail match, so its
|
||||
* CRC is what the settle must compare. */
|
||||
uint32_t dump_crc = 0;
|
||||
if (launcher_bios_path && launcher_bios_path[0])
|
||||
dump_crc = retail_crc_of(resolve_bios_path(launcher_bios_path, argv0));
|
||||
if (!dump_crc) dump_crc = retail_crc_of(read_cached_path(argv0, "bios.cfg"));
|
||||
if (!dump_crc) dump_crc = retail_crc_of(discover_retail_bios_near(argv0));
|
||||
offer.can_scph1001 =
|
||||
(psx_bios_has_selectable() && has_dump) ? 1 : 0;
|
||||
(psx_bios_has_selectable() && dump_crc) ? 1 : 0;
|
||||
offer.retail_crc = offer.can_scph1001 ? dump_crc : 0;
|
||||
|
||||
/* Empty / bundled path = explicit OpenBIOS preference. */
|
||||
offer.prefer_openbios = 1;
|
||||
@@ -9783,18 +9833,35 @@ namespace {
|
||||
ae_np_refresh_bios_offer(nullptr);
|
||||
}
|
||||
|
||||
static void ae_np_set_session_bios_token(const char* token) {
|
||||
static void ae_np_set_session_bios_token(const char* token, uint32_t retail_crc) {
|
||||
g_lnch_session_bios[0] = '\0';
|
||||
g_lnch_session_bios_crc = 0;
|
||||
if (!token || !token[0]) return;
|
||||
if (std::strcmp(token, "openbios") != 0 &&
|
||||
std::strcmp(token, "scph1001") != 0)
|
||||
return;
|
||||
std::snprintf(g_lnch_session_bios, sizeof(g_lnch_session_bios), "%s",
|
||||
token);
|
||||
if (std::strcmp(token, "scph1001") == 0)
|
||||
g_lnch_session_bios_crc = retail_crc;
|
||||
}
|
||||
|
||||
static void ae_np_clear_session_bios_token(void) {
|
||||
g_lnch_session_bios[0] = '\0';
|
||||
g_lnch_session_bios_crc = 0;
|
||||
}
|
||||
|
||||
static void ae_np_log_settled_bios(const char* where) {
|
||||
const char* token = g_lnch_session_bios[0] ? g_lnch_session_bios : "openbios";
|
||||
if (std::strcmp(token, "scph1001") == 0 && g_lnch_session_bios_crc) {
|
||||
char name[32];
|
||||
netplay_bios_describe_crc(g_lnch_session_bios_crc, name, sizeof(name));
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: %s settled session BIOS = %s (retail %s, crc %08x)\n",
|
||||
where, token, name, (unsigned)g_lnch_session_bios_crc);
|
||||
} else {
|
||||
std::fprintf(stdout, "psxrecomp: %s settled session BIOS = %s\n", where, token);
|
||||
}
|
||||
}
|
||||
|
||||
static void ae_np_lan_clear_slot_bios(int slot) {
|
||||
@@ -9805,13 +9872,14 @@ namespace {
|
||||
}
|
||||
|
||||
static void ae_np_lan_store_slot_bios(int slot, int prefer_open, int can_open,
|
||||
int can_scph) {
|
||||
int can_scph, uint32_t retail_crc) {
|
||||
if (slot < 0 || slot >= kAeLanMaxSlots) return;
|
||||
AeLanSlotBios& b = g_lnch_lan_slot_bios[slot];
|
||||
b.valid = 1;
|
||||
b.prefer_openbios = prefer_open ? 1 : 0;
|
||||
b.can_openbios = can_open ? 1 : 0;
|
||||
b.can_scph1001 = can_scph ? 1 : 0;
|
||||
b.retail_crc = can_scph ? retail_crc : 0;
|
||||
if (!b.can_openbios && !b.can_scph1001) b.can_openbios = 1;
|
||||
}
|
||||
|
||||
@@ -9827,7 +9895,7 @@ namespace {
|
||||
}
|
||||
if (slot < 0 || !offer || !offer->valid) return;
|
||||
ae_np_lan_store_slot_bios(slot, offer->prefer_openbios, offer->can_openbios,
|
||||
offer->can_scph1001);
|
||||
offer->can_scph1001, offer->retail_crc);
|
||||
}
|
||||
|
||||
static void ae_np_lan_send_chat_to_peers(const char* player_id, const char* from,
|
||||
@@ -9988,10 +10056,15 @@ namespace {
|
||||
return (mc.valid && mc.has_card && mc.share) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Same settle rule as psx_lobby_settle_session_bios, over LAN seat offers. */
|
||||
static int ae_np_lan_settle_session_bios(char* out, size_t out_cap) {
|
||||
/* Same settle rule as psx_lobby_settle_session_bios (netplay_bios_settle),
|
||||
* over LAN seat offers. Returns 0 with the token in out and the retail CRC
|
||||
* in *out_crc; 1 when no BIOS suits every seat (why says which). */
|
||||
static int ae_np_lan_settle_session_bios(char* out, size_t out_cap, uint32_t* out_crc,
|
||||
char* why, size_t why_cap) {
|
||||
if (!out || out_cap < 9) return -1;
|
||||
out[0] = '\0';
|
||||
if (out_crc) *out_crc = 0;
|
||||
if (why && why_cap) why[0] = '\0';
|
||||
AeLanLobbyState st;
|
||||
if (!ae_np_read_lan_state(&st)) {
|
||||
std::strncpy(out, "openbios", out_cap - 1);
|
||||
@@ -9999,35 +10072,30 @@ namespace {
|
||||
return 0;
|
||||
}
|
||||
ae_np_lan_sync_local_slot_bios();
|
||||
int any_prefer_open = 0;
|
||||
int any_cannot_scph = 0;
|
||||
int host_prefer_scph = 0;
|
||||
int saw_peer = 0;
|
||||
NetplayBiosSeat seats[kAeLanMaxSlots] = {};
|
||||
int n = 0;
|
||||
const int host_slot =
|
||||
(st.host_slot >= 0 && st.host_slot < kAeLanMaxSlots) ? st.host_slot : 0;
|
||||
for (int i = 0; i < st.max_slots && i < kAeLanMaxSlots; ++i) {
|
||||
if (st.slot_name[i].empty()) continue;
|
||||
saw_peer = 1;
|
||||
const AeLanSlotBios& b = g_lnch_lan_slot_bios[i];
|
||||
if (!b.valid) {
|
||||
any_cannot_scph = 1;
|
||||
continue;
|
||||
}
|
||||
if (b.prefer_openbios) any_prefer_open = 1;
|
||||
if (!b.can_scph1001) any_cannot_scph = 1;
|
||||
if (i == host_slot && !b.prefer_openbios && b.can_scph1001)
|
||||
host_prefer_scph = 1;
|
||||
NetplayBiosSeat& s = seats[n++];
|
||||
s.offered = b.valid;
|
||||
s.can_openbios = b.can_openbios;
|
||||
s.can_retail = b.can_scph1001;
|
||||
s.prefer_openbios = b.prefer_openbios;
|
||||
s.retail_crc = b.retail_crc;
|
||||
s.is_host = (i == host_slot) ? 1 : 0;
|
||||
}
|
||||
if (!saw_peer) any_cannot_scph = 1;
|
||||
if (any_cannot_scph)
|
||||
std::strncpy(out, "openbios", out_cap - 1);
|
||||
else if (host_prefer_scph)
|
||||
std::strncpy(out, "scph1001", out_cap - 1);
|
||||
else if (any_prefer_open)
|
||||
std::strncpy(out, "openbios", out_cap - 1);
|
||||
else
|
||||
std::strncpy(out, "scph1001", out_cap - 1);
|
||||
const NetplayBiosSettle settle = netplay_bios_settle(seats, n);
|
||||
if (settle.kind == NETPLAY_BIOS_NONE) {
|
||||
if (why && why_cap)
|
||||
netplay_bios_describe_refusal(&settle, seats, n, why, why_cap);
|
||||
return 1;
|
||||
}
|
||||
std::strncpy(out, netplay_bios_token(settle.kind), out_cap - 1);
|
||||
out[out_cap - 1] = '\0';
|
||||
if (out_crc) *out_crc = settle.retail_crc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -10055,6 +10123,36 @@ namespace {
|
||||
g_lnch_memcard_offer.share ? 1 : 0);
|
||||
if (m > 0) *io_off += m;
|
||||
}
|
||||
/* Line 6: CRC-32 of the retail image this peer boots; ""
|
||||
* when it has none. Older hosts stop after the memcard lines. */
|
||||
if (*io_off > 0 && (size_t)*io_off < msg_cap) {
|
||||
char crc[16];
|
||||
netplay_bios_format_crc((offer && offer->valid && offer->can_scph1001)
|
||||
? offer->retail_crc : 0,
|
||||
crc, sizeof(crc));
|
||||
const int c = std::snprintf(msg + *io_off, msg_cap - (size_t)*io_off,
|
||||
"%s\n", crc);
|
||||
if (c > 0) *io_off += c;
|
||||
}
|
||||
}
|
||||
|
||||
/* Optional JOIN retail CRC: line 6 of the tail, after the three bios and
|
||||
* two memcard lines. 0 when absent (an older guest). Does not modify tail. */
|
||||
static uint32_t ae_np_parse_lan_bios_crc_tail(const char* tail) {
|
||||
if (!tail) return 0;
|
||||
const char* p = tail;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
const char* nl = std::strchr(p, '\n');
|
||||
if (!nl) return 0;
|
||||
p = nl + 1;
|
||||
}
|
||||
char line[16] = {};
|
||||
size_t n = 0;
|
||||
while (p[n] && p[n] != '\n' && n + 1 < sizeof(line)) {
|
||||
line[n] = p[n];
|
||||
++n;
|
||||
}
|
||||
return netplay_bios_parse_crc(line);
|
||||
}
|
||||
|
||||
/* Optional JOIN memcard tail after the 3 bios lines: has_card\nshare\n.
|
||||
@@ -10860,10 +10958,13 @@ namespace {
|
||||
int prefer_open = 1, can_open = 1, can_scph = 0;
|
||||
AeLanSlotMemcard mc_offer{};
|
||||
(void)ae_np_parse_lan_memcard_tail(bios_tail, &mc_offer);
|
||||
/* Read before ae_np_parse_lan_bios_tail, which cuts the tail. */
|
||||
const uint32_t retail_crc = ae_np_parse_lan_bios_crc_tail(bios_tail);
|
||||
if (bios_tail &&
|
||||
ae_np_parse_lan_bios_tail(bios_tail, &prefer_open, &can_open,
|
||||
&can_scph) == 0) {
|
||||
ae_np_lan_store_slot_bios(slot, prefer_open, can_open, can_scph);
|
||||
ae_np_lan_store_slot_bios(slot, prefer_open, can_open, can_scph,
|
||||
retail_crc);
|
||||
} else {
|
||||
/* Legacy JOIN without bios_offer — cannot assume SCPH. */
|
||||
ae_np_lan_clear_slot_bios(slot);
|
||||
@@ -11229,7 +11330,7 @@ namespace {
|
||||
if (std::strncmp(buf, "MOTK1 START\n", 12) == 0) {
|
||||
g_lnch_remote_lan_state.started = true;
|
||||
/* MOTK1 START\n<session>\n[<delay>\n<prediction>\n<rollback>\n
|
||||
* [<session_bios>\n]]
|
||||
* [<session_bios>\n[<guest_memcard>\n[<session_bios_crc>\n]]]]
|
||||
* Trailing caps are host-authoritative (incl. settled BIOS). */
|
||||
char* p = buf + 12;
|
||||
char* nl = std::strchr(p, '\n');
|
||||
@@ -11246,8 +11347,8 @@ namespace {
|
||||
g_lnch_lan_guest_memcard_active = 0;
|
||||
if (nl) {
|
||||
p = nl + 1;
|
||||
char* lines[5] = {};
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
char* lines[6] = {};
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (!p || !*p) break;
|
||||
lines[i] = p;
|
||||
char* n2 = std::strchr(p, '\n');
|
||||
@@ -11276,7 +11377,10 @@ namespace {
|
||||
if (lines[2] && lines[2][0])
|
||||
g_lnch_rollback = (std::atoi(lines[2]) != 0) ? 1 : 0;
|
||||
if (lines[3] && lines[3][0])
|
||||
ae_np_set_session_bios_token(lines[3]);
|
||||
/* Line 6 names the retail image; an
|
||||
* older host sends none, which accepts any dump. */
|
||||
ae_np_set_session_bios_token(
|
||||
lines[3], lines[5] ? netplay_bios_parse_crc(lines[5]) : 0u);
|
||||
else
|
||||
/* Legacy host: force OpenBIOS so mixed local prefs
|
||||
* cannot silently desync. */
|
||||
@@ -12141,12 +12245,21 @@ namespace {
|
||||
else
|
||||
ae_np_refresh_bios_offer_from_disk();
|
||||
char session_bios[16] = {};
|
||||
(void)ae_np_lan_settle_session_bios(session_bios, sizeof(session_bios));
|
||||
uint32_t session_crc = 0;
|
||||
char why[192] = {};
|
||||
if (ae_np_lan_settle_session_bios(session_bios, sizeof(session_bios),
|
||||
&session_crc, why, sizeof(why)) == 1) {
|
||||
/* No BIOS every seat can boot: starting would only end in a
|
||||
* boot-digest stall. Say why instead. */
|
||||
std::fprintf(stdout, "psxrecomp: LAN start refused: %s\n", why);
|
||||
psx_lobby_set_last_error(why);
|
||||
ae_np_clear_session_bios_token();
|
||||
return -1;
|
||||
}
|
||||
ae_np_set_session_bios_token(session_bios[0] ? session_bios
|
||||
: "openbios");
|
||||
std::fprintf(stdout, "psxrecomp: LAN settled session BIOS = %s\n",
|
||||
g_lnch_session_bios[0] ? g_lnch_session_bios
|
||||
: "openbios");
|
||||
: "openbios",
|
||||
session_crc);
|
||||
ae_np_log_settled_bios("LAN");
|
||||
state.started = true;
|
||||
state.session_id += 1u;
|
||||
if (state.session_id == 0) state.session_id = 1;
|
||||
@@ -12164,13 +12277,17 @@ namespace {
|
||||
g_lnch_lan_guest_memcard_active = ae_np_lan_guest_memcard_effective(state);
|
||||
std::fprintf(stdout, "psxrecomp: LAN guest memcard (P2 card as slot 2) = %s\n",
|
||||
g_lnch_lan_guest_memcard_active ? "on" : "off");
|
||||
char session_crc_text[16];
|
||||
netplay_bios_format_crc(g_lnch_session_bios_crc, session_crc_text,
|
||||
sizeof(session_crc_text));
|
||||
std::snprintf(start_msg, sizeof(start_msg),
|
||||
"MOTK1 START\n%u\n%d\n%d\n%d\n%s\n%d\n",
|
||||
"MOTK1 START\n%u\n%d\n%d\n%d\n%s\n%d\n%s\n",
|
||||
(unsigned)state.session_id, delay, pred,
|
||||
g_lnch_rollback ? 1 : 0,
|
||||
g_lnch_session_bios[0] ? g_lnch_session_bios
|
||||
: "openbios",
|
||||
g_lnch_lan_guest_memcard_active ? 1 : 0);
|
||||
g_lnch_lan_guest_memcard_active ? 1 : 0,
|
||||
session_crc_text);
|
||||
for (int i = 0; i < kAeLanMaxSlots; ++i) {
|
||||
if (g_lnch_lan_peer_ok[i])
|
||||
ae_np_lan_udp_sendto(g_lnch_lan_peers[i], start_msg);
|
||||
@@ -12188,12 +12305,19 @@ namespace {
|
||||
caps.guest_memcard_active = ae_np_ws_guest_memcard_effective();
|
||||
std::fprintf(stdout, "psxrecomp: lobby guest memcard (P2 card as slot 2) = %s\n",
|
||||
caps.guest_memcard_active ? "on" : "off");
|
||||
(void)psx_lobby_settle_session_bios(caps.session_bios,
|
||||
sizeof(caps.session_bios));
|
||||
char why[192] = {};
|
||||
if (psx_lobby_settle_session_bios(caps.session_bios, sizeof(caps.session_bios),
|
||||
&caps.session_bios_crc, why, sizeof(why)) == 1) {
|
||||
/* No BIOS every seat can boot: refuse, and say why. */
|
||||
std::fprintf(stdout, "psxrecomp: lobby start refused: %s\n", why);
|
||||
psx_lobby_set_last_error(why);
|
||||
ae_np_clear_session_bios_token();
|
||||
return -1;
|
||||
}
|
||||
ae_np_set_session_bios_token(caps.session_bios[0] ? caps.session_bios
|
||||
: "openbios");
|
||||
std::fprintf(stdout, "psxrecomp: lobby settled session BIOS = %s\n",
|
||||
caps.session_bios[0] ? caps.session_bios : "openbios");
|
||||
: "openbios",
|
||||
caps.session_bios_crc);
|
||||
ae_np_log_settled_bios("lobby");
|
||||
return psx_lobby_request_start(&caps);
|
||||
}
|
||||
|
||||
@@ -12430,7 +12554,8 @@ namespace {
|
||||
* it once, so a toggle racing the start cannot split the room. */
|
||||
out->guest_memcard = caps->guest_memcard_active ? 1 : 0;
|
||||
if (caps->session_bios[0])
|
||||
ae_np_set_session_bios_token(caps->session_bios);
|
||||
ae_np_set_session_bios_token(caps->session_bios,
|
||||
caps->session_bios_crc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -14692,24 +14817,27 @@ int main(int argc, char** argv) {
|
||||
* Ephemeral for this boot only — seed / bios.cfg keep
|
||||
* the player's offline preference. */
|
||||
if (caps->session_bios[0])
|
||||
ae_np_set_session_bios_token(caps->session_bios);
|
||||
ae_np_set_session_bios_token(caps->session_bios,
|
||||
caps->session_bios_crc);
|
||||
}
|
||||
}
|
||||
/* LAN START / lobby token — apply even when match_caps absent. */
|
||||
const char* match_bios_id = nullptr;
|
||||
if (ls.netplay_launch.enabled && g_lnch_session_bios[0] &&
|
||||
resolve_match_session_bios_path(
|
||||
g_lnch_session_bios,
|
||||
g_lnch_session_bios, g_lnch_session_bios_crc,
|
||||
seed.has_bios_path ? seed.bios_path
|
||||
: std::filesystem::path{},
|
||||
ls.bios_path, argv[0],
|
||||
&match_session_bios_path)) {
|
||||
&match_session_bios_path, &match_bios_id)) {
|
||||
match_session_bios_set = true;
|
||||
if (std::strcmp(g_lnch_session_bios, "scph1001") == 0 &&
|
||||
match_session_bios_path.empty()) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: session BIOS scph1001 required but no "
|
||||
"validated dump found — aborting launch (mixed "
|
||||
"OpenBIOS/SCPH would desync)\n");
|
||||
"psxrecomp: session BIOS scph1001 (crc %08x) required "
|
||||
"but no validated dump found — aborting launch (mixed "
|
||||
"BIOS images would desync)\n",
|
||||
(unsigned)g_lnch_session_bios_crc);
|
||||
match_session_bios_set = false;
|
||||
ls.netplay_launch.enabled = 0;
|
||||
g_lnch_pending_direct_launch = {};
|
||||
@@ -14724,8 +14852,9 @@ int main(int argc, char** argv) {
|
||||
"(match only; preference unchanged)\n");
|
||||
} else {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: netplay session BIOS = SCPH-1001 "
|
||||
"psxrecomp: netplay session BIOS = %s "
|
||||
"(%s; match only; preference unchanged)\n",
|
||||
match_bios_id ? match_bios_id : "retail",
|
||||
match_session_bios_path.string().c_str());
|
||||
}
|
||||
}
|
||||
@@ -16377,6 +16506,11 @@ soft_return_lobby:
|
||||
teardown_game_session_keep_lobby();
|
||||
#if defined(RECOMP_LAUNCHER) && defined(PSX_HAS_LOBBY_CLIENT)
|
||||
ae_np_prepare_lobby_rematch();
|
||||
/* Say why the match ended on the launcher's status line; the
|
||||
* room and browser views both show last_error. Cleared when the player
|
||||
* ended the match themselves. */
|
||||
psx_lobby_set_last_error(g_netplay_exit_reason_text);
|
||||
g_netplay_exit_reason_text = nullptr;
|
||||
{
|
||||
std::string assets_dir_str = exe_dir_from_argv(argv[0]).string();
|
||||
std::string rui_title = (game_name.empty() ? std::string("PSX") : game_name)
|
||||
@@ -16928,19 +17062,22 @@ soft_return_lobby:
|
||||
if (net_cfg.enabled) {
|
||||
const PsxLobbyMatchCaps* caps = psx_lobby_match_caps();
|
||||
if (caps && caps->valid && caps->session_bios[0])
|
||||
ae_np_set_session_bios_token(caps->session_bios);
|
||||
ae_np_set_session_bios_token(caps->session_bios,
|
||||
caps->session_bios_crc);
|
||||
std::filesystem::path session_path;
|
||||
const char* session_bios_id = nullptr;
|
||||
if (g_lnch_session_bios[0] &&
|
||||
resolve_match_session_bios_path(
|
||||
g_lnch_session_bios,
|
||||
g_lnch_session_bios, g_lnch_session_bios_crc,
|
||||
ls.bios_path[0] ? std::filesystem::path(ls.bios_path)
|
||||
: std::filesystem::path{},
|
||||
ls.bios_path, argv[0], &session_path)) {
|
||||
ls.bios_path, argv[0], &session_path, &session_bios_id)) {
|
||||
if (std::strcmp(g_lnch_session_bios, "scph1001") == 0 &&
|
||||
session_path.empty()) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: rematch session BIOS scph1001 required "
|
||||
"but no validated dump — aborting\n");
|
||||
"psxrecomp: rematch session BIOS scph1001 (crc %08x) "
|
||||
"required but no validated dump — aborting\n",
|
||||
(unsigned)g_lnch_session_bios_crc);
|
||||
SDL_Quit();
|
||||
return 1;
|
||||
}
|
||||
@@ -16955,8 +17092,9 @@ soft_return_lobby:
|
||||
} else {
|
||||
bios_path_str = session_path.string();
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: rematch session BIOS = SCPH-1001 (%s; "
|
||||
"psxrecomp: rematch session BIOS = %s (%s; "
|
||||
"preference unchanged)\n",
|
||||
session_bios_id ? session_bios_id : "retail",
|
||||
bios_path_str.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/* Which BIOS every peer boots for a netplay match.
|
||||
* See netplay_bios_settle.h for the rule. */
|
||||
#include "netplay_bios_settle.h"
|
||||
#include "psx_bios_known_images.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
NetplayBiosSettle netplay_bios_settle(const NetplayBiosSeat *seats, int count)
|
||||
{
|
||||
NetplayBiosSettle s;
|
||||
int retail_ok = count > 0;
|
||||
int open_ok = count > 0;
|
||||
int host_prefers_retail = 0;
|
||||
int any_prefer_open = 0;
|
||||
int first = -1;
|
||||
int i;
|
||||
|
||||
s.kind = NETPLAY_BIOS_OPENBIOS;
|
||||
s.retail_crc = 0;
|
||||
s.seat_a = -1;
|
||||
s.seat_b = -1;
|
||||
if (!seats || count <= 0)
|
||||
return s;
|
||||
|
||||
for (i = 0; i < count; ++i) {
|
||||
const NetplayBiosSeat *seat = &seats[i];
|
||||
if (!seat->offered) {
|
||||
/* Older client: no retail identity; assumed to link OpenBIOS. */
|
||||
if (retail_ok) s.seat_a = i;
|
||||
retail_ok = 0;
|
||||
continue;
|
||||
}
|
||||
if (!seat->can_openbios) open_ok = 0;
|
||||
if (seat->prefer_openbios) any_prefer_open = 1;
|
||||
if (seat->is_host && !seat->prefer_openbios && seat->can_retail)
|
||||
host_prefers_retail = 1;
|
||||
if (!seat->can_retail || !seat->retail_crc) {
|
||||
if (retail_ok) s.seat_a = i;
|
||||
retail_ok = 0;
|
||||
continue;
|
||||
}
|
||||
if (first < 0) {
|
||||
first = i;
|
||||
} else if (seat->retail_crc != seats[first].retail_crc && retail_ok) {
|
||||
retail_ok = 0;
|
||||
s.seat_a = first;
|
||||
s.seat_b = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (retail_ok && (host_prefers_retail || !any_prefer_open)) {
|
||||
s.kind = NETPLAY_BIOS_RETAIL;
|
||||
} else if (open_ok) {
|
||||
s.kind = NETPLAY_BIOS_OPENBIOS;
|
||||
} else if (retail_ok) {
|
||||
s.kind = NETPLAY_BIOS_RETAIL;
|
||||
} else {
|
||||
s.kind = NETPLAY_BIOS_NONE;
|
||||
return s;
|
||||
}
|
||||
if (s.kind == NETPLAY_BIOS_RETAIL)
|
||||
s.retail_crc = seats[first].retail_crc;
|
||||
s.seat_a = -1;
|
||||
s.seat_b = -1;
|
||||
return s;
|
||||
}
|
||||
|
||||
const char *netplay_bios_token(int kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case NETPLAY_BIOS_OPENBIOS: return "openbios";
|
||||
case NETPLAY_BIOS_RETAIL: return "scph1001";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
void netplay_bios_format_crc(uint32_t crc, char *out, size_t cap)
|
||||
{
|
||||
if (!out || !cap) return;
|
||||
if (!crc) {
|
||||
out[0] = '\0';
|
||||
return;
|
||||
}
|
||||
snprintf(out, cap, "%08x", (unsigned)crc);
|
||||
}
|
||||
|
||||
uint32_t netplay_bios_parse_crc(const char *text)
|
||||
{
|
||||
uint32_t v = 0;
|
||||
int digits = 0;
|
||||
if (!text) return 0;
|
||||
while (*text == ' ' || *text == '\t') ++text;
|
||||
for (; *text; ++text) {
|
||||
const char c = *text;
|
||||
uint32_t d;
|
||||
if (c >= '0' && c <= '9') d = (uint32_t)(c - '0');
|
||||
else if (c >= 'a' && c <= 'f') d = (uint32_t)(c - 'a' + 10);
|
||||
else if (c >= 'A' && c <= 'F') d = (uint32_t)(c - 'A' + 10);
|
||||
else break;
|
||||
if (++digits > 8) return 0;
|
||||
v = (v << 4) | d;
|
||||
}
|
||||
while (*text == ' ' || *text == '\t' || *text == '\r' || *text == '\n') ++text;
|
||||
if (digits != 8 || *text) return 0;
|
||||
return v;
|
||||
}
|
||||
|
||||
void netplay_bios_describe_crc(uint32_t crc, char *out, size_t cap)
|
||||
{
|
||||
size_t i;
|
||||
if (!out || !cap) return;
|
||||
for (i = 0; i < sizeof(psx_known_bios_images) / sizeof(psx_known_bios_images[0]); ++i) {
|
||||
if (psx_known_bios_images[i].crc32 == crc) {
|
||||
snprintf(out, cap, "%s", psx_known_bios_images[i].id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
snprintf(out, cap, "BIOS %08x", (unsigned)crc);
|
||||
}
|
||||
|
||||
void netplay_bios_describe_refusal(const NetplayBiosSettle *s,
|
||||
const NetplayBiosSeat *seats, int count,
|
||||
char *out, size_t cap)
|
||||
{
|
||||
char a[32], b[32];
|
||||
if (!out || !cap) return;
|
||||
out[0] = '\0';
|
||||
if (!s || s->kind != NETPLAY_BIOS_NONE) return;
|
||||
if (seats && s->seat_a >= 0 && s->seat_a < count &&
|
||||
s->seat_b >= 0 && s->seat_b < count) {
|
||||
netplay_bios_describe_crc(seats[s->seat_a].retail_crc, a, sizeof(a));
|
||||
netplay_bios_describe_crc(seats[s->seat_b].retail_crc, b, sizeof(b));
|
||||
snprintf(out, cap,
|
||||
"Players use different BIOS images (%s and %s), and not "
|
||||
"everyone has OpenBIOS. Use the same BIOS dump.", a, b);
|
||||
return;
|
||||
}
|
||||
snprintf(out, cap,
|
||||
"A player has no usable BIOS dump for this game (or an older "
|
||||
"build), and not everyone has OpenBIOS.");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* Why a netplay match ended, in words. See netplay_exit_reason.h. */
|
||||
#include "netplay_exit_reason.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const char *origin;
|
||||
const char *text;
|
||||
} NetplayExitReason;
|
||||
|
||||
static const NetplayExitReason k_reasons[] = {
|
||||
{ "netplay_boot_mismatch",
|
||||
"The match ended: the players' consoles started differently "
|
||||
"(different BIOS image or boot settings)." },
|
||||
{ "netplay_peer_disconnect",
|
||||
"The match ended: the other player left or stopped responding." },
|
||||
{ "netplay_load_failed",
|
||||
"The match ended: a player could not load the shared save state." },
|
||||
{ "netplay_load_stall",
|
||||
"The match ended: loading the shared save state timed out." },
|
||||
{ "netplay_link_stall",
|
||||
"The match ended: could not connect to the other player in time." },
|
||||
{ "netplay_admit_stall",
|
||||
"The match ended: the players' games stopped running in step." },
|
||||
};
|
||||
|
||||
const char *netplay_exit_reason_text(const char *origin)
|
||||
{
|
||||
size_t i;
|
||||
if (!origin) return NULL;
|
||||
for (i = 0; i < sizeof(k_reasons) / sizeof(k_reasons[0]); ++i) {
|
||||
if (strcmp(k_reasons[i].origin, origin) == 0)
|
||||
return k_reasons[i].text;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int netplay_boot_mismatch_final(uint32_t mismatch_since_ms, uint32_t now_ms,
|
||||
uint32_t grace_ms)
|
||||
{
|
||||
if (!mismatch_since_ms) return 0;
|
||||
return (uint32_t)(now_ms - mismatch_since_ms) >= grace_ms;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "psx_lobby_client.h"
|
||||
#include "netplay_bios_settle.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
@@ -151,13 +152,17 @@ void psx_lobby_seat_swap_clear(void) {}
|
||||
int psx_lobby_chat_count(void) { return 0; }
|
||||
int psx_lobby_chat_get(int index, PsxLobbyChatMsg *out) { (void)index; (void)out; return 0; }
|
||||
void psx_lobby_chat_clear(void) {}
|
||||
int psx_lobby_settle_session_bios(char *out, size_t out_cap)
|
||||
int psx_lobby_settle_session_bios(char *out, size_t out_cap, uint32_t *out_crc,
|
||||
char *why, size_t why_cap)
|
||||
{
|
||||
if (!out || out_cap < 9) return -1;
|
||||
strncpy(out, "openbios", out_cap - 1);
|
||||
out[out_cap - 1] = '\0';
|
||||
if (out_crc) *out_crc = 0;
|
||||
if (why && why_cap) why[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
void psx_lobby_set_last_error(const char *text) { (void)text; }
|
||||
int psx_lobby_request_start(const PsxLobbyMatchCaps *c) { (void)c; return -1; }
|
||||
int psx_lobby_launch_pending(void) { return 0; }
|
||||
void psx_lobby_clear_launch_pending(void) {}
|
||||
@@ -1479,6 +1484,15 @@ static void parse_match_caps_object(const char *obj, PsxLobbyMatchCaps *out)
|
||||
strcmp(out->session_bios, "openbios") != 0 &&
|
||||
strcmp(out->session_bios, "scph1001") != 0)
|
||||
out->session_bios[0] = '\0';
|
||||
/* Retail image every peer boots. Absent from an older host:
|
||||
* 0, and peers accept any retail dump as they used to. */
|
||||
{
|
||||
char crc[16];
|
||||
crc[0] = '\0';
|
||||
json_get_str(obj, "session_bios_crc", crc, sizeof(crc));
|
||||
out->session_bios_crc = strcmp(out->session_bios, "scph1001") == 0
|
||||
? netplay_bios_parse_crc(crc) : 0;
|
||||
}
|
||||
out->valid = 1;
|
||||
}
|
||||
|
||||
@@ -1505,8 +1519,11 @@ static int append_match_caps_json(char *dst, size_t dst_cap, const PsxLobbyMatch
|
||||
if (!lang[0]) strncpy(lang, "en", sizeof(lang) - 1);
|
||||
{
|
||||
const char *sb = caps->session_bios;
|
||||
char crc[16];
|
||||
if (!sb[0] || (strcmp(sb, "openbios") != 0 && strcmp(sb, "scph1001") != 0))
|
||||
sb = "";
|
||||
netplay_bios_format_crc(strcmp(sb, "scph1001") == 0 ? caps->session_bios_crc : 0,
|
||||
crc, sizeof(crc));
|
||||
return snprintf(dst, dst_cap,
|
||||
",\"match_caps\":{\"v\":1,\"aspect_num\":%d,\"aspect_den\":%d,"
|
||||
"\"turbo_loads\":%s,\"bios_hle\":%s,\"fast_boot\":%s,"
|
||||
@@ -1514,7 +1531,8 @@ static int append_match_caps_json(char *dst, size_t dst_cap, const PsxLobbyMatch
|
||||
"\"force_input_relay\":%s,\"force_turn\":%s,\"rollback\":%s,"
|
||||
"\"multitap_analog\":%s,\"guest_memcard\":%s,"
|
||||
"\"guest_memcard_active\":%s,"
|
||||
"\"language\":\"%s\",\"session_bios\":\"%s\"}",
|
||||
"\"language\":\"%s\",\"session_bios\":\"%s\","
|
||||
"\"session_bios_crc\":\"%s\"}",
|
||||
caps->aspect_num, caps->aspect_den,
|
||||
caps->turbo_loads ? "true" : "false",
|
||||
caps->bios_hle ? "true" : "false",
|
||||
@@ -1528,7 +1546,7 @@ static int append_match_caps_json(char *dst, size_t dst_cap, const PsxLobbyMatch
|
||||
caps->multitap_analog ? "true" : "false",
|
||||
caps->guest_memcard ? "true" : "false",
|
||||
caps->guest_memcard_active ? "true" : "false",
|
||||
lang, sb);
|
||||
lang, sb, crc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1699,9 +1717,14 @@ static int parse_seat_array(const char *json, const char *key, int is_spectator,
|
||||
sizeof(g_lc.members[n].display_name));
|
||||
g_lc.members[n].ready = json_get_bool(chunk, "ready", 0);
|
||||
g_lc.members[n].is_spectator = is_spectator;
|
||||
g_lc.members[n].bios_retail_crc = 0;
|
||||
if (json_extract_object(chunk, "bios_offer", offer, sizeof(offer))) {
|
||||
char prefer[24];
|
||||
char crc[16];
|
||||
prefer[0] = '\0';
|
||||
crc[0] = '\0';
|
||||
json_get_str(offer, "retail_crc", crc, sizeof(crc));
|
||||
g_lc.members[n].bios_retail_crc = netplay_bios_parse_crc(crc);
|
||||
g_lc.members[n].bios_offer_valid = 1;
|
||||
g_lc.members[n].bios_can_openbios =
|
||||
json_get_bool(offer, "can_openbios", 1);
|
||||
@@ -4566,60 +4589,65 @@ int psx_lobby_chat_get(int index, PsxLobbyChatMsg *out)
|
||||
return 1;
|
||||
}
|
||||
|
||||
int psx_lobby_settle_session_bios(char *out, size_t out_cap)
|
||||
int psx_lobby_settle_session_bios(char *out, size_t out_cap, uint32_t *out_crc,
|
||||
char *why, size_t why_cap)
|
||||
{
|
||||
NetplayBiosSeat seats[PSX_LOBBY_MAX_MEMBERS + 1];
|
||||
NetplayBiosSettle settle;
|
||||
const char *me = psx_lobby_player_id();
|
||||
int n = 0;
|
||||
int i;
|
||||
int any_prefer_open = 0;
|
||||
int any_cannot_scph = 0;
|
||||
int host_prefer_scph = 0;
|
||||
int saw_peer = 0;
|
||||
if (!out || out_cap < 9) return -1;
|
||||
out[0] = '\0';
|
||||
if (out_crc) *out_crc = 0;
|
||||
if (why && why_cap) why[0] = '\0';
|
||||
|
||||
for (i = 0; i < g_lc.member_count; ++i) {
|
||||
memset(seats, 0, sizeof(seats));
|
||||
for (i = 0; i < g_lc.member_count && n < PSX_LOBBY_MAX_MEMBERS; ++i) {
|
||||
const PsxLobbyMember *m = &g_lc.members[i];
|
||||
if (!m->player_id[0] && !m->display_name[0]) continue;
|
||||
saw_peer = 1;
|
||||
if (!m->bios_offer_valid) {
|
||||
/* Legacy client / not ready yet — cannot assume SCPH. */
|
||||
any_cannot_scph = 1;
|
||||
/* This client's own row: the local offer below is newer than the
|
||||
* server's echo of it (the player may have just changed BIOS). */
|
||||
if (g_lc.bios_offer.valid && me && me[0] && strcmp(m->player_id, me) == 0)
|
||||
continue;
|
||||
}
|
||||
if (m->bios_prefer_openbios) any_prefer_open = 1;
|
||||
if (!m->bios_can_scph1001) any_cannot_scph = 1;
|
||||
if (!m->bios_can_openbios && !m->bios_can_scph1001)
|
||||
any_cannot_scph = 1;
|
||||
if (psx_lobby_member_is_host(m) && !m->bios_prefer_openbios &&
|
||||
m->bios_can_scph1001)
|
||||
host_prefer_scph = 1;
|
||||
seats[n].offered = m->bios_offer_valid;
|
||||
seats[n].can_openbios = m->bios_can_openbios;
|
||||
seats[n].can_retail = m->bios_can_scph1001;
|
||||
seats[n].prefer_openbios = m->bios_prefer_openbios;
|
||||
seats[n].retail_crc = m->bios_retail_crc;
|
||||
seats[n].is_host = psx_lobby_member_is_host(m);
|
||||
++n;
|
||||
}
|
||||
|
||||
/* Include local offer even before lobby_update echoes it. */
|
||||
/* Include the local offer even before lobby_update echoes it. */
|
||||
if (g_lc.bios_offer.valid) {
|
||||
saw_peer = 1;
|
||||
if (g_lc.bios_offer.prefer_openbios) any_prefer_open = 1;
|
||||
if (!g_lc.bios_offer.can_scph1001) any_cannot_scph = 1;
|
||||
if (g_lc.is_host && !g_lc.bios_offer.prefer_openbios &&
|
||||
g_lc.bios_offer.can_scph1001)
|
||||
host_prefer_scph = 1;
|
||||
} else if (!saw_peer) {
|
||||
any_cannot_scph = 1;
|
||||
seats[n].offered = 1;
|
||||
seats[n].can_openbios = g_lc.bios_offer.can_openbios;
|
||||
seats[n].can_retail = g_lc.bios_offer.can_scph1001;
|
||||
seats[n].prefer_openbios = g_lc.bios_offer.prefer_openbios;
|
||||
seats[n].retail_crc = g_lc.bios_offer.retail_crc;
|
||||
seats[n].is_host = g_lc.is_host;
|
||||
++n;
|
||||
}
|
||||
|
||||
/* Capability first: without universal SCPH support, OpenBIOS is required.
|
||||
* Otherwise the host's retail pick wins over guest OpenBIOS preferences. */
|
||||
if (any_cannot_scph || !saw_peer)
|
||||
strncpy(out, "openbios", out_cap - 1);
|
||||
else if (host_prefer_scph)
|
||||
strncpy(out, "scph1001", out_cap - 1);
|
||||
else if (any_prefer_open)
|
||||
strncpy(out, "openbios", out_cap - 1);
|
||||
else
|
||||
strncpy(out, "scph1001", out_cap - 1);
|
||||
settle = netplay_bios_settle(seats, n);
|
||||
if (settle.kind == NETPLAY_BIOS_NONE) {
|
||||
if (why && why_cap)
|
||||
netplay_bios_describe_refusal(&settle, seats, n, why, why_cap);
|
||||
return 1;
|
||||
}
|
||||
strncpy(out, netplay_bios_token(settle.kind), out_cap - 1);
|
||||
out[out_cap - 1] = '\0';
|
||||
if (out_crc) *out_crc = settle.retail_crc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void psx_lobby_set_last_error(const char *text)
|
||||
{
|
||||
if (!text) text = "";
|
||||
strncpy(g_lc.join.last_error, text, sizeof(g_lc.join.last_error) - 1);
|
||||
g_lc.join.last_error[sizeof(g_lc.join.last_error) - 1] = '\0';
|
||||
}
|
||||
|
||||
int psx_lobby_set_ready(int ready)
|
||||
{
|
||||
char msg[512];
|
||||
@@ -4636,15 +4664,21 @@ int psx_lobby_set_ready(int ready)
|
||||
g_lc.memcard_offer.share ? "true" : "false");
|
||||
}
|
||||
if (g_lc.bios_offer.valid) {
|
||||
/* retail_crc names the image this peer would boot. Older
|
||||
* peers ignore the field; the server relays bios_offer as is. */
|
||||
char crc[16];
|
||||
netplay_bios_format_crc(g_lc.bios_offer.can_scph1001 ? g_lc.bios_offer.retail_crc : 0,
|
||||
crc, sizeof(crc));
|
||||
n = snprintf(msg, sizeof(msg),
|
||||
"{\"op\":\"set_ready\",\"ready\":%s,"
|
||||
"\"bios_offer\":{\"v\":1,\"prefer\":\"%s\","
|
||||
"\"can_openbios\":%s,\"can_scph1001\":%s}%s}",
|
||||
"\"can_openbios\":%s,\"can_scph1001\":%s,"
|
||||
"\"retail_crc\":\"%s\"}%s}",
|
||||
ready ? "true" : "false",
|
||||
g_lc.bios_offer.prefer_openbios ? "openbios" : "scph1001",
|
||||
g_lc.bios_offer.can_openbios ? "true" : "false",
|
||||
g_lc.bios_offer.can_scph1001 ? "true" : "false",
|
||||
memcard);
|
||||
crc, memcard);
|
||||
} else {
|
||||
n = snprintf(msg, sizeof(msg), "{\"op\":\"set_ready\",\"ready\":%s%s}",
|
||||
ready ? "true" : "false", memcard);
|
||||
@@ -4667,9 +4701,16 @@ int psx_lobby_request_start(const PsxLobbyMatchCaps *match_caps)
|
||||
caps_json[0] = '\0';
|
||||
if (match_caps && match_caps->valid) {
|
||||
caps_local = *match_caps;
|
||||
if (!caps_local.session_bios[0])
|
||||
(void)psx_lobby_settle_session_bios(caps_local.session_bios,
|
||||
sizeof(caps_local.session_bios));
|
||||
if (!caps_local.session_bios[0]) {
|
||||
char why[192];
|
||||
if (psx_lobby_settle_session_bios(caps_local.session_bios,
|
||||
sizeof(caps_local.session_bios),
|
||||
&caps_local.session_bios_crc,
|
||||
why, sizeof(why)) == 1) {
|
||||
psx_lobby_set_last_error(why);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
g_lc.match_caps = caps_local;
|
||||
append_match_caps_json(caps_json, sizeof(caps_json), &caps_local);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ int psx_netplay_rb_take_promote_sweep(void) { return 0; }
|
||||
int psx_netplay_rb_fmv_unlock_grace_active(void) { return 0; }
|
||||
void psx_netplay_rb_pump(void) {}
|
||||
int psx_netplay_rb_boot_dig0_gate(void) { return 0; }
|
||||
uint32_t psx_netplay_rb_boot_dig0_mismatch_since_ms(void) { return 0; }
|
||||
void psx_netplay_rb_boot_dig0_note_local(uint32_t core) { (void)core; }
|
||||
void psx_netplay_rb_boot_dig0_note_peer(uint32_t core) { (void)core; }
|
||||
int psx_netplay_rb_active(void) { return 0; }
|
||||
@@ -340,6 +341,13 @@ static uint32_t g_boot_dig0_peer_core;
|
||||
static uint8_t g_boot_dig0_peer_valid;
|
||||
static uint32_t g_boot_dig0_last_rexmit_ms;
|
||||
static int g_boot_dig0_wait_kind_logged;
|
||||
/* psx_host_mono_ms() since when the same pair of dig0s (below) has been known
|
||||
* and different; 0 = no mismatch. A peer FC can replace the peer latch, so a
|
||||
* new pair restarts the clock; a pair that holds for the watchdog's grace is
|
||||
* final and ends the match with its own reason. */
|
||||
static uint32_t g_boot_dig0_mismatch_since_ms;
|
||||
static uint32_t g_boot_dig0_mismatch_local;
|
||||
static uint32_t g_boot_dig0_mismatch_peer;
|
||||
static int g_follow_nack_pending;
|
||||
static uint32_t g_follow_nack_epoch;
|
||||
static uint32_t g_follow_nack_mismatch;
|
||||
@@ -6277,6 +6285,7 @@ void psx_netplay_rb_cold_reset(void)
|
||||
g_boot_dig0_peer_valid = 0;
|
||||
g_boot_dig0_last_rexmit_ms = 0;
|
||||
g_boot_dig0_wait_kind_logged = 0;
|
||||
g_boot_dig0_mismatch_since_ms = 0;
|
||||
g_follow_nack_pending = 0;
|
||||
g_follow_nack_epoch = 0;
|
||||
g_follow_nack_mismatch = 0;
|
||||
@@ -9410,6 +9419,13 @@ int psx_netplay_rb_boot_dig0_gate(void)
|
||||
}
|
||||
if (ld != pd) {
|
||||
boot_dig0_rexmit_local();
|
||||
if (!g_boot_dig0_mismatch_since_ms ||
|
||||
g_boot_dig0_mismatch_local != ld || g_boot_dig0_mismatch_peer != pd) {
|
||||
const uint32_t now = (uint32_t)psx_host_mono_ms();
|
||||
g_boot_dig0_mismatch_since_ms = now ? now : 1u;
|
||||
g_boot_dig0_mismatch_local = ld;
|
||||
g_boot_dig0_mismatch_peer = pd;
|
||||
}
|
||||
if (!g_boot_dig0_mismatch_logged) {
|
||||
g_boot_dig0_mismatch_logged = 1;
|
||||
fprintf(stderr,
|
||||
@@ -9422,6 +9438,7 @@ int psx_netplay_rb_boot_dig0_gate(void)
|
||||
}
|
||||
g_boot_dig0_mismatch_logged = 0;
|
||||
g_boot_dig0_wait_kind_logged = 0;
|
||||
g_boot_dig0_mismatch_since_ms = 0;
|
||||
/* One more send after we saw peer dig0 — the slower peer may still be
|
||||
* waiting (guest often syncs first; host then needs this rexmit). */
|
||||
g_boot_dig0_last_rexmit_ms = 0;
|
||||
@@ -9433,6 +9450,11 @@ int psx_netplay_rb_boot_dig0_gate(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t psx_netplay_rb_boot_dig0_mismatch_since_ms(void)
|
||||
{
|
||||
return g_boot_dig0_synced ? 0u : g_boot_dig0_mismatch_since_ms;
|
||||
}
|
||||
|
||||
int psx_netplay_rb_active(void)
|
||||
{
|
||||
return g_rb && rnet_rb_is_active(g_rb);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/* Netplay BIOS settle rule and its wire helpers.
|
||||
* Plain checks, not assert(): Release test builds define NDEBUG. */
|
||||
#include "netplay_bios_settle.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond) do { if (!(cond)) { \
|
||||
fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \
|
||||
++failures; } } while (0)
|
||||
|
||||
enum { SCPH1001 = 0x37157331u, SCPH5552 = 0xD786F0B9u };
|
||||
|
||||
/* A seat with a retail dump; OpenBIOS as given. */
|
||||
static NetplayBiosSeat retail(uint32_t crc, int can_open, int host)
|
||||
{
|
||||
NetplayBiosSeat s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
s.offered = 1;
|
||||
s.can_openbios = can_open;
|
||||
s.can_retail = 1;
|
||||
s.retail_crc = crc;
|
||||
s.is_host = host;
|
||||
return s;
|
||||
}
|
||||
|
||||
static NetplayBiosSeat openbios_only(int prefer, int host)
|
||||
{
|
||||
NetplayBiosSeat s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
s.offered = 1;
|
||||
s.can_openbios = 1;
|
||||
s.prefer_openbios = prefer;
|
||||
s.is_host = host;
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
NetplayBiosSeat seats[3];
|
||||
NetplayBiosSettle r;
|
||||
char text[256];
|
||||
|
||||
/* The 09-16 defect: SCPH-5552 host, SCPH-1001 guest, both link OpenBIOS.
|
||||
* Before the fix both booted their own dump. Now: OpenBIOS. */
|
||||
seats[0] = retail(SCPH5552, 1, 1);
|
||||
seats[1] = retail(SCPH1001, 1, 0);
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_OPENBIOS);
|
||||
CHECK(strcmp(netplay_bios_token(r.kind), "openbios") == 0);
|
||||
|
||||
/* Same pair, but builds that link one retail backend and no OpenBIOS:
|
||||
* no BIOS both can boot, so the host must refuse, naming both images. */
|
||||
seats[0] = retail(SCPH5552, 0, 1);
|
||||
seats[1] = retail(SCPH1001, 0, 0);
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_NONE);
|
||||
CHECK(r.seat_a == 0 && r.seat_b == 1);
|
||||
netplay_bios_describe_refusal(&r, seats, 2, text, sizeof(text));
|
||||
CHECK(strstr(text, "SCPH-5552") != NULL);
|
||||
CHECK(strstr(text, "SCPH-1001") != NULL);
|
||||
|
||||
/* EU vs EU without OpenBIOS works today and must keep working. */
|
||||
seats[0] = retail(SCPH5552, 0, 1);
|
||||
seats[1] = retail(SCPH5552, 0, 0);
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_RETAIL);
|
||||
CHECK(r.retail_crc == SCPH5552);
|
||||
CHECK(strcmp(netplay_bios_token(r.kind), "scph1001") == 0);
|
||||
|
||||
/* US vs US with OpenBIOS linked: retail, as before. */
|
||||
seats[0] = retail(SCPH1001, 1, 1);
|
||||
seats[1] = retail(SCPH1001, 1, 0);
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_RETAIL && r.retail_crc == SCPH1001);
|
||||
|
||||
/* A guest prefers OpenBIOS; the host's retail pick still wins. */
|
||||
seats[1].prefer_openbios = 1;
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_RETAIL && r.retail_crc == SCPH1001);
|
||||
|
||||
/* The host prefers OpenBIOS: OpenBIOS. */
|
||||
seats[0].prefer_openbios = 1;
|
||||
seats[1].prefer_openbios = 0;
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_OPENBIOS);
|
||||
|
||||
/* Three seats, nobody prefers OpenBIOS: retail when every image matches. */
|
||||
seats[0] = retail(SCPH1001, 1, 1);
|
||||
seats[1] = retail(SCPH1001, 1, 0);
|
||||
seats[2] = retail(SCPH1001, 1, 0);
|
||||
r = netplay_bios_settle(seats, 3);
|
||||
CHECK(r.kind == NETPLAY_BIOS_RETAIL);
|
||||
|
||||
/* One seat has only OpenBIOS: OpenBIOS for everyone who links it. */
|
||||
seats[2] = openbios_only(1, 0);
|
||||
r = netplay_bios_settle(seats, 3);
|
||||
CHECK(r.kind == NETPLAY_BIOS_OPENBIOS);
|
||||
|
||||
/* An older peer sends no CRC: retail is unsafe, OpenBIOS if all link it. */
|
||||
seats[0] = retail(SCPH1001, 1, 1);
|
||||
seats[1] = retail(0, 1, 0);
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_OPENBIOS);
|
||||
/* ...and a refusal when the host cannot run OpenBIOS. */
|
||||
seats[0].can_openbios = 0;
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_NONE);
|
||||
CHECK(r.seat_a == 1 && r.seat_b == -1);
|
||||
netplay_bios_describe_refusal(&r, seats, 2, text, sizeof(text));
|
||||
CHECK(strstr(text, "no usable BIOS") != NULL);
|
||||
|
||||
/* A seat that sent no offer at all is an older client: OpenBIOS. */
|
||||
memset(&seats[1], 0, sizeof(seats[1]));
|
||||
seats[0] = retail(SCPH1001, 1, 1);
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_OPENBIOS);
|
||||
|
||||
/* Every seat prefers OpenBIOS but one cannot run it: retail if possible. */
|
||||
seats[0] = retail(SCPH5552, 0, 1);
|
||||
seats[1] = retail(SCPH5552, 1, 0);
|
||||
seats[1].prefer_openbios = 1;
|
||||
r = netplay_bios_settle(seats, 2);
|
||||
CHECK(r.kind == NETPLAY_BIOS_RETAIL && r.retail_crc == SCPH5552);
|
||||
|
||||
/* No seats: OpenBIOS, the old default. */
|
||||
r = netplay_bios_settle(seats, 0);
|
||||
CHECK(r.kind == NETPLAY_BIOS_OPENBIOS);
|
||||
CHECK(strcmp(netplay_bios_token(NETPLAY_BIOS_NONE), "") == 0);
|
||||
|
||||
/* Wire helpers. */
|
||||
netplay_bios_format_crc(SCPH5552, text, sizeof(text));
|
||||
CHECK(strcmp(text, "d786f0b9") == 0);
|
||||
CHECK(netplay_bios_parse_crc(text) == SCPH5552);
|
||||
CHECK(netplay_bios_parse_crc("D786F0B9") == SCPH5552);
|
||||
CHECK(netplay_bios_parse_crc(" 37157331\n") == SCPH1001);
|
||||
netplay_bios_format_crc(0, text, sizeof(text));
|
||||
CHECK(text[0] == '\0');
|
||||
CHECK(netplay_bios_parse_crc("") == 0);
|
||||
CHECK(netplay_bios_parse_crc(NULL) == 0);
|
||||
CHECK(netplay_bios_parse_crc("1234567") == 0); /* too short */
|
||||
CHECK(netplay_bios_parse_crc("123456789") == 0); /* too long */
|
||||
CHECK(netplay_bios_parse_crc("scph1001") == 0); /* a token, not a CRC */
|
||||
netplay_bios_describe_crc(SCPH1001, text, sizeof(text));
|
||||
CHECK(strcmp(text, "SCPH-1001") == 0);
|
||||
netplay_bios_describe_crc(0x12345678u, text, sizeof(text));
|
||||
CHECK(strcmp(text, "BIOS 12345678") == 0);
|
||||
|
||||
if (failures) {
|
||||
fprintf(stderr, "netplay_bios_settle: %d check(s) FAILED\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("netplay_bios_settle: PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/* Netplay exit reasons and the boot-mismatch deadline.
|
||||
* Plain checks, not assert(): Release test builds define NDEBUG. */
|
||||
#include "netplay_exit_reason.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond) do { if (!(cond)) { \
|
||||
fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \
|
||||
++failures; } } while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const char *const ended[] = {
|
||||
"netplay_boot_mismatch", "netplay_peer_disconnect", "netplay_load_failed",
|
||||
"netplay_load_stall", "netplay_link_stall", "netplay_admit_stall",
|
||||
};
|
||||
size_t i;
|
||||
|
||||
/* Every origin netplay_soft_exit uses for a failure has a sentence, and it
|
||||
* fits the lobby client's last_error (192 bytes). */
|
||||
for (i = 0; i < sizeof(ended) / sizeof(ended[0]); ++i) {
|
||||
const char *t = netplay_exit_reason_text(ended[i]);
|
||||
CHECK(t != NULL);
|
||||
CHECK(t && strlen(t) > 20 && strlen(t) < 192);
|
||||
}
|
||||
CHECK(strstr(netplay_exit_reason_text("netplay_boot_mismatch"), "BIOS") != NULL);
|
||||
|
||||
/* A player who closed the window or pressed Escape needs no reason. */
|
||||
CHECK(netplay_exit_reason_text("sdl_window_close") == NULL);
|
||||
CHECK(netplay_exit_reason_text("netplay_escape") == NULL);
|
||||
CHECK(netplay_exit_reason_text("netplay_barrier_escape") == NULL);
|
||||
CHECK(netplay_exit_reason_text("") == NULL);
|
||||
CHECK(netplay_exit_reason_text(NULL) == NULL);
|
||||
|
||||
/* No mismatch seen: never final. */
|
||||
CHECK(!netplay_boot_mismatch_final(0u, 100000u, NETPLAY_BOOT_MISMATCH_GRACE_MS));
|
||||
/* Seen, grace not yet over, then over. */
|
||||
CHECK(!netplay_boot_mismatch_final(1000u, 3999u, NETPLAY_BOOT_MISMATCH_GRACE_MS));
|
||||
CHECK(netplay_boot_mismatch_final(1000u, 4000u, NETPLAY_BOOT_MISMATCH_GRACE_MS));
|
||||
/* The 32-bit millisecond clock wraps. */
|
||||
CHECK(!netplay_boot_mismatch_final(0xFFFFFF00u, 0x00000100u, NETPLAY_BOOT_MISMATCH_GRACE_MS));
|
||||
CHECK(netplay_boot_mismatch_final(0xFFFFFF00u, 0x00000C00u, NETPLAY_BOOT_MISMATCH_GRACE_MS));
|
||||
/* The old behaviour was a 20 s admit-stall watchdog; the grace is far
|
||||
* shorter than that. */
|
||||
CHECK(NETPLAY_BOOT_MISMATCH_GRACE_MS < 20000u);
|
||||
|
||||
if (failures) {
|
||||
fprintf(stderr, "netplay_exit_reason: %d check(s) FAILED\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("netplay_exit_reason: PASS");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user