- 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?'.
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_relayserver (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
- Launch the game, press M to open the multiplayer menu.
- Tick Enable ghost multiplayer.
- 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.
- Use Browse rooms (lobby) to see who is on the configured relay and click a room to join it.
- 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). Setskate3_ghost_debug 1(F12 menu) for verbose ghost lines. - Relay logs:
journalctl -u ghost-relay(add--verbosetoExecStartfor 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-clientssane 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 protocolsrc/native/skate3_native_ghost_net.h— POSIX/Winsock UDP shimsrc/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/capturesrc/skate3_native_ghost_dialog.{h,cpp}— multiplayer menu + nameplatestools/ghost_relay_main.cpp,tools/ghost_relay.service,tools/ghost_relay_deploy.sh— the standalone server