diff --git a/src/native/skate3_native_ghost_proto.h b/src/native/skate3_native_ghost_proto.h index b9f61b8..dd09887 100644 --- a/src/native/skate3_native_ghost_proto.h +++ b/src/native/skate3_native_ghost_proto.h @@ -23,7 +23,7 @@ namespace skate3::native_ghost { constexpr uint32_t kGhostMagic = 0x54484753u; // "GHST" (LE) -constexpr uint8_t kGhostVersion = 4; +constexpr uint8_t kGhostVersion = 5; constexpr uint16_t kGhostDefaultPort = 40001; constexpr uint32_t kGhostMaxRoomLen = 8; constexpr uint32_t kGhostMaxRoster = 32; @@ -53,6 +53,10 @@ enum PacketType : uint8_t { kTypePoseChunk = 7, // client -> relay -> other members: { PoseChunkPayload } kTypeList = 8, // client -> relay: request the room list (empty payload) kTypeRooms = 9, // relay -> client: { RoomsPayload } (the lobby browser) + // netcode-revival (v5): the game's own RakNet UDP rides the same relay. + kTypeGame = 10, // client -> relay -> member: { GamePayload } (opaque) + kTypeSessionQuery = 11, // client -> relay: request sessions in my room + kTypeSessionReply = 12, // relay -> client: { SessionReplyPayload } }; // True byte-packing: the struct layouts below must match the wire format @@ -72,13 +76,17 @@ struct PacketHeader { // JOIN and PING share the room payload (PING may carry the probing client's // room so LAN hosts can answer only matching rooms); JOIN additionally // carries the client's display name, which the relay echoes to room members -// in the HELLO roster so nameplates can label every ghost. +// in the HELLO roster so nameplates can label every ghost. v5 adds the +// client's virtual IP (the 10.80.0.x identity XNet reports for this machine) +// so the relay can route the game's own UDP (kTypeGame) and answer session +// queries (kTypeSessionReply) with per-member addresses. struct alignas(1) JoinPayload { uint8_t version; // protocol version (kGhostVersion); mismatches are refused uint8_t room_len; char room[kGhostMaxRoomLen]; uint8_t name_len; char name[kGhostMaxNameLen]; + uint32_t virtual_ip; // host-order virtual IP (10.80.0.x); 0 = unknown }; // One room member as reported in a HELLO roster: session id + display name. @@ -114,6 +122,34 @@ struct alignas(1) RoomsPayload { RoomEntry rooms[kGhostMaxRooms]; }; +// ---- netcode-revival: the game's own UDP through the relay ------------------ +// Opaque game datagrams (the game's RakNet netcode) are wrapped by rexglue's +// XSocket relay-transport and forwarded by the relay to the destination +// member. dst_virtual_ip selects the recipient (0 = broadcast to every other +// member of the room); the relay re-stamps src_virtual_ip with the sender's +// identity before forwarding so the recipient can reply. data[] is capped so +// a full ~1400-byte UDP payload fits an MTU-safe datagram. +constexpr uint16_t kGhostGameData = 1400; +struct alignas(1) GamePayload { + uint32_t dst_virtual_ip; // recipient (10.80.0.x) or 0 = all room members + uint32_t src_virtual_ip; // filled by the relay on forward + uint16_t data_len; // bytes valid in data[] + uint8_t data[kGhostGameData]; +}; + +// One discovered session (a member of the room) as reported to XNetQosLookup. +struct alignas(1) SessionEntry { + uint32_t session_id; // the member's relay session id + uint32_t virtual_ip; // the member's 10.80.0.x address (host order) + uint8_t name_len; + char name[kGhostMaxNameLen]; +}; + +struct alignas(1) SessionReplyPayload { + uint8_t count; // entries in list[] (<= kGhostMaxRoster) + SessionEntry list[kGhostMaxRoster]; +}; + // Full-pose blob (the character-model path): the sender's skater skeleton, // 3 float4 rows per bone (column-vector affine [R | t], model -> world, row // k = bone k exactly as the scene renderer's bone ring expects). Flags bit 0 diff --git a/src/native/skate3_native_ghost_relay.cpp b/src/native/skate3_native_ghost_relay.cpp index a0356d5..c124b97 100644 --- a/src/native/skate3_native_ghost_relay.cpp +++ b/src/native/skate3_native_ghost_relay.cpp @@ -228,6 +228,11 @@ void GhostRelay::HandlePacket(const GhostAddr& from, const PacketBuf& p, std::memcpy(me->name, jp.name, jp.name_len); me->name_len = jp.name_len; } + // v5: the client's virtual IP identity (used to route kTypeGame and to + // answer kTypeSessionQuery). PING probes may omit it. + if (jp.virtual_ip != 0) { + me->virtual_ip = jp.virtual_ip; + } // Empty room: assign one (uppercase alnum, 4 chars, seeded by the // session id and the wall of the receiving thread). if (me->room_len == 0) { @@ -326,6 +331,58 @@ void GhostRelay::HandlePacket(const GhostAddr& from, const PacketBuf& p, break; } + case kTypeGame: { + // netcode-revival: the game's own RakNet UDP, opaque to us. Forward + // to the destination member (dst_virtual_ip, or every other member + // when 0), re-stamping the sender's virtual IP so the recipient can + // reply. Unknown addresses are dropped (a member with a bogus dst is + // gone or never joined). + if (me->refused) { + return; + } + if (p.h.payload_size != sizeof(GamePayload) || me->virtual_ip == 0) { + return; + } + GamePayload gp = {}; + std::memcpy(&gp, p.payload, sizeof(gp)); + if (gp.data_len > kGhostGameData) { + return; + } + me->last_seen = now; + PacketBuf out = p; + out.h.magic = kGhostMagic; + out.h.type = kTypeGame; + GamePayload* fwd = reinterpret_cast(out.payload); + fwd->src_virtual_ip = me->virtual_ip; + const uint16_t size = PacketSize(out); + for (Client& c : clients_) { + if (&c == me || c.refused) { + continue; + } + if (c.room_len != me->room_len || + !RoomEquals(c.room, c.room_len, me->room, me->room_len)) { + continue; + } + if (gp.dst_virtual_ip != 0 && c.virtual_ip != gp.dst_virtual_ip) { + continue; + } + GhostSockSendTo(sock_, c.addr, &out, size); + } + break; + } + + case kTypeSessionQuery: { + // netcode-revival: a client asks "who's in my room?" so XNetQosLookup + // can discover sessions. Report every OTHER member with a known + // virtual IP (their session id + address + name). + if (me->refused) { + return; + } + me->last_seen = now; + BuildSessionReply(from, *me); + break; + } + case kTypeLeave: { const auto it = std::remove_if(clients_.begin(), clients_.end(), [&from](const Client& c) { @@ -399,6 +456,34 @@ void GhostRelay::BuildRoomList(const GhostAddr& to) { GhostSockSendTo(sock_, to, &out, PacketSize(out)); } +void GhostRelay::BuildSessionReply(const GhostAddr& to, const Client& me) { + // mu_ is held by the caller. Report the OTHER members of the requester's + // room that have a known virtual IP (their relay session id + 10.80.0.x + // address + display name) - exactly the discovery data XNetQosLookup needs + // to hand the game. + SessionReplyPayload sr = {}; + for (const Client& c : clients_) { + if (&c == &me || c.refused || c.virtual_ip == 0) { + continue; + } + if (c.room_len != me.room_len || !RoomEquals(c.room, c.room_len, me.room, me.room_len)) { + continue; + } + if (sr.count >= kGhostMaxRoster) { + break; + } + SessionEntry& e = sr.list[sr.count++]; + e.session_id = c.session_id; + e.virtual_ip = c.virtual_ip; + e.name_len = c.name_len; + std::memcpy(e.name, c.name, c.name_len); + } + + PacketBuf out; + BuildPacket(out, kTypeSessionReply, &sr, static_cast(sizeof(sr))); + GhostSockSendTo(sock_, to, &out, PacketSize(out)); +} + void GhostRelay::Sweep(double now) { std::lock_guard lock(mu_); if (now <= 0.0) { diff --git a/src/native/skate3_native_ghost_relay.h b/src/native/skate3_native_ghost_relay.h index ca0aef6..82d2a72 100644 --- a/src/native/skate3_native_ghost_relay.h +++ b/src/native/skate3_native_ghost_relay.h @@ -52,9 +52,11 @@ class GhostRelay { void SetVerbose(bool verbose) { verbose_ = verbose; } private: + struct Client; void Run(); void HandlePacket(const GhostAddr& from, const PacketBuf& p, double now); void BuildRoomList(const GhostAddr& to); + void BuildSessionReply(const GhostAddr& to, const Client& me); void Sweep(double now); GhostSock sock_ = GHOST_INVALID_SOCKET; @@ -68,6 +70,7 @@ class GhostRelay { struct Client { GhostAddr addr; uint32_t session_id = 0; + uint32_t virtual_ip = 0; // 10.80.0.x identity from the JOIN (0 = unknown) char room[kGhostMaxRoomLen] = {}; uint8_t room_len = 0; char name[kGhostMaxNameLen] = {};