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.
This commit is contained in:
MiTHRAL 2026-08-28 01:54:51 -04:00
parent 395e1ce67d
commit 767bd6e4bc

View file

@ -125,6 +125,57 @@ 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);
}
id.virtual_ip = (10u << 24) | (80u << 16) | (0u << 8) | id.machine_id;
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) {
@ -433,26 +484,15 @@ 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);
// netcode-revival: return this machine's stable virtual identity (10.80.0.x
// + a unique abEnet) so RakNet gets a non-zero, per-machine GUID seed and
// the game's address resolution has a consistent LAN-class address.
const XnetIdentity& id = XnetLocalIdentity();
addr_ptr->ina.s_addr = htonl(id.virtual_ip);
addr_ptr->inaOnline.s_addr = 0;
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;
}
@ -464,33 +504,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
@ -556,7 +620,130 @@ 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;
return 0;
}
// netcode-revival: session search. Phase 0 reports zero discovered sessions
// (the relay registry lands in Phase 1) but returns success and signals the
// completion event so the game's search finishes instead of hanging.
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)dwTimeoutMs;
(void)dwFlags;
if (apxnqosResults) {
auto qos_guest = REX_KERNEL_MEMORY()->SystemHeapAlloc(sizeof(XNQOS));
auto qos = REX_KERNEL_MEMORY()->TranslateVirtual<XNQOS*>(qos_guest);
qos->count = qos->count_pending = 0;
*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;
}
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) {
(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) {
(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) {
(void)xn_addr;
if (status_ptr) {
const uint32_t connected = 2; // XNET_CONNECT_STATUS_CONNECTED
std::memcpy(status_ptr, &connected, sizeof(connected));
}
return 0;
}
// INT XNetServerToInAddr(const XNADDR* pxn, IN_ADDR* pina, DWORD dwFlags)
// netcode-revival: our server addresses also carry the virtual IP in ina.
u32 NetDll_XNetServerToInAddr_entry(u32 caller, ppc_ptr_t<XNADDR> pxn, mapped_void pina,
u32 dwFlags) {
(void)dwFlags;
if (!pxn || !pina) {
return X_ERROR_INVALID_PARAMETER;
}
uint32_t ip = pxn->ina.s_addr;
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 +1214,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 +1283,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)