The live-flag address was wrong: sub_8263E788's lis r11,-32003 gives
r11 = -2097348608 = 0x82FD0000 (recompiler value + 2^32), so the flag is at
0x82FC8851, not 0x83028851 (an arithmetic slip mis-multiplied 0x82FD0000).
We were forcing/reading an unrelated mapped byte, so the gate never saw the
forced value. Also force the game's cached 'live user exists' flags the
frontend online-access controllers check: 0x8308396C (14700) and 0x83084690
(18064).
The game's 'Party Play' gate treats its internal live flag (0x83028851) as a
USER INDEX, then checks XamUserGetSigninState(idx) and XamUserGetSigninInfo
(idx). rexglue only answered user 0, so the predicate failed and the option
stayed greyed out (no rexglue API is even reached on click). Now:
- XamUserGetSigninState/GetSigninInfo/GetXUID/GetName/GetGamerTag/
GetMembershipTier accept any slot < 4 and return the single profile
(signed-in, live, Gold).
- ForceLiveFlag writes 0 (the profile's real slot) instead of 1, since the
flag is a user index; slot 0 is safe for every index-into-live-array read.
Writes the title's internal 'live user' flag byte (0x83028851, derived from
sub_8263E788's lis -32003 / lbz -30639 = recompiler value -2097348608 ->
0x83030000 - 0x77AF) to 1 so the frontend stops refusing Xbox Live features
before it ever calls a rexglue API. Logs the first force.
- Deadlock: SetRelay held the transport mutex while join()ing the receive
thread, which needs the mutex to observe running_ and exit. With the 200ms
recv timeout the thread woke and hit the lock -> a room/server change froze
the game (every room keystroke reconnects the transport). StopThread now
detaches the thread under the lock and joins WITHOUT it.
- XamUserCheckPrivilege denied everything, which is the gate the game's
'create a party' flow checks (multiplayer sessions); grant requested
privileges so the P2P session flow opens, and log the mask.
The Run thread blocked in recvfrom with no SO_RCVTIMEO, so when there was no
inbound game traffic the 4s heartbeat never fired (the relay swept the
silent transport client after ~8s) AND StopLocked()'s join() deadlocked
SetRelay on a room change (hanging the ghost network loop). A 200ms receive
timeout wakes the loop regularly: the heartbeat keeps the transport alive
and shutdown/room-change joins complete promptly.
- #pragma pack(push,1) on the wire structs (the relay's proto is packed; the
transport's were not, which would inflate sizes and corrupt every datagram)
plus static_asserts pinning sizes to 7/31/1410/25/801.
- Heartbeat re-JOIN every 4s so the relay never times the transport out.
- REXLOG info for connect/join/session-reply so the flow is observable.
Route the game's own RakNet UDP through the ghost relay so real internet
peers can play, with the server as a pure relay/NAT stabilizer:
- XNetTransport: a rexglue relay client (joins the same relay + room as the
ghost client), a receive thread, and:
- SendGame(): wraps a datagram addressed to a virtual 10.80.0.x peer in a
relay Game payload and sends it OUTBOUND to the relay (no inbound
reachability / port forwarding needed).
- QuerySessions()/Sessions(): discovery - the room members that
XNetQosLookup reports to the game's session search.
- Receive injection: relayed Game payloads are pushed into the game's
registered sockets via XSocket::QueuePacket.
- XSocket: SendTo() to a virtual address routes through the transport;
RecvFrom() drains queued relayed packets first; sockets register with the
transport on Initialize.
- XNetQosLookup: queries the relay and returns the room's members as
discovered sessions (with their virtual addresses), signaling completion.
- XNetGetTitleXnAddr identity now shares one source of truth with the
transport (same 10.80.0.x).
Wire structs are minimal copies of the game's ghost protocol (kept
dependency-free); the relay's version check refuses mismatches.