/** * 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; // 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]; }; // 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(); // --- 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(); void StopLocked(); 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; 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