Compare commits
No commits in common. "netcode-revival" and "main" have entirely different histories.
netcode-re
...
main
8 changed files with 21 additions and 331 deletions
|
|
@ -1,82 +0,0 @@
|
||||||
# Netcode revival (branch `netcode-revival`)
|
|
||||||
|
|
||||||
Goal: revive Skate 3's **original** Xbox 360 multiplayer — the game's own
|
|
||||||
RakNet-based peer-to-peer netcode — instead of only the ghost co-presence
|
|
||||||
layer. `main` keeps the working ghost multiplayer + releases; this branch
|
|
||||||
carries the netcode work and stays clearly separate.
|
|
||||||
|
|
||||||
The transport is **RakNet over XNet/XSocket** (confirmed: rexglue's XNet
|
|
||||||
comments reference RakNet, and the game's `socket → setsockopt → bind →
|
|
||||||
sendto` sequence matches `RakPeer::Startup`). The game speaks its own
|
|
||||||
protocol; we only need the **platform netcode layer to work** (implement
|
|
||||||
rexglue's stubbed XNet session APIs + route the game's UDP through the
|
|
||||||
relay). We do not decode the game protocol.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
- **Virtual addressing**: each machine gets a stable `XNADDR` (virtual IP
|
|
||||||
`10.80.0.x` + unique 6-byte `abEnet` → RakNet GUID). `XNetXnAddrToInAddr` /
|
|
||||||
`XNetServerToInAddr` resolve those into the socket layer's address space.
|
|
||||||
- **Relay = P2P stabilizer**: session UDP is forwarded outbound through the
|
|
||||||
relay (tagged `game` vs `ghost`) to the destination member's real socket;
|
|
||||||
on a LAN the transport can go direct. The server stabilizes NAT/remote
|
|
||||||
play but is not required on a LAN.
|
|
||||||
- **Session registry in the relay**: `XNetQosListen` advertises a session;
|
|
||||||
`XNetQosLookup` returns discovered sessions; the lobby/room-code UI stays
|
|
||||||
as the manual join path.
|
|
||||||
- **Ghost layer is kept** as the guaranteed fallback (co-presence that works
|
|
||||||
for everyone even if the netcode path fails).
|
|
||||||
|
|
||||||
## Phase 0 — XNet session/address layer (done, rexglue `767bd6e`)
|
|
||||||
Implemented in `rexglue-sdk/src/kernel/xam/xam_net.cpp`:
|
|
||||||
- `XNetGetTitleXnAddr`: stable virtual identity (was loopback + `0xCC` MAC).
|
|
||||||
- `XNetXnAddrToInAddr` / `XNetInAddrToXnAddr` / `XNetServerToInAddr` /
|
|
||||||
`XNetXnAddrToMachineId` / `XNetInAddrToString`: resolve virtual addresses
|
|
||||||
(were failure / `666.666.666.666`).
|
|
||||||
- `XNetQosListen`: accept advertisement (was `X_ERROR_FUNCTION_FAILED`).
|
|
||||||
- `XNetQosLookup`: success + empty `XNQOS` + completion signal (Phase 1
|
|
||||||
fills it from the relay registry).
|
|
||||||
- `XNetCreateKey` / `XNetRegisterKey` / `XNetUnregisterKey`: session keys.
|
|
||||||
- `XNetConnect` / `XNetGetConnectStatus`: report established.
|
|
||||||
- `getsockopt`: real basic reads off the native socket.
|
|
||||||
- `XNetSetSystemLinkPort`: accept.
|
|
||||||
|
|
||||||
## Phase 1 — relay game-transport + session registry (done, built)
|
|
||||||
- v5 relay: `kTypeGame` (opaque game UDP, unicast or room-broadcast) +
|
|
||||||
`kTypeSessionQuery`/`Reply` (discovery). JoinPayload carries the virtual IP.
|
|
||||||
- rexglue `XNetTransport`: joins the same relay + room as the ghost client,
|
|
||||||
routes `sendto` to `10.80.0.x` peers through the relay, injects inbound
|
|
||||||
game datagrams into the game's sockets, and serves session discovery to
|
|
||||||
`XNetQosLookup`.
|
|
||||||
- Wire structs byte-verified against the game's proto (sizes 7/31/1410/25/801)
|
|
||||||
with `static_assert`s on both sides.
|
|
||||||
|
|
||||||
## Test recipe (two instances on one machine, local relay)
|
|
||||||
1. Build: `cmake --build out/build/linux-relwithdebinfo --target ghost_relay skate3 -j$(nproc)`
|
|
||||||
2. Start the local relay: `./out/build/linux-relwithdebinfo/ghost_relay --port 40001`
|
|
||||||
3. Launch instance A: `./out/build/linux-relwithdebinfo/skate3`
|
|
||||||
Launch instance B (same machine; the relay loops them via 127.0.0.1).
|
|
||||||
4. In each instance's M menu: set `skate3_ghost_server = 127.0.0.1`, enable
|
|
||||||
multiplayer, join the SAME room code. The relay then routes both the ghost
|
|
||||||
poses and the game's own netcode traffic.
|
|
||||||
5. Verify in the log (stdout): `xnet transport: connecting to relay ...`,
|
|
||||||
`xnet transport: relay joined`, and on session search
|
|
||||||
`xnet transport: session reply with N session(s)`. Enable the game's
|
|
||||||
online flow (invite/join) and check whether RakNet establishes a session.
|
|
||||||
6. Two instances bind the same UDP data port → the second may fail to bind.
|
|
||||||
If so, the game's socket bind must become relay-aware (bind to an
|
|
||||||
ephemeral port in relay mode) — a known Phase 3 item.
|
|
||||||
|
|
||||||
## Phase 2 — invite UI
|
|
||||||
- `XamShowGameInviteUI` / `XamShowFriendsUI` as ImGui `XamDialog`s backed by
|
|
||||||
the relay lobby (real player list). Receive side (`XamNotifyCreateListener`
|
|
||||||
/ `XamSessionRefObjByHandle`) already works.
|
|
||||||
|
|
||||||
## Phase 3 — iterate to playable
|
|
||||||
Whatever the game needs past discovery (RakNet handshake, ports, privilege
|
|
||||||
fakes). Ghost stays as fallback throughout.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Testing is done with **two instances on one machine** (no second device);
|
|
||||||
the relay loops them via `127.0.0.1`. A headless harness may be added.
|
|
||||||
- Build: `clang-22` override, preset `linux-relwithdebinfo`, per
|
|
||||||
`docs/build.md` — same as `main`.
|
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
|
|
||||||
#include <rex/cvar.h>
|
#include <rex/cvar.h>
|
||||||
#include <rex/logging.h>
|
#include <rex/logging.h>
|
||||||
#include <rex/system/xnet_transport.h>
|
|
||||||
|
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
@ -364,10 +363,6 @@ void SendJoin(GhostSock sock, const GhostAddr& to, bool ping) {
|
||||||
std::memcpy(jp.room, g_room, g_room_len);
|
std::memcpy(jp.room, g_room, g_room_len);
|
||||||
jp.room_len = g_room_len;
|
jp.room_len = g_room_len;
|
||||||
}
|
}
|
||||||
// v5: the machine's virtual IP (10.80.0.x) so the relay can route the
|
|
||||||
// game's own netcode and answer session queries. Mirrors the XNet layer's
|
|
||||||
// identity (single source of truth in the rexglue transport).
|
|
||||||
jp.virtual_ip = rex::system::XNetTransport::LocalVirtualIp();
|
|
||||||
// Display name: JOIN carries it so the relay can echo it in the roster;
|
// Display name: JOIN carries it so the relay can echo it in the roster;
|
||||||
// the LAN discovery PING carries it too (a host can tag probes).
|
// the LAN discovery PING carries it too (a host can tag probes).
|
||||||
CurrentDisplayName(jp.name, &jp.name_len);
|
CurrentDisplayName(jp.name, &jp.name_len);
|
||||||
|
|
@ -726,11 +721,6 @@ void GhostNetworkLoop() {
|
||||||
const double dt = std::min(now - last_tick, 0.25);
|
const double dt = std::min(now - last_tick, 0.25);
|
||||||
last_tick = now;
|
last_tick = now;
|
||||||
|
|
||||||
// netcode-revival: the game's frontend refuses "Xbox Live features"
|
|
||||||
// before calling any rexglue API when its internal live flag is 0; force
|
|
||||||
// it so the party/EA flow can start. Runs regardless of ghost enable.
|
|
||||||
rex::system::XNetTransport::Instance().ForceLiveFlag();
|
|
||||||
|
|
||||||
if (!Enabled()) {
|
if (!Enabled()) {
|
||||||
ResetSession();
|
ResetSession();
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
|
@ -738,27 +728,11 @@ void GhostNetworkLoop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool debug = Debug();
|
const bool debug = Debug();
|
||||||
|
const uint16_t port = uint16_t(REXCVAR_GET(skate3_ghost_port));
|
||||||
std::string server = REXCVAR_GET(skate3_ghost_server);
|
std::string server = REXCVAR_GET(skate3_ghost_server);
|
||||||
if (server.empty()) {
|
if (server.empty()) {
|
||||||
server = kGhostDefaultServer;
|
server = kGhostDefaultServer;
|
||||||
}
|
}
|
||||||
// A server string may carry its own port ("host:port"); prefer it over
|
|
||||||
// the port slider so the relay resolve AND the game's netcode transport
|
|
||||||
// agree on the endpoint.
|
|
||||||
std::string host = server;
|
|
||||||
uint16_t port = uint16_t(REXCVAR_GET(skate3_ghost_port));
|
|
||||||
const size_t colon = server.rfind(':');
|
|
||||||
if (colon != std::string::npos && colon != 0 &&
|
|
||||||
colon != server.size() - 1) {
|
|
||||||
host = server.substr(0, colon);
|
|
||||||
try {
|
|
||||||
const int p = std::stoi(server.substr(colon + 1));
|
|
||||||
if (p > 0 && p <= 65535) {
|
|
||||||
port = uint16_t(p);
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const std::string room_str = REXCVAR_GET(skate3_ghost_room);
|
const std::string room_str = REXCVAR_GET(skate3_ghost_room);
|
||||||
char room_buf[kGhostMaxRoomLen] = {};
|
char room_buf[kGhostMaxRoomLen] = {};
|
||||||
uint8_t room_len = 0;
|
uint8_t room_len = 0;
|
||||||
|
|
@ -775,13 +749,6 @@ void GhostNetworkLoop() {
|
||||||
g_room_len = room_len;
|
g_room_len = room_len;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A typed/clicked room change re-joins the relay on the next tick
|
|
||||||
// instead of waiting up to the heartbeat interval.
|
|
||||||
static std::string s_last_room;
|
|
||||||
if (room_len != 0 && s_last_room != room_str) {
|
|
||||||
s_last_room = room_str;
|
|
||||||
g_peer_public.last_join = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sock == GHOST_INVALID_SOCKET) {
|
if (sock == GHOST_INVALID_SOCKET) {
|
||||||
sock = GhostSocketCreate(/*allow_broadcast=*/true);
|
sock = GhostSocketCreate(/*allow_broadcast=*/true);
|
||||||
|
|
@ -796,7 +763,7 @@ void GhostNetworkLoop() {
|
||||||
if (server != last_server || port != last_port) {
|
if (server != last_server || port != last_port) {
|
||||||
GhostAddr a;
|
GhostAddr a;
|
||||||
g_peer_public.configured =
|
g_peer_public.configured =
|
||||||
GhostResolve(host + ":" + std::to_string(port), port, &a);
|
GhostResolve(server + ":" + std::to_string(port), port, &a);
|
||||||
if (g_peer_public.configured) {
|
if (g_peer_public.configured) {
|
||||||
g_peer_public.addr = a;
|
g_peer_public.addr = a;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -814,14 +781,6 @@ void GhostNetworkLoop() {
|
||||||
// keeps the roster/name labels current as members join and leave), and
|
// keeps the roster/name labels current as members join and leave), and
|
||||||
// reconnect when the HELLO stream goes quiet.
|
// reconnect when the HELLO stream goes quiet.
|
||||||
if (g_peer_public.configured) {
|
if (g_peer_public.configured) {
|
||||||
// netcode-revival: keep the game's own netcode transport on the same
|
|
||||||
// relay + room (idempotent; reconnect happens on any change).
|
|
||||||
char active_room[kGhostMaxRoomLen] = {};
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(g_mu);
|
|
||||||
std::memcpy(active_room, g_room, g_room_len);
|
|
||||||
}
|
|
||||||
rex::system::XNetTransport::Instance().SetRelay(host.c_str(), port, active_room);
|
|
||||||
if (now - g_peer_public.last_join >= kJoinIntervalSec) {
|
if (now - g_peer_public.last_join >= kJoinIntervalSec) {
|
||||||
SendJoin(sock, g_peer_public.addr, /*ping=*/false);
|
SendJoin(sock, g_peer_public.addr, /*ping=*/false);
|
||||||
g_peer_public.last_join = now;
|
g_peer_public.last_join = now;
|
||||||
|
|
|
||||||
|
|
@ -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 = 5;
|
constexpr uint8_t kGhostVersion = 4;
|
||||||
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,10 +53,6 @@ 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
|
||||||
|
|
@ -76,17 +72,13 @@ 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. v5 adds the
|
// in the HELLO roster so nameplates can label every ghost.
|
||||||
// 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.
|
||||||
|
|
@ -122,34 +114,6 @@ 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,11 +228,6 @@ 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) {
|
||||||
|
|
@ -331,73 +326,6 @@ 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);
|
|
||||||
uint32_t recipients = 0;
|
|
||||||
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);
|
|
||||||
++recipients;
|
|
||||||
}
|
|
||||||
// Verbose: a windowed summary of game datagrams relayed, so the game's
|
|
||||||
// own netcode traffic is visible in the log.
|
|
||||||
if (verbose_.load(std::memory_order_relaxed) && recipients != 0) {
|
|
||||||
++game_forwards_;
|
|
||||||
if (now - last_game_log_ >= 2.0 || game_forwards_ == 1) {
|
|
||||||
GHOST_RELAY_INFO("game traffic: {} datagram(s) relayed to {} "
|
|
||||||
"member(s) in room '{}'",
|
|
||||||
game_forwards_, recipients,
|
|
||||||
std::string(me->room, me->room_len));
|
|
||||||
last_game_log_ = now;
|
|
||||||
game_forwards_ = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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) {
|
||||||
|
|
@ -471,53 +399,15 @@ 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) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bool verbose = verbose_.load(std::memory_order_relaxed);
|
clients_.erase(std::remove_if(clients_.begin(), clients_.end(),
|
||||||
clients_.erase(std::remove_if(
|
[now](const Client& c) {
|
||||||
clients_.begin(), clients_.end(),
|
return now - c.last_seen > kClientTimeoutSec;
|
||||||
[now, verbose](const Client& c) {
|
}),
|
||||||
const bool stale = now - c.last_seen > kClientTimeoutSec;
|
|
||||||
if (stale && verbose) {
|
|
||||||
GHOST_RELAY_INFO("{} timed out ({}s silence; {} joined "
|
|
||||||
"room '{}')",
|
|
||||||
GhostAddrStr(c.addr),
|
|
||||||
int(now - c.last_seen), c.session_id,
|
|
||||||
std::string(c.room, c.room_len));
|
|
||||||
}
|
|
||||||
return stale;
|
|
||||||
}),
|
|
||||||
clients_.end());
|
clients_.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,9 @@ 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;
|
||||||
|
|
@ -70,7 +68,6 @@ 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] = {};
|
||||||
|
|
@ -84,11 +81,6 @@ class GhostRelay {
|
||||||
bool locked_ = false;
|
bool locked_ = false;
|
||||||
char locked_room_[kGhostMaxRoomLen] = {};
|
char locked_room_[kGhostMaxRoomLen] = {};
|
||||||
uint8_t locked_room_len_ = 0;
|
uint8_t locked_room_len_ = 0;
|
||||||
// Verbose game-traffic telemetry (netcode-revival): count forwarded game
|
|
||||||
// datagrams and log a windowed summary so the game's own RakNet traffic is
|
|
||||||
// observable without spamming the log per packet.
|
|
||||||
uint32_t game_forwards_ = 0;
|
|
||||||
double last_game_log_ = 0.0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace skate3::native_ghost
|
} // namespace skate3::native_ghost
|
||||||
|
|
@ -3,13 +3,11 @@
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
#include <rex/cvar.h>
|
#include <rex/cvar.h>
|
||||||
#include <rex/logging.h>
|
#include <rex/logging.h>
|
||||||
#include <rex/system/xnet_transport.h>
|
|
||||||
|
|
||||||
#include "native/skate3_native_ghost.h"
|
#include "native/skate3_native_ghost.h"
|
||||||
#include "skate3_native_scene.h"
|
#include "skate3_native_scene.h"
|
||||||
|
|
@ -99,12 +97,11 @@ void GhostDialog::OnDraw(ImGuiIO& io) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
ImGui::Text("Room code");
|
ImGui::Text("Room code");
|
||||||
// Commits on every change so the relay follows the typed code immediately
|
|
||||||
// (no Enter required - Enter was flaky here). Partial codes are harmless:
|
|
||||||
// the relay just moves you to each intermediate room as you type.
|
|
||||||
if (ImGui::InputText("##ghost_room", room_input_, sizeof(room_input_),
|
if (ImGui::InputText("##ghost_room", room_input_, sizeof(room_input_),
|
||||||
ImGuiInputTextFlags_CharsUppercase)) {
|
ImGuiInputTextFlags_EnterReturnsTrue |
|
||||||
|
ImGuiInputTextFlags_CharsUppercase)) {
|
||||||
REXCVAR_SET(skate3_ghost_room, std::string(room_input_));
|
REXCVAR_SET(skate3_ghost_room, std::string(room_input_));
|
||||||
|
ImGui::SetKeyboardFocusHere(-1);
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
// Copy the ACTIVE room (the one this player is actually in), which for an
|
// Copy the ACTIVE room (the one this player is actually in), which for an
|
||||||
|
|
@ -128,8 +125,10 @@ void GhostDialog::OnDraw(ImGuiIO& io) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
ImGui::Text("Display name");
|
ImGui::Text("Display name");
|
||||||
if (ImGui::InputText("##ghost_name", name_input_, sizeof(name_input_))) {
|
if (ImGui::InputText("##ghost_name", name_input_, sizeof(name_input_),
|
||||||
|
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||||
REXCVAR_SET(skate3_ghost_name, std::string(name_input_));
|
REXCVAR_SET(skate3_ghost_name, std::string(name_input_));
|
||||||
|
ImGui::SetKeyboardFocusHere(-1);
|
||||||
}
|
}
|
||||||
ImGui::TextDisabled("Shown on your nameplate above the ghost. Empty = "
|
ImGui::TextDisabled("Shown on your nameplate above the ghost. Empty = "
|
||||||
"your OS user name.");
|
"your OS user name.");
|
||||||
|
|
@ -175,16 +174,14 @@ void GhostDialog::OnDraw(ImGuiIO& io) {
|
||||||
|
|
||||||
ImGui::Text("Relay server (optional)");
|
ImGui::Text("Relay server (optional)");
|
||||||
ImGui::PushItemWidth(-1.0f);
|
ImGui::PushItemWidth(-1.0f);
|
||||||
// Commit on Enter OR when the field loses focus (clicking away), so a
|
if (ImGui::InputText("##ghost_server", server_input_, sizeof(server_input_),
|
||||||
// swallowed Enter cannot leave the old server active. Not commit-on-change:
|
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||||
// partial hostnames would flap the connection while typing.
|
|
||||||
ImGui::InputText("##ghost_server", server_input_, sizeof(server_input_));
|
|
||||||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
|
||||||
REXCVAR_SET(skate3_ghost_server, std::string(server_input_));
|
REXCVAR_SET(skate3_ghost_server, std::string(server_input_));
|
||||||
|
ImGui::SetKeyboardFocusHere(-1);
|
||||||
}
|
}
|
||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
ImGui::TextDisabled("Empty = built-in default relay. Press Enter or click "
|
ImGui::TextDisabled("Empty = built-in default relay. No port forwarding "
|
||||||
"away to apply. No port forwarding needed on any client.");
|
"needed on any client.");
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
|
|
@ -218,34 +215,6 @@ void GhostDialog::OnDraw(ImGuiIO& io) {
|
||||||
if (st.peer[0] != '\0') {
|
if (st.peer[0] != '\0') {
|
||||||
ImGui::Text(" Relay: %s", st.peer);
|
ImGui::Text(" Relay: %s", st.peer);
|
||||||
}
|
}
|
||||||
// netcode-revival: the game's own netcode transport (RakNet via the relay).
|
|
||||||
{
|
|
||||||
using rex::system::XNetTransport;
|
|
||||||
ImGui::Text(" Netcode: %s",
|
|
||||||
XNetTransport::Instance().Active()
|
|
||||||
? "relay joined"
|
|
||||||
: "not connected");
|
|
||||||
const std::vector<rex::system::XNetSession> xnet_sessions =
|
|
||||||
XNetTransport::Instance().Sessions();
|
|
||||||
if (!xnet_sessions.empty()) {
|
|
||||||
ImGui::Text(" Sessions: %zu discoverable",
|
|
||||||
xnet_sessions.size());
|
|
||||||
}
|
|
||||||
// netcode-revival: the frontend "Xbox Live features" gate inputs, so we
|
|
||||||
// can see why a menu item is greyed out without needing stdout.
|
|
||||||
const XNetTransport::GateStatus gs = XNetTransport::Instance().GetGateStatus();
|
|
||||||
ImGui::TextDisabled("-- Party Play gate --");
|
|
||||||
ImGui::Text(" Live flag: %s value=%d",
|
|
||||||
gs.live_flag_mapped ? "mapped" : "NOT MAPPED",
|
|
||||||
gs.live_flag_value);
|
|
||||||
ImGui::Text(" Signin slot: 0=%u 1=%u 2=%u 3=%u", gs.signin_state[0],
|
|
||||||
gs.signin_state[1], gs.signin_state[2], gs.signin_state[3]);
|
|
||||||
ImGui::Text(" Signin info: %s%s%s%s", gs.signin_info_ok[0] ? "0" : "x",
|
|
||||||
gs.signin_info_ok[1] ? "1" : "x", gs.signin_info_ok[2] ? "2" : "x",
|
|
||||||
gs.signin_info_ok[3] ? "3" : "x");
|
|
||||||
ImGui::Text(" Link active: %s", gs.link_active ? "yes" : "no");
|
|
||||||
ImGui::Text(" Title online: %s", gs.title_online ? "yes" : "no");
|
|
||||||
}
|
|
||||||
if (st.refused) {
|
if (st.refused) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f),
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f),
|
||||||
" Refused by relay (protocol version mismatch)");
|
" Refused by relay (protocol version mismatch)");
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ struct LocalProfile {
|
||||||
std::string gamertag;
|
std::string gamertag;
|
||||||
uint64_t xuid = 0;
|
uint64_t xuid = 0;
|
||||||
bool signed_in = true;
|
bool signed_in = true;
|
||||||
// netcode-revival: default the profile to Xbox Live signed-in so the
|
bool live_signed_in = false;
|
||||||
// game's online gates (which require XamUserGetSigninState == 2) open.
|
|
||||||
bool live_signed_in = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LocalProfileStore {
|
struct LocalProfileStore {
|
||||||
|
|
|
||||||
2
third_party/rexglue-sdk
vendored
2
third_party/rexglue-sdk
vendored
|
|
@ -1 +1 @@
|
||||||
Subproject commit 30ee3e2b8647d80a48ce823a75ea3facf60f60af
|
Subproject commit 7eb0faf7787f5e01333c228b8e3f03c32f7295ea
|
||||||
Loading…
Add table
Reference in a new issue