netcode-revival: relay game-transport + session discovery (Phase 1a)
The game's own RakNet UDP now has a path through the relay (v5 protocol): - kTypeGame: opaque game datagrams forwarded to the destination member (dst_virtual_ip, or broadcast to all room members when 0); the relay re-stamps the sender's virtual IP so the recipient can reply. This gives the netcode a P2P route with the server as a pure relay/stabilizer. - kTypeSessionQuery / kTypeSessionReply: a client asks 'who's in my room?' and gets the other members' session ids + virtual IPs + names - exactly what XNetQosLookup must hand the game's session search. - JoinPayload now carries the client's virtual IP (10.80.0.x identity) so the relay can route and answer discovery. Tested end-to-end (unicast + broadcast + room isolation + discovery).
This commit is contained in:
parent
bd855c5d88
commit
0ab155341c
3 changed files with 126 additions and 2 deletions
|
|
@ -23,7 +23,7 @@
|
||||||
namespace skate3::native_ghost {
|
namespace skate3::native_ghost {
|
||||||
|
|
||||||
constexpr uint32_t kGhostMagic = 0x54484753u; // "GHST" (LE)
|
constexpr uint32_t kGhostMagic = 0x54484753u; // "GHST" (LE)
|
||||||
constexpr uint8_t kGhostVersion = 4;
|
constexpr uint8_t kGhostVersion = 5;
|
||||||
constexpr uint16_t kGhostDefaultPort = 40001;
|
constexpr uint16_t kGhostDefaultPort = 40001;
|
||||||
constexpr uint32_t kGhostMaxRoomLen = 8;
|
constexpr uint32_t kGhostMaxRoomLen = 8;
|
||||||
constexpr uint32_t kGhostMaxRoster = 32;
|
constexpr uint32_t kGhostMaxRoster = 32;
|
||||||
|
|
@ -53,6 +53,10 @@ enum PacketType : uint8_t {
|
||||||
kTypePoseChunk = 7, // client -> relay -> other members: { PoseChunkPayload }
|
kTypePoseChunk = 7, // client -> relay -> other members: { PoseChunkPayload }
|
||||||
kTypeList = 8, // client -> relay: request the room list (empty payload)
|
kTypeList = 8, // client -> relay: request the room list (empty payload)
|
||||||
kTypeRooms = 9, // relay -> client: { RoomsPayload } (the lobby browser)
|
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
|
// 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
|
// 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
|
// room so LAN hosts can answer only matching rooms); JOIN additionally
|
||||||
// carries the client's display name, which the relay echoes to room members
|
// 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 {
|
struct alignas(1) JoinPayload {
|
||||||
uint8_t version; // protocol version (kGhostVersion); mismatches are refused
|
uint8_t version; // protocol version (kGhostVersion); mismatches are refused
|
||||||
uint8_t room_len;
|
uint8_t room_len;
|
||||||
char room[kGhostMaxRoomLen];
|
char room[kGhostMaxRoomLen];
|
||||||
uint8_t name_len;
|
uint8_t name_len;
|
||||||
char name[kGhostMaxNameLen];
|
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.
|
// One room member as reported in a HELLO roster: session id + display name.
|
||||||
|
|
@ -114,6 +122,34 @@ struct alignas(1) RoomsPayload {
|
||||||
RoomEntry rooms[kGhostMaxRooms];
|
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,
|
// 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
|
// 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
|
// k = bone k exactly as the scene renderer's bone ring expects). Flags bit 0
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,11 @@ void GhostRelay::HandlePacket(const GhostAddr& from, const PacketBuf& p,
|
||||||
std::memcpy(me->name, jp.name, jp.name_len);
|
std::memcpy(me->name, jp.name, jp.name_len);
|
||||||
me->name_len = 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
|
// Empty room: assign one (uppercase alnum, 4 chars, seeded by the
|
||||||
// session id and the wall of the receiving thread).
|
// session id and the wall of the receiving thread).
|
||||||
if (me->room_len == 0) {
|
if (me->room_len == 0) {
|
||||||
|
|
@ -326,6 +331,58 @@ void GhostRelay::HandlePacket(const GhostAddr& from, const PacketBuf& p,
|
||||||
break;
|
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<GamePayload*>(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: {
|
case kTypeLeave: {
|
||||||
const auto it = std::remove_if(clients_.begin(), clients_.end(),
|
const auto it = std::remove_if(clients_.begin(), clients_.end(),
|
||||||
[&from](const Client& c) {
|
[&from](const Client& c) {
|
||||||
|
|
@ -399,6 +456,34 @@ void GhostRelay::BuildRoomList(const GhostAddr& to) {
|
||||||
GhostSockSendTo(sock_, to, &out, PacketSize(out));
|
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<uint16_t>(sizeof(sr)));
|
||||||
|
GhostSockSendTo(sock_, to, &out, PacketSize(out));
|
||||||
|
}
|
||||||
|
|
||||||
void GhostRelay::Sweep(double now) {
|
void GhostRelay::Sweep(double now) {
|
||||||
std::lock_guard<std::mutex> lock(mu_);
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
if (now <= 0.0) {
|
if (now <= 0.0) {
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,11 @@ class GhostRelay {
|
||||||
void SetVerbose(bool verbose) { verbose_ = verbose; }
|
void SetVerbose(bool verbose) { verbose_ = verbose; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct Client;
|
||||||
void Run();
|
void Run();
|
||||||
void HandlePacket(const GhostAddr& from, const PacketBuf& p, double now);
|
void HandlePacket(const GhostAddr& from, const PacketBuf& p, double now);
|
||||||
void BuildRoomList(const GhostAddr& to);
|
void BuildRoomList(const GhostAddr& to);
|
||||||
|
void BuildSessionReply(const GhostAddr& to, const Client& me);
|
||||||
void Sweep(double now);
|
void Sweep(double now);
|
||||||
|
|
||||||
GhostSock sock_ = GHOST_INVALID_SOCKET;
|
GhostSock sock_ = GHOST_INVALID_SOCKET;
|
||||||
|
|
@ -68,6 +70,7 @@ class GhostRelay {
|
||||||
struct Client {
|
struct Client {
|
||||||
GhostAddr addr;
|
GhostAddr addr;
|
||||||
uint32_t session_id = 0;
|
uint32_t session_id = 0;
|
||||||
|
uint32_t virtual_ip = 0; // 10.80.0.x identity from the JOIN (0 = unknown)
|
||||||
char room[kGhostMaxRoomLen] = {};
|
char room[kGhostMaxRoomLen] = {};
|
||||||
uint8_t room_len = 0;
|
uint8_t room_len = 0;
|
||||||
char name[kGhostMaxNameLen] = {};
|
char name[kGhostMaxNameLen] = {};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue