skate3recomp-multiplayer/docs/ghost-multiplayer.md
MiTHRAL 1763b39eca ghost multiplayer: deployment docs + systemd unit + install script
- docs/ghost-multiplayer.md: the full beta guide - player usage, cvars,
  relay deployment (VPS build/install, free-tunnel option, verification),
  logging/troubleshooting and security notes.
- tools/ghost_relay.service: hardened systemd unit for the public relay
  (Restart=on-failure, NoNewPrivileges, PrivateTmp, ProtectSystem).
- tools/ghost_relay_deploy.sh: one-shot build + install + enable script for
  the VPS (GHOST_RELAY_PORT / GHOST_RELAY_VERBOSE overrides; opens the UDP
  port via ufw). Verified: the exact build line compiles and runs.
- README: multiplayer pointer under 'How Do I Play?'.
2026-08-26 22:21:08 -04:00

6.7 KiB

Ghost multiplayer (beta)

A from-scratch "ghost player" layer for the Skate 3 recomp native renderer: every player's skater skeleton is relayed through a small UDP server so other players see a real character model (driven by the remote skeleton) plus a nameplate. Nothing touches the game's own netcode, animation or physics.

How it works

  • Topology: every client connects outbound (UDP) to a relay. No port forwarding on any client. The relay forwards each player's pose stream to the other members of the same room (a 4-8 character code).
  • Relay: either the public ghost_relay server (below) or a player's game in LAN host mode. When the configured relay is unreachable, the game automatically starts an in-process relay on the LAN (UDP broadcast discovery) so same-network play still works.
  • Protocol: versioned (kGhostVersion). A mismatched client/server is refused with a clear message rather than silently misbehaving.
  • Rendering: ghosts are drawn with this player's own character model driven by the remote skeleton (bone palettes, no mesh transfer). Cloth garments drape in the remote root frame; nameplates label each ghost with the owner's display name.
  • Logs: everything important is logged to the game's log file (next to the executable in logs/). The relay logs refusals/caps to its own output.

Player usage

  1. Launch the game, press M to open the multiplayer menu.
  2. Tick Enable ghost multiplayer.
  3. Enter a room code (or leave empty to get an auto-assigned one, shown in the menu — share it with friends). Players sharing a code see each other.
  4. Use Browse rooms (lobby) to see who is on the configured relay and click a room to join it.
  5. Set a display name for your nameplate (default: your OS user name).

Settings (server, room, name, port, rate) persist across restarts.

Configuration cvars

cvar default meaning
skate3_ghost_enable false master switch (also the menu toggle)
skate3_ghost_server "" relay host[:port]; empty = the compiled-in default
skate3_ghost_port 40001 UDP port (outbound + LAN discovery/host)
skate3_ghost_room "" room code; empty = server assigns one
skate3_ghost_name "" nameplate name; empty = OS user name
skate3_ghost_rate 15 pose broadcast rate (Hz)
skate3_ghost_lan true LAN auto-discovery + auto-host fallback
skate3_ghost_draw_local false debug: show your own capture cube
skate3_ghost_debug false verbose ghost logging

The compiled-in default relay address is kGhostDefaultServer in src/native/skate3_native_ghost_proto.h — set it to your relay before shipping a build.

Deploying the relay (public server)

The relay is a single dependency-free binary (plain sockets, no rexglue). Run it on a public VPS (any size; the workload is a few UDP forwards, a small VPS is plenty) or on your own machine behind a free tunnel (playit.gg / ngrok / Tailscale Funnel).

Build

# from the repo root
cmake --preset linux-relwithdebinfo -DSKATE3_GAME_DATA_ROOT="$PWD/game"
cmake --build --preset linux-relwithdebinfo --target ghost_relay --parallel
# -> out/build/linux-relwithdebinfo/ghost_relay

Or compile in place (no CMake/rexglue needed, just a C++23 compiler):

g++ -std=c++23 -O2 -DSKATE3_RELAY_STANDALONE \
    -Isrc/native \
    tools/ghost_relay_main.cpp src/native/skate3_native_ghost_relay.cpp \
    -o ghost_relay

Install on the VPS

tools/ghost_relay_deploy.sh builds, installs and registers the service:

# copy the repo (or just tools/ + the relay sources) to the VPS, then:
./tools/ghost_relay_deploy.sh          # uses UDP 40001 by default
GHOST_RELAY_PORT=40002 ./tools/ghost_relay_deploy.sh

Manual:

install -m 0755 ghost_relay /usr/local/bin/ghost_relay
install -m 0644 tools/ghost_relay.service /etc/systemd/system/ghost-relay.service
systemctl daemon-reload && systemctl enable --now ghost-relay
ufw allow 40001/udp          # open the port; nothing else is needed

The ghost_relay.service unit is:

[Unit]
Description=Skate 3 recomp ghost multiplayer relay
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/ghost_relay --port 40001 --max-clients 64
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target

Free-tunnel option (no VPS)

The relay only needs a public UDP endpoint. Run it on any machine and expose it with a free tunnel that supports UDP (playit.gg is the most straightforward for games): it gives you a public host:port that forwards to your local relay. Point clients at it with skate3_ghost_server (or set it as the compiled-in default). No client-side port forwarding either way.

Verify

systemctl status ghost-relay
journalctl -u ghost-relay -f          # join/leave lines (--verbose)
ss -ulnp | grep 40001                 # listening on UDP

From a second machine:

printf '\x53\x47\x48\x54\x08\x00\x00' | nc -u <relay-ip> 40001  # LIST probe
# expect a kTypeRooms reply (starts 'S G H T' + type 9 + payload)

Logging & troubleshooting

  • Game logs: <exe dir>/logs/ (SDK file logging; also where crash dumps land). Set skate3_ghost_debug 1 (F12 menu) for verbose ghost lines.
  • Relay logs: journalctl -u ghost-relay (add --verbose to ExecStart for per-join/leave lines).
  • "Refused by relay (protocol version mismatch)": the game build and the relay must agree on kGhostVersion (both from the same build). Rebuild both.
  • "Cannot resolve the relay": the default address is a placeholder until you set skate3_ghost_server; the game then falls back to LAN auto-host.
  • Ghosts show as cubes: the sender had no skeleton capture ready (menus, loading) — the cube is the graceful fallback.

Security notes

  • The relay is an anonymous UDP relay: any client that knows the address can join a room. It is intended for friends / a community, not untrusted internet strangers. Keep --max-clients sane and do not expose it beyond your intended audience.
  • Room codes are not passwords. Do not put personal information in the room code or display name.

Repository layout

  • src/native/skate3_native_ghost_proto.h — wire protocol
  • src/native/skate3_native_ghost_net.h — POSIX/Winsock UDP shim
  • src/native/skate3_native_ghost_relay.{h,cpp} — relay core (in-game host mode + standalone binary)
  • src/native/skate3_native_ghost.{h,cpp} — in-game client/capture
  • src/skate3_native_ghost_dialog.{h,cpp} — multiplayer menu + nameplates
  • tools/ghost_relay_main.cpp, tools/ghost_relay.service, tools/ghost_relay_deploy.sh — the standalone server