/** * 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 #include #include #include #include #include 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 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 sessions_; std::unordered_set sockets_; }; } // namespace system } // namespace rex