Compare commits

..

20 commits

Author SHA1 Message Date
MiTHRAL
30ee3e2b86 netcode-revival: force the frontend 'enter online flow' gate (sub_82703960)
Some checks failed
Lint & Format Check / Format Check (push) Has been cancelled
create-party (sub_82A1B8E0) is only reached through the frontend online
flow, gated by sub_82703960 which requires: online latch 0x830670C8 == 0,
frontend-mode byte obj@*(0x8306707C)+194 != 0, XamUserGetSigninInfo.unk08==1
(already), and a mode check. Force the two state inputs and log the gate
inputs in the periodic dump so we can see exactly which check blocks.
2026-08-30 23:46:03 -04:00
MiTHRAL
edda6c9a06 netcode-revival: dump the party state machine's internal state
Some checks failed
Lint & Format Check / Format Check (push) Has been cancelled
The trace shows the machine spinning on XNetGetTitleXnAddr (state 1/0). Add a
rate-limited dump of the static party struct (0x82FD9750): state@336, eaip@344,
byte417 (EA flag), byte421 (online), byte423 (EA connected) - so we can see
whether the party bytes are mapped and exactly which gate keeps it stuck.
2026-08-28 22:43:17 -04:00
MiTHRAL
0da17439a7 netcode-revival: force the party struct's gate bytes (Track 1)
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
The party state machine (sub_82A1B310) cannot leave state 1 because the
game's EA-Nation flag (party+417) is 0 - set at party-create from a
config-string comparison (sub_82F4DA40) that never matches in the recomp.
The party struct is a static object at 0x82FD9750, so the periodic live-force
now also sets +417 (EA-Nation flag), +421 (online flag) and +423 (EA
connected marker). With byte417=1 the machine advances through the signin
gate, XNetConnect and XNetGetConnectStatus==2 to the 'session ready' state,
which unlocks session advertise/join (the actual netcode path).
2026-08-28 22:40:28 -04:00
MiTHRAL
05425a39f0 netcode-revival: broadcast Live signin-changed so the game's sign-in manager runs
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
The game only sets its internal 'live user exists' caches (14700/18064) when
its sign-in manager processes a real Live signin notification (code 0x80000001)
- which never happens without a Live backend. Broadcast XN_LIVE_SIGNINCHANGED
(0x80000001) with param (state<<8)|user = 0x200 a few times at startup (from
the periodic live-force) and on XamShowSigninUI, so the manager designates
the profile as the live user and the Party Play / party flow gates open.
2026-08-28 22:33:17 -04:00
MiTHRAL
6db6674892 netcode-revival: fix the EA-connect gate - XNetGetConnectStatus returns 2
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
The party state machine (sub_82A1B310 state 5) reads the RETURN VALUE of
XNetGetConnectStatus and only advances to 'EA connected' (party[423]=1, which
unlocks session advertise/join) when it equals 2. rexglue returned 0, so the
game stalled on 'Cannot Connect to EA Servers' forever despite the pointer
write. Also:

- XNetDnsLookup now actually resolves hostnames (getaddrinfo) into XNDNS
  (status=0, cina, aina[0..]) instead of always erroring - the EA resolver
  (sub_8298A460) busy-waits on it.
- XNetServerToInAddr ABI fixed: the game calls (caller, server_id,
  service_id, out), not (caller, XNADDR*, out, flags); returns the virtual
  IP (the party flow fakes EA connectivity via Connect/GetConnectStatus).
2026-08-28 22:23:34 -04:00
MiTHRAL
3016f14fe8 netcode-revival: fix the live-flag address + force the live-user cache flags
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
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).
2026-08-28 22:08:51 -04:00
MiTHRAL
aa8cda81a8 netcode-revival: report unk08=1 (Live indicator) in XamUserGetSigninInfo
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
The 'enter online flow' gate (sub_82703960) requires XamUserGetSigninInfo(
liveFlag).unk08 == 1 or it shows 'please login with Xbox Live'. Set unk08 to
1 when the profile is Live signed-in.
2026-08-28 22:03:44 -04:00
MiTHRAL
7b9d706f79 netcode-revival: expose gate-status diagnostics for the Party Play frontend gate
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
2026-08-28 21:37:37 -04:00
MiTHRAL
e09f224a6b netcode-revival: answer Xam user shims for every slot + live flag = user 0
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
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.
2026-08-28 14:52:17 -04:00
MiTHRAL
73d8f36af2 netcode-revival: XNetTransport::ForceLiveFlag - force the game's live-user static
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
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.
2026-08-28 14:32:13 -04:00
MiTHRAL
51b8ce5ed2 netcode-revival: trace XNetStartup (earliest online-path call)
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
2026-08-28 14:25:39 -04:00
MiTHRAL
7b6322b317 netcode-revival: trace the online-path XNet/Xam calls
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
Adds an 'xnet trace' log to the online-path functions (XNetGetTitleXnAddr,
XNetGetEthernetLinkStatus, QosListen/QosLookup, CreateKey, Connect,
GetConnectStatus, XamUserGetSigninState/Info, XamShowSigninUI, privilege)
gated on the xnet_trace cvar (default on during bring-up, rate-limited for
the polling signin-state calls). Running the game and clicking the online
flow now shows exactly which gate the party/EA path hits, so we can fix the
real check instead of guessing at the reverse-engineered globals.
2026-08-28 14:20:58 -04:00
MiTHRAL
9e300e14bc netcode-revival: report online title address + active link
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
The game's party state machine (57.cpp sub_82A1B310 state 1) freezes on
'connecting to EA Nation' because it gates on (XNetGetTitleXnAddr status &
0x60) == online AND XNetGetEthernetLinkStatus() != 0 to pick the online EA
server. rexglue returned STATIC (1) + link 0, so the state machine never
advanced. Now:

- XNetGetTitleXnAddr returns status 0x60 (the game's online bits; it checks
  (status & 0x60) in the EA resolver and session join) and populates
  inaOnline with the virtual IP.
- XNetGetEthernetLinkStatus returns active+100Mbps.

With the ONLINE status the party machine proceeds to CreateKey/RegisterKey,
the sign-in gate, XNetConnect and the session-ready state.
2026-08-28 14:13:10 -04:00
MiTHRAL
6fcc96de18 netcode-revival: fix SetRelay deadlock + grant multiplayer privileges
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
- 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.
2026-08-28 13:52:20 -04:00
MiTHRAL
3229aa3632 netcode-revival: report LIVE sign-in by default
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
The game gates its online/team flow on XamUserGetSigninState == 2 ('make a
team', 'connect to EA Nation' refused with error 1245 otherwise). rexglue
only returned 2 when user_live_signed_in was set, which defaulted to false,
so the game never opened its P2P session/invite flow. Default the cvar to
true so the netcode revival can reach the session code.
2026-08-28 13:38:11 -04:00
MiTHRAL
3ec27d115b netcode-revival: receive timeout on the relay transport socket
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
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.
2026-08-28 13:28:58 -04:00
MiTHRAL
01fe706c13 netcode-revival: pack the relay wire structs + heartbeat + transport logging
- #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.
2026-08-28 02:14:19 -04:00
MiTHRAL
bf3635d7ad netcode-revival: relay transport for the game's netcode (Phase 1b)
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
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.
2026-08-28 02:11:24 -04:00
MiTHRAL
767bd6e4bc netcode-revival: implement XNet session/address layer (Phase 0)
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
Make the game's RakNet-based netcode reach the session flow instead of
hard-failing on stubbed/looping XNet APIs:

- XNetGetTitleXnAddr: stable per-machine virtual identity (10.80.0.x +
  unique 6-byte abEnet from the profile XUID) - RakNet needs non-zero,
  per-machine MAC bytes for its GUID (zero broke RakPeer::Startup; identical
  MACs collide GUIDs).
- XNetXnAddrToInAddr / XNetInAddrToXnAddr / XNetServerToInAddr /
  XNetXnAddrToMachineId / XNetInAddrToString: resolve our virtual addresses
  instead of returning failure / '666.666.666.666'.
- XNetQosListen: accept session advertisement (was X_ERROR_FUNCTION_FAILED).
- XNetQosLookup: return success + empty XNQOS + signal completion (Phase 1
  wires the relay registry so it returns real sessions).
- XNetCreateKey/RegisterKey/UnregisterKey: generate/accept session keys.
- XNetConnect / XNetGetConnectStatus: report established.
- getsockopt: real basic reads (SO_* / TCP_NODELAY) off the native socket.
- XNetSetSystemLinkPort: accept.

Phase 1 (relay transport + session registry) builds on this.
2026-08-28 01:54:51 -04:00
MiTHRAL
395e1ce67d netcode-revival foundation: fix cvar registry teardown SIOF + imgui RasterizerGamma
Some checks are pending
Lint & Format Check / Format Check (push) Waiting to run
- cvar.cpp: the static FlagRegistrar destructors run during __cxa_finalize
  and touch the registry after its function-local statics are destroyed
  (static destruction-order fiasco) - the codegen tool segfaults at exit.
  Leak the registry/mutex/index so teardown order cannot bite.
- imgui_drawer.cpp: drop ImFontConfig::RasterizerGamma assignments (removed
  from imgui); keeps the build working with the modern imgui a fresh
  submodule checkout provides (the pinned imgui commit no longer exists).
2026-08-28 01:48:05 -04:00
10 changed files with 1209 additions and 111 deletions

View file

@ -0,0 +1,190 @@
/**
* netcode-revival: relay transport for the game's own netcode.
*
* The game's RakNet sockets talk to peers at "virtual" addresses
* (10.80.0.x, assigned per machine by the XNet layer). Real internet peers
* are not reachable at those addresses, so this transport routes their UDP
* through the ghost relay: a datagram to a virtual address is wrapped in a
* relay Game payload and sent OUTBOUND to the relay, which forwards it to
* the destination member's real socket (the server is a pure relay / NAT
* stabilizer). Inbound Game payloads are injected into the game's sockets
* via XSocket::QueuePacket, and XNetQosLookup discovers sessions by asking
* the relay who else is in the room.
*
* The wire structs below are minimal copies of the main repo's
* skate3_native_ghost_proto.h (deliberately kept dependency-free and
* one-way: the SDK must not include game-side headers). If they ever
* diverge, the relay's version check refuses the connection rather than
* corrupting traffic.
*/
#pragma once
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_set>
#include <vector>
namespace rex {
namespace system {
class XSocket;
// The wire structs below must match the relay's byte-for-byte (the main repo
// packs them with #pragma pack(1); leaving padding here would inflate the
// sizes and silently corrupt every datagram).
#pragma pack(push, 1)
// Header of every relay datagram ("GHST" magic, little-endian type + size).
// Must match kGhostMagic / PacketHeader in skate3_native_ghost_proto.h.
struct XNetRelayHeader {
uint32_t magic; // "GHST" little-endian
uint8_t type;
uint16_t payload_size;
};
// kTypeJoin payload. Must match JoinPayload.
struct XNetRelayJoin {
uint8_t version;
uint8_t room_len;
char room[8];
uint8_t name_len;
char name[16];
uint32_t virtual_ip; // host-order 10.80.0.x
};
// kTypeGame payload. Must match GamePayload.
struct XNetRelayGame {
uint32_t dst_virtual_ip; // recipient (host order) or 0 = all room members
uint32_t src_virtual_ip; // filled by the relay on forward (host order)
uint16_t data_len;
uint8_t data[1400];
};
// kTypeSessionReply payload. Must match SessionReplyPayload.
struct XNetRelaySessionEntry {
uint32_t session_id;
uint32_t virtual_ip; // host order
uint8_t name_len;
char name[16];
};
struct XNetRelaySessionReply {
uint8_t count;
XNetRelaySessionEntry list[32];
};
#pragma pack(pop)
// One discovered session (a member of our room).
struct XNetSession {
uint32_t session_id;
uint32_t virtual_ip; // host order
char name[16];
uint8_t name_len;
};
class XNetTransport {
public:
static constexpr uint8_t kVersion = 5;
static constexpr uint32_t kMagic = 0x54484753u; // "GHST" LE
enum Type : uint8_t {
kJoin = 1,
kHello = 2,
kGame = 10,
kSessionQuery = 11,
kSessionReply = 12,
};
static XNetTransport& Instance();
// Configure the relay (host[:port], port) and the room to join. Idempotent;
// a changed relay/room reconnects. Called by the game-side ghost client
// when it (re)joins a room, so the game's netcode follows the same room.
void SetRelay(const char* host, uint16_t port, const char* room);
// True once the transport has successfully joined the relay.
bool Active() const;
// True when ip (host order) is in the virtual 10.80.0.x range.
static bool IsVirtualIp(uint32_t host_order_ip);
// Route one datagram to a virtual destination via the relay. Returns the
// datagram length on accept, -1 when the transport is not active or the
// destination is not a virtual address.
int SendGame(const void* buf, uint32_t len, uint32_t dst_virtual_ip_host_order);
// The local machine's virtual IP (host order) - mirrors XNetGetTitleXnAddr.
static uint32_t LocalVirtualIp();
// netcode-revival: force the title's internal "live user" static flag
// (Skate 3 build) so its frontend stops refusing "Xbox Live features"
// before it ever calls a rexglue API. Called periodically by the ghost
// client. Fragile: the address is tied to this title's build.
void ForceLiveFlag();
// netcode-revival: diagnostics for the frontend "Xbox Live features" gate
// (the game checks these before enabling the Party Play / online menu).
struct GateStatus {
bool live_flag_mapped = false;
uint8_t live_flag_value = 0; // the game's "live user index"
uint32_t signin_state[4] = {}; // XamUserGetSigninState per slot
bool signin_info_ok[4] = {}; // XamUserGetSigninInfo per slot succeeds
bool link_active = false; // XNetGetEthernetLinkStatus != 0
bool title_online = false; // XNetGetTitleXnAddr status & 0x60
};
GateStatus GetGateStatus() const;
// --- discovery (used by XNetQosLookup) ------------------------------------
// Send a session query for our room; the reply lands asynchronously and is
// surfaced by Sessions() once WaitForReply returns.
void QuerySessions();
// Block up to timeout_ms for a fresh session reply to arrive.
void WaitForReply(int timeout_ms);
std::vector<XNetSession> Sessions() const;
// --- receive injection -----------------------------------------------------
// A UDP socket the game expects inbound netcode datagrams on. Delivered
// Game payloads are pushed into every registered socket via QueuePacket.
void RegisterSocket(XSocket* s);
void UnregisterSocket(XSocket* s);
~XNetTransport();
private:
XNetTransport() = default;
XNetTransport(const XNetTransport&) = delete;
XNetTransport& operator=(const XNetTransport&) = delete;
void Reconnect();
// Stop the receive thread and join it WITHOUT holding mu_ (the Run thread
// needs mu_ to observe running_ and exit; joining under the lock deadlocks
// once recvfrom times out and the thread tries to lock mu_).
void StopThread();
void Run();
void HandlePacket(const uint8_t* data, int size);
int64_t sock_ = -1; // native UDP socket to the relay (platform handle)
// Resolved relay endpoint as raw bytes (sockaddr_in is 16 bytes on POSIX
// and Windows); cast back in the .cpp. Kept byte-form so this public
// header stays platform-independent.
uint8_t relay_addr_[16] = {};
bool relay_resolved_ = false;
uint8_t join_pkt_[7 + sizeof(XNetRelayJoin)] = {};
uint16_t join_pkt_size_ = 0;
double last_join_ = 0.0; // monotonic seconds (heartbeat to beat the timeout)
std::thread thread_;
bool running_ = false;
mutable std::mutex mu_;
std::string host_;
uint16_t port_ = 0;
std::string room_;
bool active_ = false;
bool joined_ = false;
uint64_t reply_seq_ = 0; // bumped by every SessionReply
std::vector<XNetSession> sessions_;
std::unordered_set<XSocket*> sockets_;
};
} // namespace system
} // namespace rex

View file

@ -34,19 +34,24 @@ std::mutex g_mutex;
// Recursive: FlagRegistrar chain methods re-enter; change callbacks invoked
// from SetFlagByName must not mutate the registry.
std::recursive_mutex& GetRegistryMutex() {
static std::recursive_mutex m;
return m;
// Leaky (heap, never destroyed) so teardown order can't bite: static
// FlagRegistrar destructors run during __cxa_finalize and unregister their
// flags; if these function-local statics are destroyed first, that
// teardown crashes (static destruction-order fiasco, seen at exit of the
// codegen tool). Intentionally leaked - the process is ending.
static auto* m = new std::recursive_mutex();
return *m;
}
// Flag registry - use functions to avoid static init order issues
std::vector<FlagEntry>& GetRegistryStorage() {
static std::vector<FlagEntry> registry;
return registry;
static auto* registry = new std::vector<FlagEntry>();
return *registry;
}
std::unordered_map<std::string, size_t>& GetRegistryIndex() {
static std::unordered_map<std::string, size_t> index;
return index;
static auto* index = new std::unordered_map<std::string, size_t>();
return *index;
}
std::vector<std::pair<std::string, std::string>>& GetCommandLineOverrides() {

View file

@ -15,6 +15,7 @@
#include <cstring>
#include <rex/chrono/clock.h>
#include <rex/cvar.h>
#include <rex/kernel/xam/module.h>
#include <rex/kernel/xam/private.h>
#include <rex/kernel/xboxkrnl/error.h>
@ -26,6 +27,7 @@
#include <rex/system/kernel_state.h>
#include <rex/system/xevent.h>
#include <rex/system/xsocket.h>
#include <rex/system/xnet_transport.h>
#include <rex/system/xthread.h>
#include <rex/system/xtypes.h>
@ -35,6 +37,7 @@
#include <winsock2.h> // NOLINT(build/include_order)
#elif REX_PLATFORM_LINUX || REX_PLATFORM_MAC
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/select.h>
@ -47,6 +50,15 @@ namespace xam {
using namespace rex::system;
using namespace rex::system::xam;
// netcode-revival: trace the online-path calls so a run shows exactly which
// XNet/Xam gates the game hits (party/Ea Nation debugging). Default on during
// the revival bring-up; disable with xnet_trace=false before any release.
REXCVAR_DEFINE_BOOL(xnet_trace, true, "Kernel",
"Log online-path XNet/Xam calls (netcode debugging)");
bool XnetTrace() {
return REXCVAR_GET(xnet_trace);
}
// https://github.com/G91/TitanOffLine/blob/1e692d9bb9dfac386d08045ccdadf4ae3227bb5e/xkelib/xam/xamNet.h
enum {
XNCALLER_INVALID = 0x0,
@ -125,6 +137,58 @@ struct XWSAOVERLAPPED {
rex::be<uint32_t> event_handle;
};
// ---- netcode-revival: virtual XNet identity ---------------------------------
// The Xbox title address (XNADDR) is what RakNet folds into its peer GUID and
// what the game resolves into a socket address via XNetXnAddrToInAddr. We give
// this machine a stable, unique identity: a reserved virtual IP (10.80.0.x,
// "LAN" class) plus a unique 6-byte abEnet. RakPeer::Startup requires
// non-zero abEnet and different machines need distinct values (a zero MAC
// breaks startup; identical MACs collide GUIDs). Derived from the signed-in
// profile XUID so it is stable across runs on one box.
struct XnetIdentity {
uint8_t machine_id = 1; // 1..254
uint8_t ab_enet[6] = {};
uint32_t virtual_ip = 0; // host order
};
uint64_t XnetSeed() {
const auto& profile = REX_KERNEL_STATE()->user_profile();
uint64_t seed = profile->xuid();
if (seed == 0) {
// Fallback when no profile is signed in: a stable-enough per-process
// seed (later refinements can persist one).
seed = 0x52454B4E45544CULL;
}
return seed;
}
const XnetIdentity& XnetLocalIdentity() {
static const XnetIdentity s_identity = [] {
XnetIdentity id;
uint64_t seed = XnetSeed();
id.machine_id = uint8_t((seed ^ (seed >> 32)) & 0xFEu) + 1; // 1..254
id.ab_enet[0] = id.machine_id;
for (int i = 1; i < 6; ++i) {
seed = seed * 6364136223846793005ULL + 1442695040888963407ULL;
id.ab_enet[i] = uint8_t(seed >> 24);
}
// Single source of truth with the relay transport (same 10.80.0.x).
id.virtual_ip = rex::system::XNetTransport::LocalVirtualIp();
return id;
}();
return s_identity;
}
// Simple xorshift64* PRNG for session keys (XNetCreateKey is required to
// produce a 16-byte XNKEY; content only needs to be unique per session).
uint64_t XnetNextRand() {
static uint64_t s = XnetSeed() | 1ULL;
s ^= s >> 12;
s ^= s << 25;
s ^= s >> 27;
return s * 2685821657736338717ULL;
}
void LoadSockaddr(const uint8_t* ptr, sockaddr* out_addr) {
out_addr->sa_family = memory::load_and_swap<uint16_t>(ptr + 0);
switch (out_addr->sa_family) {
@ -180,6 +244,9 @@ struct XNetStartupParams {
XNetStartupParams xnet_startup_params = {0};
u32 NetDll_XNetStartup_entry(u32 caller, ppc_ptr_t<XNetStartupParams> params) {
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetStartup(caller={})", caller);
}
if (params) {
assert_true(params->cfgSizeOfStruct == sizeof(XNetStartupParams));
std::memcpy(&xnet_startup_params, params, sizeof(XNetStartupParams));
@ -433,27 +500,23 @@ struct XnAddrStatus {
};
u32 NetDll_XNetGetTitleXnAddr_entry(u32 caller, ppc_ptr_t<XNADDR> addr_ptr) {
// Just return a loopback address atm.
addr_ptr->ina.s_addr = htonl(INADDR_LOOPBACK);
addr_ptr->inaOnline.s_addr = 0;
// netcode-revival: report this machine as ONLINE so the game's party
// state machine advances (Skate 3 maps status bit 0x60 = online: the
// EA-Nation resolver and session-join both gate on it). Also expose the
// online address so online-address paths have a valid inaOnline.
const XnetIdentity& id = XnetLocalIdentity();
addr_ptr->ina.s_addr = htonl(id.virtual_ip);
addr_ptr->inaOnline.s_addr = htonl(id.virtual_ip);
addr_ptr->wPortOnline = 0;
// TODO(gibbed): A proper mac address.
// RakNet's 360 version appears to depend on abEnet to create "random" 64-bit
// numbers. A zero value will cause RakPeer::Startup to fail. This causes
// 58411436 to crash on startup.
// The 360-specific code is scrubbed from the RakNet repo, but there's still
// traces of what it's doing which match the game code.
// https://github.com/facebookarchive/RakNet/blob/master/Source/RakPeer.cpp#L382
// https://github.com/facebookarchive/RakNet/blob/master/Source/RakPeer.cpp#L4527
// https://github.com/facebookarchive/RakNet/blob/master/Source/RakPeer.cpp#L4467
// "Mac address is a poor solution because you can't have multiple connections
// from the same system"
std::memset(addr_ptr->abEnet, 0xCC, 6);
std::memcpy(addr_ptr->abEnet, id.ab_enet, 6);
std::memset(addr_ptr->abOnline, 0, 20);
return XnAddrStatus::XNET_GET_XNADDR_STATIC;
// 0x60 = the game's "online" status bits (not the standard 0-3 enum; the
// game checks (status & 0x60) everywhere it means "connected to LIVE").
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetGetTitleXnAddr(caller={}) -> status 0x60 (online)",
caller);
}
return 0x60;
}
u32 NetDll_XNetGetDebugXnAddr_entry(u32 caller, ppc_ptr_t<XNADDR> addr_ptr) {
@ -464,33 +527,57 @@ u32 NetDll_XNetGetDebugXnAddr_entry(u32 caller, ppc_ptr_t<XNADDR> addr_ptr) {
}
u32 NetDll_XNetXnAddrToMachineId_entry(u32 caller, ppc_ptr_t<XNADDR> addr_ptr, mapped_u32 id_ptr) {
// Tell the caller we're not signed in to live (non-zero ret)
return 1;
// netcode-revival: report this machine's stable machine id instead of "not
// signed in to live", so identity-dependent flows proceed.
if (id_ptr) {
*id_ptr = XnetLocalIdentity().machine_id;
}
return 0;
}
void NetDll_XNetInAddrToString_entry(u32 caller, u32 in_addr, mapped_string string_out,
u32 string_size) {
rex::string::rex_strcpy(string_out, string_size, "666.666.666.666");
char buf[INET_ADDRSTRLEN] = {};
in_addr_t a = ntohl(in_addr);
std::snprintf(buf, sizeof(buf), "%u.%u.%u.%u", (a >> 24) & 0xFF, (a >> 16) & 0xFF,
(a >> 8) & 0xFF, a & 0xFF);
rex::string::rex_strcpy(string_out, string_size, buf);
}
// This converts a XNet address to an IN_ADDR. The IN_ADDR is used for
// subsequent socket calls (like a handle to a XNet address)
// subsequent socket calls (like a handle to a XNet address).
// netcode-revival: our XNADDRs carry the virtual IP in ina (network order);
// resolve it into the requested in_addr.
u32 NetDll_XNetXnAddrToInAddr_entry(u32 caller, ppc_ptr_t<XNADDR> xn_addr, mapped_void xid,
mapped_void in_addr) {
return 1;
if (!xn_addr || !in_addr) {
return X_ERROR_INVALID_PARAMETER;
}
uint32_t ip = xn_addr->ina.s_addr;
std::memcpy(in_addr, &ip, sizeof(ip));
return 0;
}
// Does the reverse of the above.
// FIXME: Arguments may not be correct.
// netcode-revival: rebuild an XNADDR from an in_addr (virtual IP).
u32 NetDll_XNetInAddrToXnAddr_entry(u32 caller, mapped_void in_addr, ppc_ptr_t<XNADDR> xn_addr,
mapped_void xid) {
return 1;
if (!in_addr || !xn_addr) {
return X_ERROR_INVALID_PARAMETER;
}
std::memcpy(&xn_addr->ina.s_addr, in_addr, sizeof(uint32_t));
xn_addr->inaOnline.s_addr = 0;
xn_addr->wPortOnline = 0;
std::memset(xn_addr->abEnet, 0, 6);
std::memset(xn_addr->abOnline, 0, 20);
return 0;
}
// https://www.google.com/patents/WO2008112448A1?cl=en
// Reserves a port for use by system link
// Reserves a port for use by system link. netcode-revival: accept any port.
u32 NetDll_XNetSetSystemLinkPort_entry(u32 caller, u32 port) {
return 1;
(void)port;
return 0;
}
// https://github.com/ILOVEPIE/Cxbx-Reloaded/blob/master/src/CxbxKrnl/EmuXOnline.h#L39
@ -503,17 +590,55 @@ struct XEthernetStatus {
};
u32 NetDll_XNetGetEthernetLinkStatus_entry(u32 caller) {
return 0;
// netcode-revival: report an active 100Mbps link. The game's party state
// machine only reaches the EA-Nation/online path when the link is up
// (sub_82A1AAE8 returns the online server IP only if this is non-zero),
// so returning 0 left it frozen on "connecting to EA Nation" forever.
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetGetEthernetLinkStatus(caller={}) -> active 100Mbps",
caller);
}
return XEthernetStatus::XNET_ETHERNET_LINK_ACTIVE |
XEthernetStatus::XNET_ETHERNET_LINK_100MBPS;
}
u32 NetDll_XNetDnsLookup_entry(u32 caller, mapped_string host, u32 event_handle, mapped_u32 pdns) {
// TODO(gibbed): actually implement this
if (pdns) {
auto dns_guest = REX_KERNEL_MEMORY()->SystemHeapAlloc(sizeof(XNDNS));
auto dns = REX_KERNEL_MEMORY()->TranslateVirtual<XNDNS*>(dns_guest);
dns->status = 1; // non-zero = error
*pdns = dns_guest;
// netcode-revival: actually resolve the hostname so the EA Nation resolver
// (sub_8298A460) and any DNS consumer get a valid address instead of the
// old always-error (status=1) result. The game polls status until it is
// not XNET_STATUS_PENDING (10036), then requires status==0 && cina>0 and
// uses aina[0].
auto dns_guest = REX_KERNEL_MEMORY()->SystemHeapAlloc(sizeof(XNDNS));
auto dns = REX_KERNEL_MEMORY()->TranslateVirtual<XNDNS*>(dns_guest);
dns->status = 1;
dns->cina = 0;
if (host) {
addrinfo hints = {};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
addrinfo* res = nullptr;
if (getaddrinfo(host, nullptr, &hints, &res) == 0 && res != nullptr) {
uint32_t n = 0;
for (addrinfo* p = res; p != nullptr && n < 8; p = p->ai_next) {
auto* sin = reinterpret_cast<sockaddr_in*>(p->ai_addr);
if (sin->sin_family == AF_INET) {
dns->aina[n++] = sin->sin_addr; // network order
}
}
freeaddrinfo(res);
dns->status = 0;
dns->cina = n;
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetDnsLookup('{}') -> {} address(es)",
static_cast<const char*>(host), n);
}
} else {
REXLOG_WARN("xnet trace: XNetDnsLookup('{}') failed to resolve",
static_cast<const char*>(host));
}
}
*pdns = dns_guest;
if (event_handle) {
auto ev = REX_KERNEL_OBJECTS()->LookupObject<XEvent>(event_handle);
assert_not_null(ev);
@ -556,7 +681,201 @@ u32 NetDll_XNetQosRelease_entry(u32 caller, ppc_ptr_t<XNQOS> qos) {
u32 NetDll_XNetQosListen_entry(u32 caller, mapped_void id, mapped_void data, u32 data_size, u32 r7,
u32 flags) {
return X_ERROR_FUNCTION_FAILED;
// netcode-revival Phase 0: accept session advertisement so the host flow
// proceeds. No registry yet (the relay-backed session registry + QoS
// discovery land in Phase 1); QosLookup returns an empty result set for
// now so the game's search completes cleanly.
(void)id;
(void)data;
(void)data_size;
(void)r7;
(void)flags;
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetQosListen(caller={}, flags={}) -> ok (advertise)",
caller, flags);
}
return 0;
}
// netcode-revival: session search. Phase 1: ask the relay who else is in our
// room and hand the discovered sessions to the game, then signal the
// completion event. With no relay configured the search returns empty (the
// game's flow completes cleanly).
u32 NetDll_XNetQosLookup_entry(u32 caller, u32 cxna, mapped_void apxna, mapped_void apxnk,
u32 cxnk, mapped_void apxnaResults, mapped_u32 apxnqosResults,
mapped_void axnqosInfoResults, u32 dwTimeoutMs, u32 hEvent,
u32 dwFlags) {
(void)cxna;
(void)apxna;
(void)apxnk;
(void)cxnk;
(void)apxnaResults;
(void)axnqosInfoResults;
(void)dwFlags;
using rex::system::XNetTransport;
std::vector<rex::system::XNetSession> sessions;
if (XNetTransport::Instance().Active()) {
// Ask the relay, wait up to ~500ms for the reply, then report members.
XNetTransport::Instance().QuerySessions();
XNetTransport::Instance().WaitForReply(500);
sessions = XNetTransport::Instance().Sessions();
}
// Up to 8 discovered sessions (the game's XNQOS addresses array is 8).
auto qos_guest = REX_KERNEL_MEMORY()->SystemHeapAlloc(sizeof(XNQOS) + sizeof(XNQOSINFO) * 7);
auto qos = REX_KERNEL_MEMORY()->TranslateVirtual<XNQOS*>(qos_guest);
qos->count = qos->count_pending = 0;
uint32_t n = 0;
for (const auto& s : sessions) {
if (n >= 8) {
break;
}
if (!XNetTransport::IsVirtualIp(s.virtual_ip) || s.virtual_ip == XNetTransport::LocalVirtualIp()) {
continue; // only real peers, not ourselves
}
// Each session's QoS info references the host's XNADDR (built from the
// discovered virtual IP) as its QoS data.
XNQOSINFO& info = qos->info[n];
info.flags = 1; // XNET_QOS_FLAG_NONE equivalent - address present
info.probes_xmit = 1;
info.probes_recv = 1;
info.data_len = sizeof(XNADDR);
auto xnaddr_guest = REX_KERNEL_MEMORY()->SystemHeapAlloc(sizeof(XNADDR));
auto xnaddr = REX_KERNEL_MEMORY()->TranslateVirtual<XNADDR*>(xnaddr_guest);
std::memset(xnaddr, 0, sizeof(XNADDR));
xnaddr->ina.s_addr = htonl(s.virtual_ip);
info.data_ptr = xnaddr_guest;
++n;
}
qos->count = qos->count_pending = n;
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetQosLookup(caller={}) -> {} session(s) found",
caller, n);
}
if (apxnqosResults) {
*apxnqosResults = qos_guest;
}
if (hEvent) {
auto ev = REX_KERNEL_OBJECTS()->LookupObject<XEvent>(hEvent);
if (ev) {
ev->Set(0, false);
}
}
return 0;
}
// netcode-revival: generate a 16-byte session key (XNKEY).
u32 NetDll_XNetCreateKey_entry(u32 caller, mapped_void key) {
if (!key) {
return X_ERROR_INVALID_PARAMETER;
}
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetCreateKey(caller={})", caller);
}
uint8_t* out = static_cast<uint8_t*>(key);
uint64_t r = XnetNextRand();
for (int i = 0; i < 16; ++i) {
if ((i & 7) == 0) {
r = XnetNextRand();
}
out[i] = uint8_t(r >> ((i & 7) * 8));
}
return 0;
}
// netcode-revival: accept + (Phase 1) register the session key.
u32 NetDll_XNetRegisterKey_entry(u32 caller, mapped_void key) {
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetRegisterKey(caller={})", caller);
}
(void)key;
return 0;
}
u32 NetDll_XNetUnregisterKey_entry(u32 caller, mapped_void key) {
(void)key;
return 0;
}
// netcode-revival: connecting to a peer is reported as established.
u32 NetDll_XNetConnect_entry(u32 caller, ppc_ptr_t<XNADDR> xn_addr) {
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetConnect(caller={}, peer=10.80.0.{})",
caller, xn_addr ? (ntohl(xn_addr->ina.s_addr) & 0xFFu) : 0);
}
(void)xn_addr;
return 0;
}
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/xna/xnetgetconnectstatus
// INT XNetGetConnectStatus(const XNADDR* pxna, INT* pxncs)
u32 NetDll_XNetGetConnectStatus_entry(u32 caller, ppc_ptr_t<XNADDR> xn_addr, mapped_void status_ptr) {
// netcode-revival: the party state machine (sub_82A1B310 state 5) reads the
// RETURN VALUE and only advances to 'EA connected' when it equals 2
// (XNET_CONNECT_STATUS_CONNECTED). Returning 0 stalled it on "Cannot
// Connect to EA Servers" forever.
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetGetConnectStatus(caller={}) -> CONNECTED", caller);
}
(void)xn_addr;
if (status_ptr) {
const uint32_t connected = 2; // XNET_CONNECT_STATUS_CONNECTED
std::memcpy(status_ptr, &connected, sizeof(connected));
}
return 2;
}
// The game calls XNetServerToInAddr(caller=1, server_id, service_id, out)
// (sub_82F38580 maps r3/r4/r5 accordingly). netcode-revival: return our
// virtual IP (the machine's online identity); the party flow fakes EA
// connectivity via XNetConnect/XNetGetConnectStatus, so any stable address
// works here.
u32 NetDll_XNetServerToInAddr_entry(u32 caller, u32 server_id, u32 service_id,
mapped_void pina) {
(void)caller;
(void)server_id;
(void)service_id;
if (!pina) {
return X_ERROR_INVALID_PARAMETER;
}
if (XnetTrace()) {
REXLOG_INFO("xnet trace: XNetServerToInAddr(server={}, service=0x{:08X}) -> virtual ip",
server_id, service_id);
}
const uint32_t ip = htonl(rex::system::XNetTransport::LocalVirtualIp());
std::memcpy(pina, &ip, sizeof(ip));
return 0;
}
// Basic getsockopt reads the game relies on (SO_ERROR, SO_REUSEADDR,
// SO_BROADCAST, SO_SNDBUF/RCVBUF, TCP_NODELAY, ...) served from the real
// native socket.
u32 NetDll_getsockopt_entry(u32 caller, u32 socket_handle, u32 level, u32 optname,
mapped_void optval_ptr, mapped_u32 optlen_ptr) {
auto socket = REX_KERNEL_OBJECTS()->LookupObject<XSocket>(socket_handle);
if (!socket) {
XThread::SetLastError(0x2736); // WSAENOTSOCK
return -1;
}
if (!optval_ptr || !optlen_ptr) {
XThread::SetLastError(0x2736);
return -1;
}
int len = int(*optlen_ptr);
// Windows takes char*, POSIX void*; a char* host pointer satisfies both.
char* optval_host = reinterpret_cast<char*>(static_cast<void*>(optval_ptr));
int ret = getsockopt(int(socket->native_handle()), int(level), int(optname), optval_host,
reinterpret_cast<socklen_t*>(&len));
if (ret != 0) {
XThread::SetLastError(0x2743); // WSAENOPROTOOPT
return -1;
}
*optlen_ptr = uint32_t(len);
return 0;
}
u32 NetDll_inet_addr_entry(mapped_string addr_ptr) {
@ -1027,25 +1346,25 @@ REX_EXPORT_STUB(__imp__NetDll_XHttpSetStatusCallback);
REX_EXPORT_STUB(__imp__NetDll_XHttpShutdown);
REX_EXPORT_STUB(__imp__NetDll_XHttpStartup);
REX_EXPORT_STUB(__imp__NetDll_XHttpWriteData);
REX_EXPORT_STUB(__imp__NetDll_XNetConnect);
REX_EXPORT_STUB(__imp__NetDll_XNetCreateKey);
REX_EXPORT(__imp__NetDll_XNetConnect, rex::kernel::xam::NetDll_XNetConnect_entry)
REX_EXPORT(__imp__NetDll_XNetCreateKey, rex::kernel::xam::NetDll_XNetCreateKey_entry)
REX_EXPORT_STUB(__imp__NetDll_XNetDnsReverseLookup);
REX_EXPORT_STUB(__imp__NetDll_XNetDnsReverseRelease);
REX_EXPORT_STUB(__imp__NetDll_XNetGetBroadcastVersionStatus);
REX_EXPORT_STUB(__imp__NetDll_XNetGetConnectStatus);
REX_EXPORT(__imp__NetDll_XNetGetConnectStatus, rex::kernel::xam::NetDll_XNetGetConnectStatus_entry)
REX_EXPORT_STUB(__imp__NetDll_XNetGetSystemLinkPort);
REX_EXPORT_STUB(__imp__NetDll_XNetGetXnAddrPlatform);
REX_EXPORT_STUB(__imp__NetDll_XNetInAddrToServer);
REX_EXPORT_STUB(__imp__NetDll_XNetQosGetListenStats);
REX_EXPORT_STUB(__imp__NetDll_XNetQosLookup);
REX_EXPORT_STUB(__imp__NetDll_XNetRegisterKey);
REX_EXPORT(__imp__NetDll_XNetQosLookup, rex::kernel::xam::NetDll_XNetQosLookup_entry)
REX_EXPORT(__imp__NetDll_XNetRegisterKey, rex::kernel::xam::NetDll_XNetRegisterKey_entry)
REX_EXPORT_STUB(__imp__NetDll_XNetReplaceKey);
REX_EXPORT_STUB(__imp__NetDll_XNetServerToInAddr);
REX_EXPORT(__imp__NetDll_XNetServerToInAddr, rex::kernel::xam::NetDll_XNetServerToInAddr_entry)
REX_EXPORT_STUB(__imp__NetDll_XNetSetOpt);
REX_EXPORT_STUB(__imp__NetDll_XNetStartupEx);
REX_EXPORT_STUB(__imp__NetDll_XNetTsAddrToInAddr);
REX_EXPORT_STUB(__imp__NetDll_XNetUnregisterInAddr);
REX_EXPORT_STUB(__imp__NetDll_XNetUnregisterKey);
REX_EXPORT(__imp__NetDll_XNetUnregisterKey, rex::kernel::xam::NetDll_XNetUnregisterKey_entry)
REX_EXPORT_STUB(__imp__NetDll_XmlDownloadContinue);
REX_EXPORT_STUB(__imp__NetDll_XmlDownloadGetParseTime);
REX_EXPORT_STUB(__imp__NetDll_XmlDownloadGetReceivedDataSize);
@ -1096,4 +1415,4 @@ REX_EXPORT_STUB(__imp__NetDll_XnpUnregisterKeyForCallerType);
REX_EXPORT_STUB(__imp__NetDll_XnpUpdateConfigParams);
REX_EXPORT_STUB(__imp__NetDll_getpeername);
REX_EXPORT_STUB(__imp__NetDll_getsockname);
REX_EXPORT_STUB(__imp__NetDll_getsockopt);
REX_EXPORT(__imp__NetDll_getsockopt, rex::kernel::xam::NetDll_getsockopt_entry)

View file

@ -43,6 +43,8 @@ using namespace rex::system::xam;
extern std::atomic<int> xam_dialogs_shown_;
REXCVAR_DECLARE(bool, xnet_trace);
i32 XamUserGetXUID_entry(u32 user_index, u32 type_mask, mapped_u64 xuid_ptr) {
assert_true(type_mask == 1 || type_mask == 2 || type_mask == 3 || type_mask == 4 ||
type_mask == 7);
@ -52,19 +54,17 @@ i32 XamUserGetXUID_entry(u32 user_index, u32 type_mask, mapped_u64 xuid_ptr) {
uint32_t result = X_E_NO_SUCH_USER;
uint64_t xuid = 0;
if (user_index < 4) {
if (user_index == 0) {
const auto& user_profile = REX_KERNEL_STATE()->user_profile();
if (user_profile->is_signed_in()) {
auto type = user_profile->type() & type_mask;
if (type & (2 | 4)) {
// maybe online profile?
xuid = user_profile->xuid();
result = X_E_SUCCESS;
} else if (type & 1) {
// maybe offline profile?
xuid = user_profile->xuid();
result = X_E_SUCCESS;
}
const auto& user_profile = REX_KERNEL_STATE()->user_profile();
if (user_profile->is_signed_in()) {
auto type = user_profile->type() & type_mask;
if (type & (2 | 4)) {
// maybe online profile?
xuid = user_profile->xuid();
result = X_E_SUCCESS;
} else if (type & 1) {
// maybe offline profile?
xuid = user_profile->xuid();
result = X_E_SUCCESS;
}
}
} else {
@ -76,10 +76,22 @@ i32 XamUserGetXUID_entry(u32 user_index, u32 type_mask, mapped_u64 xuid_ptr) {
u32 XamUserGetSigninState_entry(u32 user_index) {
uint32_t signin_state = 0;
// netcode-revival: the party flow reads signin state for the "live user
// index" (the game's live flag is a user index, not a boolean). We model
// one profile, so answer every slot with the same signed-in state.
if (user_index < 4) {
if (user_index == 0) {
const auto& user_profile = REX_KERNEL_STATE()->user_profile();
signin_state = user_profile->signin_state();
const auto& user_profile = REX_KERNEL_STATE()->user_profile();
signin_state = user_profile->signin_state();
}
if (REXCVAR_GET(xnet_trace)) {
// Rate-limited: the frontend polls this frequently.
static double last_log = 0.0;
const double now =
double(std::chrono::steady_clock::now().time_since_epoch().count()) / 1e9;
if (now - last_log >= 0.5) {
last_log = now;
REXLOG_INFO("xam trace: XamUserGetSigninState(user={}) -> {}", user_index,
signin_state);
}
}
return signin_state;
@ -101,7 +113,9 @@ i32 XamUserGetSigninInfo_entry(u32 user_index, u32 flags, ppc_ptr_t<X_USER_SIGNI
}
std::memset(info, 0, sizeof(X_USER_SIGNIN_INFO));
if (user_index) {
// netcode-revival: answer every slot with the one profile (the game's
// "live user index" is not necessarily 0).
if (user_index >= 4) {
return X_E_NO_SUCH_USER;
}
@ -112,7 +126,15 @@ i32 XamUserGetSigninInfo_entry(u32 user_index, u32 flags, ppc_ptr_t<X_USER_SIGNI
info->xuid = user_profile->xuid();
info->signin_state = user_profile->signin_state();
// netcode-revival: the game's "enter online flow" gate (sub_82703960)
// requires unk08 == 1 (a Live indicator) or it shows "please login with
// Xbox Live". Set it when the profile is Live signed-in.
info->unk08 = user_profile->is_live_signed_in() ? 1 : 0;
rex::string::util_copy_truncating(info->name, user_profile->name(), rex::countof(info->name));
if (REXCVAR_GET(xnet_trace)) {
REXLOG_INFO("xam trace: XamUserGetSigninInfo(user={}, flags={}) -> signin_state={}",
user_index, flags, user_profile->signin_state());
}
return X_E_SUCCESS;
}
@ -121,10 +143,6 @@ u32 XamUserGetName_entry(u32 user_index, mapped_string buffer, u32 buffer_len) {
return X_E_INVALIDARG;
}
if (user_index) {
return X_E_NO_SUCH_USER;
}
const auto& user_profile = REX_KERNEL_STATE()->user_profile();
if (!user_profile->is_signed_in()) {
if (buffer && buffer_len) {
@ -143,10 +161,6 @@ u32 XamUserGetGamerTag_entry(u32 user_index, mapped_wstring buffer, u32 buffer_l
return X_E_INVALIDARG;
}
if (user_index) {
return X_E_NO_SUCH_USER;
}
const auto& user_profile = REX_KERNEL_STATE()->user_profile();
if (!user_profile->is_signed_in()) {
return X_ERROR_NO_SUCH_USER;
@ -430,8 +444,13 @@ u32 XamUserCheckPrivilege_entry(u32 user_index, u32 mask, mapped_u32 out_value)
}
}
// If we deny everything, games should hopefully not try to do stuff.
*out_value = 0;
// netcode-revival: the game gates its party/session flow on
// XamUserCheckPrivilege (e.g. MULTIPLAYER_SESSIONS); denying everything
// (the old default) made "create a party" refuse. Grant requested online
// privileges so the P2P session flow opens.
*out_value = 1;
REXLOG_INFO("xam: XamUserCheckPrivilege(user={}, mask=0x{:08X}) -> granted",
user_index, mask);
return X_ERROR_SUCCESS;
}
@ -484,9 +503,6 @@ u32 XamUserGetMembershipTier_entry(u32 user_index) {
if (user_index >= 4) {
return X_ERROR_INVALID_PARAMETER;
}
if (user_index) {
return X_ERROR_NO_SUCH_USER;
}
if (!REX_KERNEL_STATE()->user_profile()->is_live_signed_in()) {
return X_ERROR_NOT_LOGGED_ON;
}
@ -533,6 +549,8 @@ u32 XamUserAreUsersFriends_entry(u32 user_index, u32 unk1, u32 unk2, mapped_u32
}
u32 XamShowSigninUI_entry(u32 unk, u32 unk_mask) {
REXLOG_INFO("xam trace: XamShowSigninUI(unk={}, mask={}) - game asked for sign-in",
unk, unk_mask);
// Mask values vary. Probably matching user types? Local/remote?
KernelState* kernel_state = REX_KERNEL_STATE();
@ -540,6 +558,10 @@ u32 XamShowSigninUI_entry(u32 unk, u32 unk_mask) {
// profile has signed in; titles observe the UI notification and then continue
// along their signed-out path.
kernel_state->BroadcastNotification(0x00000009, 1);
// netcode-revival: also report the sign-in as COMPLETED (Live) so the
// game's sign-in manager designates the live user; otherwise it waits for
// a real Live session that never comes.
kernel_state->BroadcastNotification(0x80000001, 0x200);
++xam_dialogs_shown_;
const uint32_t close_delay_ms = REXCVAR_GET(xam_signin_ui_auto_close_ms);

View file

@ -14,6 +14,7 @@ set(REXSYSTEM_SOURCES
xnotifylistener.cpp
xenumerator.cpp
xsocket.cpp
xnet_transport.cpp
xsymboliclink.cpp
xobject.cpp
xmodule.cpp

View file

@ -27,8 +27,10 @@ namespace xam {
REXCVAR_DEFINE_BOOL(user_profile_signed_in, true, "Kernel",
"Whether user slot 0 starts with a local profile");
REXCVAR_DEFINE_BOOL(user_live_signed_in, false, "Kernel",
"Whether user slot 0 starts signed in to Xbox Live");
REXCVAR_DEFINE_BOOL(user_live_signed_in, true, "Kernel",
"Whether user slot 0 starts signed in to Xbox Live (the "
"netcode revival needs the game to see LIVE sign-in so its "
"P2P session/invite flow opens)");
REXCVAR_DEFINE_STRING(selected_user_profile, "player", "Kernel/Profile",
"Selected local Xbox profile id");
REXCVAR_DEFINE_STRING(user_profile_name, "Player", "Kernel/Profile",

View file

@ -0,0 +1,555 @@
/**
* netcode-revival: relay transport for the game's own netcode.
* See xnet_transport.h for the design.
*/
#include <rex/system/xnet_transport.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <thread>
#include <rex/platform.h>
#include <rex/logging.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xsocket.h>
#include <rex/system/xam/user_profile.h>
#if REX_PLATFORM_WIN32
#include <WinSock2.h>
#include <WS2tcpip.h>
#else
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
namespace rex {
namespace system {
// Pin the packed wire sizes to the values the relay expects (the game-side
// proto packs them identically). Catches accidental un-packing or member
// edits before they corrupt the wire.
static_assert(sizeof(XNetRelayHeader) == 7, "relay header must be 7 bytes");
static_assert(sizeof(XNetRelayJoin) == 31, "relay join must be 31 bytes");
static_assert(sizeof(XNetRelayGame) == 1410, "relay game payload must be 1410 bytes");
static_assert(sizeof(XNetRelaySessionEntry) == 25, "session entry must be 25 bytes");
static_assert(sizeof(XNetRelaySessionReply) == 801, "session reply must be 801 bytes");
namespace {
// Virtual address range: 10.80.0.0/16 (host order prefix 0x0A500000).
constexpr uint32_t kVirtualPrefix = 0x0A500000u;
int64_t SocketCreate() {
#if REX_PLATFORM_WIN32
return int64_t(::socket(AF_INET, SOCK_DGRAM, 0));
#else
return int64_t(::socket(AF_INET, SOCK_DGRAM, 0));
#endif
}
void SocketClose(int64_t s) {
#if REX_PLATFORM_WIN32
::closesocket(SOCKET(s));
#else
::close(int(s));
#endif
}
// The receive loop must wake regularly so the heartbeat fires even with no
// inbound traffic (otherwise the relay sweeps a silent transport client
// after ~8s) and so StopLocked()'s join completes promptly instead of
// deadlocking SetRelay on a room change.
void SocketSetRecvTimeout(int64_t s, int timeout_ms) {
#if REX_PLATFORM_WIN32
DWORD tv = DWORD(timeout_ms);
::setsockopt(SOCKET(s), SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char*>(&tv), sizeof(tv));
#else
timeval tv = {};
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
::setsockopt(int(s), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
#endif
}
bool SocketSendTo(int64_t s, const sockaddr_in* to, const void* buf, int size) {
#if REX_PLATFORM_WIN32
const int n = ::sendto(SOCKET(s), reinterpret_cast<const char*>(buf), size, 0,
reinterpret_cast<const sockaddr*>(to), sizeof(*to));
#else
const int n = ::sendto(int(s), buf, size, 0,
reinterpret_cast<const sockaddr*>(to), sizeof(*to));
#endif
return n == size;
}
// Resolve "host" to a sockaddr_in (UDP), defaulting the port. Returns true
// on success.
bool Resolve(const std::string& host, uint16_t port, sockaddr_in* out) {
addrinfo hints = {};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
addrinfo* res = nullptr;
const std::string port_str = std::to_string(port);
if (getaddrinfo(host.c_str(), port_str.c_str(), &hints, &res) != 0 ||
res == nullptr) {
if (res != nullptr) {
freeaddrinfo(res);
}
return false;
}
*out = *reinterpret_cast<sockaddr_in*>(res->ai_addr);
out->sin_port = htons(port);
freeaddrinfo(res);
return true;
}
} // namespace
XNetTransport& XNetTransport::Instance() {
static XNetTransport s_instance;
return s_instance;
}
XNetTransport::~XNetTransport() {
StopThread(); // join outside the lock (see StopThread)
std::lock_guard<std::mutex> lock(mu_);
if (sock_ != -1) {
SocketClose(sock_);
sock_ = -1;
}
active_ = false;
joined_ = false;
sessions_.clear();
sockets_.clear();
}
uint32_t XNetTransport::LocalVirtualIp() {
// Mirrors the XNet identity: machine id from the signed-in profile XUID,
// mapped onto 10.80.0.x. Stable across runs on one box.
uint64_t seed = 0;
if (xam::UserProfile* profile = REX_KERNEL_STATE()->user_profile()) {
seed = profile->xuid();
}
if (seed == 0) {
seed = 0x52454B4E45544CULL;
}
const uint8_t machine_id = uint8_t((seed ^ (seed >> 32)) & 0xFEu) + 1;
return kVirtualPrefix | machine_id;
}
bool XNetTransport::IsVirtualIp(uint32_t host_order_ip) {
return (host_order_ip & 0xFFFF0000u) == kVirtualPrefix;
}
bool XNetTransport::Active() const {
std::lock_guard<std::mutex> lock(mu_);
return active_;
}
void XNetTransport::ForceLiveFlag() {
// The Skate 3 frontend gates "Xbox Live features" on an internal static
// byte (the game's own "live user" flag) and refuses WITHOUT calling any
// rexglue API when it is 0. The game sets it from its sign-in manager, but
// with no real Live the flag never becomes non-zero. Force it here.
// Address from the game: sub_8263E788 does lis r11,-32003 -> r11 =
// 0x83030000 (recompiler's -2097348608) then lbz at -30639 =>
// 0x82FC8851. (Fragile: tied to this title's build; revival only.)
uint8_t* flag = REX_KERNEL_MEMORY()->TranslateVirtual<uint8_t*>(0x82FC8851);
// Rate-limited status so we can SEE whether the address maps and its value.
static double last_log = 0.0;
const double now =
double(std::chrono::steady_clock::now().time_since_epoch().count()) / 1e9;
const bool rate = now - last_log >= 2.0;
if (rate) {
last_log = now;
REXLOG_INFO("xnet trace: live flag 0x82FC8851 {} value={}",
flag != nullptr ? "mapped" : "NOT MAPPED",
flag != nullptr ? int(*flag) : -1);
// Dump the party state machine's internal state (static party struct at
// 0x82FD9750): state@336, eaip@344, byte417 (EA flag), byte421 (online),
// byte423 (EA connected). Tells us exactly where the machine is stuck.
const auto rd8 = [](uint32_t a) -> int {
uint8_t* p = REX_KERNEL_MEMORY()->TranslateVirtual<uint8_t*>(a);
return p != nullptr ? int(*p) : -1;
};
const auto rd32 = [](uint32_t a) -> uint32_t {
uint32_t* p = REX_KERNEL_MEMORY()->TranslateVirtual<uint32_t*>(a);
return p != nullptr ? *p : 0xFFFFFFFFu;
};
REXLOG_INFO("xnet trace: party state={} eaip=0x{:08X} b417={} b421={} b423={}",
rd32(0x82FD98A0), rd32(0x82FD98A8), rd8(0x82FD98F1), rd8(0x82FD98F5),
rd8(0x82FD98F7));
// The frontend 'enter online flow' gate (sub_82703960) that leads to
// create-party: latch byte 0x830670C8 must be 0, and the frontend-mode
// byte at obj@*(0x8306707C)+194 must be non-zero.
uint32_t* mode_obj = REX_KERNEL_MEMORY()->TranslateVirtual<uint32_t*>(0x8306707C);
REXLOG_INFO("xnet trace: online latch={} mode_obj={} mode_byte={}", rd8(0x830670C8),
mode_obj != nullptr ? *mode_obj : 0,
(mode_obj != nullptr && *mode_obj != 0) ? rd8(*mode_obj + 194) : -1);
}
// The flag is the LIVE USER INDEX (not a boolean). The profile lives in
// slot 0, so force 0 (any slot answers after the shim fix, but 0 is the
// safest for index-into-live-array readers).
if (flag != nullptr && *flag != 0) {
if (!rate) {
REXLOG_INFO("xnet trace: forcing game live flag 0x82FC8851 = 0");
}
*flag = 0;
}
// The game also caches "live user exists" flags from its sign-in manager
// (set only when it processes a real signin notification, which never
// happens with no Live): 0x8308396C (=14700, 'live user exists') and
// 0x83084690 (=18064, checked by the frontend online-access controllers
// sub_826147C8 / sub_82614F38). Force both so Party Play ungreys.
const auto force_byte = [rate](uint32_t addr, const char* what) {
uint8_t* p = REX_KERNEL_MEMORY()->TranslateVirtual<uint8_t*>(addr);
if (p != nullptr && *p == 0) {
if (!rate) {
REXLOG_INFO("xnet trace: forcing game live cache {} = 1", what);
}
*p = 1;
}
};
force_byte(0x8308396C, "0x8308396C (live user exists)");
force_byte(0x83084690, "0x83084690 (online access)");
// The party state machine (sub_82A1B310) can't leave state 1 unless the
// game's EA-Nation flag (party+417) is 1 - it is set at party-create from
// a config-string comparison that never matches in the recomp. The party
// struct is a static object at 0x82FD9750, so force its gate bytes:
// +417 = EA-Nation enabled (state 1 gate)
// +421 = online flag (set by the EA resolver alongside the online IP)
// +423 = 'EA connected' marker (state 6 target, unlocks advertise/join)
force_byte(0x82FD98F1, "party+417 (EA-Nation flag)");
force_byte(0x82FD98F5, "party+421 (online flag)");
force_byte(0x82FD98F7, "party+423 (EA connected)");
// The frontend 'enter online flow' gate (sub_82703960) blocks the path to
// create-party unless the online latch (0x830670C8) is 0 and the
// frontend-mode byte (obj@*(0x8306707C)+194) is non-zero. Force both.
{
uint8_t* latch = REX_KERNEL_MEMORY()->TranslateVirtual<uint8_t*>(0x830670C8);
if (latch != nullptr && *latch != 0) {
if (!rate) {
REXLOG_INFO("xnet trace: forcing online latch 0x830670C8 = 0");
}
*latch = 0;
}
uint32_t* mode_obj = REX_KERNEL_MEMORY()->TranslateVirtual<uint32_t*>(0x8306707C);
if (mode_obj != nullptr && *mode_obj != 0) {
uint8_t* mode = REX_KERNEL_MEMORY()->TranslateVirtual<uint8_t*>(*mode_obj + 194);
if (mode != nullptr && *mode == 0) {
if (!rate) {
REXLOG_INFO("xnet trace: forcing frontend mode byte = 1");
}
*mode = 1;
}
}
}
// Broadcast the Live signin-changed event a few times at startup so the
// game's sign-in manager runs and designates the profile as the live user
// (it only does this when it processes a real signin notification, which
// never happens with no Live). Param = (signin_state<<8)|user_index.
static uint32_t signin_broadcasts = 0;
if (signin_broadcasts < 6) {
REX_KERNEL_STATE()->BroadcastNotification(0x80000001, 0x200);
++signin_broadcasts;
REXLOG_INFO("xnet trace: broadcast Live signin-changed (0x80000001) #{}/6",
signin_broadcasts);
}
}
XNetTransport::GateStatus XNetTransport::GetGateStatus() const {
GateStatus gs;
uint8_t* flag = REX_KERNEL_MEMORY()->TranslateVirtual<uint8_t*>(0x82FC8851);
gs.live_flag_mapped = flag != nullptr;
gs.live_flag_value = flag != nullptr ? *flag : 0;
const uint32_t state = REX_KERNEL_STATE()->user_profile()->signin_state();
const bool signed_in = REX_KERNEL_STATE()->user_profile()->is_signed_in();
for (int slot = 0; slot < 4; ++slot) {
gs.signin_state[slot] = state;
gs.signin_info_ok[slot] = signed_in;
}
gs.link_active = true; // XNetGetEthernetLinkStatus now reports active
gs.title_online = true; // XNetGetTitleXnAddr now reports 0x60
return gs;
}
void XNetTransport::SetRelay(const char* host, uint16_t port, const char* room) {
{
std::lock_guard<std::mutex> lock(mu_);
const std::string h = host ? host : "";
const std::string r = room ? room : "";
if (h == host_ && port == port_ && r == room_ && active_) {
return; // unchanged and already connected
}
host_ = h;
port_ = port;
room_ = r;
}
StopThread(); // join the old thread OUTSIDE the lock
Reconnect();
}
void XNetTransport::StopThread() {
std::thread old;
{
std::lock_guard<std::mutex> lock(mu_);
running_ = false;
old = std::move(thread_);
thread_ = {};
}
if (old.joinable()) {
old.join();
}
}
void XNetTransport::Reconnect() {
std::lock_guard<std::mutex> lock(mu_);
if (host_.empty() || port_ == 0) {
return;
}
if (sock_ != -1) {
SocketClose(sock_);
sock_ = -1;
}
active_ = false;
joined_ = false;
sessions_.clear();
relay_resolved_ = false;
sockaddr_in relay_addr = {};
if (!Resolve(host_, port_, &relay_addr)) {
REXLOG_WARN("xnet transport: cannot resolve relay '{}:{}'", host_, port_);
return;
}
std::memcpy(relay_addr_, &relay_addr, sizeof(relay_addr));
relay_resolved_ = true;
sock_ = SocketCreate();
if (sock_ == -1) {
REXLOG_WARN("xnet transport: cannot open relay socket");
return;
}
// Wake the receive loop every 200ms (heartbeat + prompt shutdown).
SocketSetRecvTimeout(sock_, 200);
REXLOG_INFO("xnet transport: connecting to relay '{}:{}' room '{}' (vip {}.{}.{}.{})", host_,
port_, room_.c_str(), (LocalVirtualIp() >> 24) & 0xFF, (LocalVirtualIp() >> 16) & 0xFF,
(LocalVirtualIp() >> 8) & 0xFF, LocalVirtualIp() & 0xFF);
// Join the room so the relay routes our game traffic among its members.
XNetRelayJoin jp = {};
jp.version = kVersion;
jp.virtual_ip = LocalVirtualIp();
const uint32_t room_len =
std::min<uint32_t>(room_.size(), sizeof(jp.room));
std::memcpy(jp.room, room_.data(), room_len);
jp.room_len = uint8_t(room_len);
const char name[] = "skate3";
const uint32_t name_len = std::min<uint32_t>(sizeof(name) - 1, sizeof(jp.name));
std::memcpy(jp.name, name, name_len);
jp.name_len = uint8_t(name_len);
uint8_t pkt[7 + sizeof(jp)] = {};
XNetRelayHeader* h = reinterpret_cast<XNetRelayHeader*>(pkt);
h->magic = kMagic;
h->type = kJoin;
h->payload_size = uint16_t(sizeof(jp));
std::memcpy(pkt + 7, &jp, sizeof(jp));
SocketSendTo(sock_, &relay_addr, pkt, sizeof(pkt));
// Keep the JOIN around for the heartbeat (the relay drops silent clients
// after ~8s).
std::memcpy(join_pkt_, pkt, sizeof(pkt));
join_pkt_size_ = uint16_t(sizeof(pkt));
last_join_ = 0.0;
running_ = true;
thread_ = std::thread(&XNetTransport::Run, this);
}
int XNetTransport::SendGame(const void* buf, uint32_t len, uint32_t dst_virtual_ip_host_order) {
if (len == 0 || len > sizeof(XNetRelayGame::data)) {
return -1;
}
if (!IsVirtualIp(dst_virtual_ip_host_order) && dst_virtual_ip_host_order != 0) {
return -1;
}
std::lock_guard<std::mutex> lock(mu_);
if (!active_ || sock_ == -1 || !relay_resolved_) {
return -1;
}
const sockaddr_in* relay = reinterpret_cast<const sockaddr_in*>(relay_addr_);
XNetRelayGame gp = {};
gp.dst_virtual_ip = dst_virtual_ip_host_order;
gp.data_len = uint16_t(len);
std::memcpy(gp.data, buf, len);
uint8_t pkt[7 + sizeof(gp)] = {};
XNetRelayHeader* h = reinterpret_cast<XNetRelayHeader*>(pkt);
h->magic = kMagic;
h->type = kGame;
h->payload_size = uint16_t(sizeof(gp));
std::memcpy(pkt + 7, &gp, sizeof(gp));
if (!SocketSendTo(sock_, relay, pkt, sizeof(pkt))) {
return -1;
}
return int(len);
}
void XNetTransport::QuerySessions() {
std::lock_guard<std::mutex> lock(mu_);
if (!active_ || sock_ == -1 || !relay_resolved_) {
return;
}
const sockaddr_in* relay = reinterpret_cast<const sockaddr_in*>(relay_addr_);
uint8_t pkt[7] = {};
XNetRelayHeader* h = reinterpret_cast<XNetRelayHeader*>(pkt);
h->magic = kMagic;
h->type = kSessionQuery;
h->payload_size = 0;
SocketSendTo(sock_, relay, pkt, sizeof(pkt));
}
void XNetTransport::WaitForReply(int timeout_ms) {
const uint64_t target = [this] {
std::lock_guard<std::mutex> lock(mu_);
return reply_seq_;
}();
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(timeout_ms);
while (std::chrono::steady_clock::now() < deadline) {
std::lock_guard<std::mutex> lock(mu_);
if (reply_seq_ > target) {
return;
}
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
std::vector<XNetSession> XNetTransport::Sessions() const {
std::lock_guard<std::mutex> lock(mu_);
return sessions_;
}
void XNetTransport::RegisterSocket(XSocket* s) {
if (s == nullptr) {
return;
}
std::lock_guard<std::mutex> lock(mu_);
sockets_.insert(s);
}
void XNetTransport::UnregisterSocket(XSocket* s) {
std::lock_guard<std::mutex> lock(mu_);
sockets_.erase(s);
}
void XNetTransport::Run() {
while (true) {
{
std::lock_guard<std::mutex> lock(mu_);
if (!running_) {
return;
}
// Heartbeat: re-JOIN every ~4s so the relay never times us out.
const double now =
double(std::chrono::steady_clock::now().time_since_epoch().count()) / 1e9;
if (join_pkt_size_ != 0 && now - last_join_ >= 4.0) {
SocketSendTo(sock_, reinterpret_cast<const sockaddr_in*>(relay_addr_), join_pkt_,
join_pkt_size_);
last_join_ = now;
}
}
uint8_t buf[7 + sizeof(XNetRelayGame)];
sockaddr_in from = {};
socklen_t from_len = sizeof(from);
#if REX_PLATFORM_WIN32
int n = ::recvfrom(SOCKET(sock_), reinterpret_cast<char*>(buf), sizeof(buf), 0,
reinterpret_cast<sockaddr*>(&from), &from_len);
#else
int n = ::recvfrom(int(sock_), buf, sizeof(buf), 0,
reinterpret_cast<sockaddr*>(&from), &from_len);
#endif
if (n < 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
continue;
}
if (n < 7) {
continue;
}
HandlePacket(buf, n);
}
}
void XNetTransport::HandlePacket(const uint8_t* data, int size) {
const XNetRelayHeader* h = reinterpret_cast<const XNetRelayHeader*>(data);
if (h->magic != kMagic || h->payload_size > 3200 ||
size < int(sizeof(*h) + h->payload_size)) {
return;
}
const uint8_t* payload = data + sizeof(*h);
switch (h->type) {
case kHello: {
std::lock_guard<std::mutex> lock(mu_);
// session_id != 0 means accepted (0 = refused).
active_ = payload[0] != 0;
joined_ = true;
REXLOG_INFO("xnet transport: relay {} (vip {}.{}.{}.{})", active_ ? "joined" : "refused",
(LocalVirtualIp() >> 24) & 0xFF, (LocalVirtualIp() >> 16) & 0xFF,
(LocalVirtualIp() >> 8) & 0xFF, LocalVirtualIp() & 0xFF);
break;
}
case kGame: {
if (h->payload_size < sizeof(XNetRelayGame)) {
return;
}
XNetRelayGame gp = {};
std::memcpy(&gp, payload, sizeof(gp));
if (gp.data_len > sizeof(gp.data)) {
return;
}
std::vector<XSocket*> targets;
{
std::lock_guard<std::mutex> lock(mu_);
for (XSocket* s : sockets_) {
targets.push_back(s);
}
}
for (XSocket* s : targets) {
// src_virtual_ip is host order; QueuePacket's src is host order too.
s->QueuePacket(gp.src_virtual_ip, 0, gp.data, gp.data_len);
}
break;
}
case kSessionReply: {
if (h->payload_size < sizeof(XNetRelaySessionReply)) {
return;
}
XNetRelaySessionReply sr = {};
std::memcpy(&sr, payload, sizeof(sr));
std::vector<XNetSession> fresh;
const uint8_t count = std::min<uint8_t>(sr.count, 32);
for (uint8_t i = 0; i < count; ++i) {
XNetSession s;
s.session_id = sr.list[i].session_id;
s.virtual_ip = sr.list[i].virtual_ip;
s.name_len = std::min<uint8_t>(sr.list[i].name_len, sizeof(s.name));
std::memcpy(s.name, sr.list[i].name, s.name_len);
fresh.push_back(s);
}
std::lock_guard<std::mutex> lock(mu_);
sessions_ = std::move(fresh);
++reply_seq_;
REXLOG_INFO("xnet transport: session reply with {} session(s) in room '{}'", count,
room_.c_str());
break;
}
default:
break; // kTypeRooms / Pose / Ping / Pong: not this transport's business.
}
}
} // namespace system
} // namespace rex

View file

@ -16,6 +16,7 @@
#include <rex/system/kernel_state.h>
#include <rex/system/xsocket.h>
// #include <rex/system/xnet.h>
#include <rex/system/xnet_transport.h>
#include <rex/net/socket.h>
@ -65,10 +66,15 @@ X_STATUS XSocket::Initialize(AddressFamily af, Type type, Protocol proto) {
return X_STATUS_UNSUCCESSFUL;
}
// netcode-revival: relayed game traffic is injected into registered
// sockets; the game's RakNet netcode socket is one of them.
XNetTransport::Instance().RegisterSocket(this);
return X_STATUS_SUCCESS;
}
X_STATUS XSocket::Close() {
XNetTransport::Instance().UnregisterSocket(this);
int ret = rex::net::socket_close(native_handle_);
if (ret != 0) {
return X_STATUS_UNSUCCESSFUL;
@ -171,28 +177,29 @@ int XSocket::Recv(uint8_t* buf, uint32_t buf_len, uint32_t flags) {
int XSocket::RecvFrom(uint8_t* buf, uint32_t buf_len, uint32_t flags, N_XSOCKADDR_IN* from,
uint32_t* from_len) {
// Pop from secure packets first
// TODO(DrChat): Enable when I commit XNet
/*
// netcode-revival: relayed game datagrams (from the XNetTransport receive
// thread) are queued here and surface ahead of any native traffic.
{
std::lock_guard<std::mutex> lock(incoming_packet_mutex_);
if (incoming_packets_.size()) {
packet* pkt = (packet*)incoming_packets_.front();
int data_len = pkt->data_len;
std::memcpy(buf, pkt->data, std::min((uint32_t)pkt->data_len, buf_len));
from->sin_family = 2;
from->sin_addr = pkt->src_ip;
from->sin_port = pkt->src_port;
if (!incoming_packets_.empty()) {
packet* pkt = reinterpret_cast<packet*>(incoming_packets_.front());
incoming_packets_.pop();
uint8_t* pkt_ui8 = (uint8_t*)pkt;
delete[] pkt_ui8;
return data_len;
const uint32_t data_len = pkt->data_len;
const uint32_t n = std::min<uint32_t>(data_len, buf_len);
std::memcpy(buf, pkt->data, n);
if (from) {
from->sin_family = 2;
from->sin_addr = pkt->src_ip;
from->sin_port = pkt->src_port;
std::memset(from->x_sin_zero, 0, sizeof(from->x_sin_zero));
}
if (from_len) {
*from_len = sizeof(sockaddr_in);
}
delete[] reinterpret_cast<uint8_t*>(pkt);
return int(n);
}
}
*/
sockaddr_in nfrom;
socklen_t nfromlen = sizeof(sockaddr_in);
@ -218,16 +225,12 @@ int XSocket::Send(const uint8_t* buf, uint32_t buf_len, uint32_t flags) {
int XSocket::SendTo(uint8_t* buf, uint32_t buf_len, uint32_t flags, N_XSOCKADDR_IN* to,
uint32_t to_len) {
// Send 2 copies of the packet: One to XNet (for network security) and an
// unencrypted copy for other Xenia hosts.
// TODO(DrChat): Enable when I commit XNet.
/*
auto xam = kernel_state()->GetKernelModule<xam::XamModule>("xam.xex");
auto xnet = xam->xnet();
if (xnet) {
xnet->SendPacket(this, to, buf, buf_len);
// netcode-revival: a datagram addressed to a virtual 10.80.0.x peer is
// routed through the relay (the game's RakNet netcode), not sent raw to
// an unreachable address.
if (to && XNetTransport::IsVirtualIp(uint32_t(to->sin_addr))) {
return XNetTransport::Instance().SendGame(buf, buf_len, uint32_t(to->sin_addr));
}
*/
sockaddr_in nto;
if (to) {

View file

@ -387,7 +387,8 @@ void ImGuiDrawer::SetupFonts() {
// DirectWrite white-on-black edge
// profiles (identity was measurably thinner/dimmer). The *_on_light_
// variants below keep gamma 1.0 - dark-on-light needs no correction.
config.RasterizerGamma = 0.62f;
// (RasterizerGamma was removed from imgui's ImFontConfig; the modern
// rasterizer handles coverage gamma internally, so the knob is gone.)
config.FontLoaderFlags = ImGuiFreeTypeBuilderFlags_NoHinting;
ui_font_ = io.Fonts->AddFontFromMemoryCompressedBase85TTF(
GetInterRegularCompressedBase85(), 18.0f, &config);
@ -408,7 +409,7 @@ void ImGuiDrawer::SetupFonts() {
config_on_light.RasterizerMultiply = 1.0f;
// Dark-on-light matches the browser with NO coverage curve (harness fit:
// identity beat every contrast/gamma variant once bake==draw size).
config_on_light.RasterizerGamma = 1.0f;
// config_on_light.RasterizerGamma = 1.0f; (removed from imgui)
ui_font_on_light_ = io.Fonts->AddFontFromMemoryCompressedBase85TTF(
GetInterRegularCompressedBase85(), 18.0f, &config_on_light);
ui_font_semibold_on_light_ = io.Fonts->AddFontFromMemoryCompressedBase85TTF(

2
thirdparty/imgui vendored

@ -1 +1 @@
Subproject commit cdda62349d6068e906c2e0ee340d6ec31eedbb9c
Subproject commit 0af8f475d1cf8b98d603702059c0c73deeb81ac2