skate3recomp-multiplayer/docs/netcode-revival.md

4.5 KiB

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_asserts 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 XamDialogs 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.