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?'.
This commit is contained in:
parent
d7c9cbcb70
commit
1763b39eca
5 changed files with 289 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,6 +6,8 @@
|
|||
/tools/*
|
||||
# ...except the ghost multiplayer relay server (a first-party tool).
|
||||
!/tools/ghost_relay_main.cpp
|
||||
!/tools/ghost_relay.service
|
||||
!/tools/ghost_relay_deploy.sh
|
||||
|
||||
# Local game dump and retail media. Users must provide their own copy.
|
||||
/game/
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ Native Rendering Showcase (click to go to YouTube):
|
|||
|
||||
## How Do I Play?
|
||||
|
||||
> **Ghost multiplayer (beta):** this fork adds a from-scratch ghost-player
|
||||
> layer — relay your skater to other players via a small UDP server (no port
|
||||
> forwarding on clients), with full-character ghosts, nameplates and a lobby
|
||||
> browser. Press **M** in-game. See [docs/ghost-multiplayer.md](docs/ghost-multiplayer.md)
|
||||
> for player setup, server deployment (VPS or free tunnel) and troubleshooting.
|
||||
|
||||
Notes:
|
||||
|
||||
- The Windows version is the most tested, followed by Linux, and then macOS.
|
||||
|
|
|
|||
178
docs/ghost-multiplayer.md
Normal file
178
docs/ghost-multiplayer.md
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# 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
|
||||
|
||||
```sh
|
||||
# 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):
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```sh
|
||||
# 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:
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```ini
|
||||
[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
|
||||
|
||||
```sh
|
||||
systemctl status ghost-relay
|
||||
journalctl -u ghost-relay -f # join/leave lines (--verbose)
|
||||
ss -ulnp | grep 40001 # listening on UDP
|
||||
```
|
||||
|
||||
From a second machine:
|
||||
|
||||
```sh
|
||||
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
|
||||
17
tools/ghost_relay.service
Normal file
17
tools/ghost_relay.service
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[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
|
||||
# Standard service hardening (the relay needs only a UDP socket).
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
86
tools/ghost_relay_deploy.sh
Executable file
86
tools/ghost_relay_deploy.sh
Executable file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build, install and register the ghost_relay server as a systemd service.
|
||||
#
|
||||
# Run on the VPS from the repo root (or after copying tools/ and
|
||||
# src/native/skate3_native_ghost_* to the server). The relay is
|
||||
# dependency-free: it needs only a C++23 compiler (g++/clang), no CMake or
|
||||
# rexglue.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/ghost_relay_deploy.sh
|
||||
# GHOST_RELAY_PORT=40002 ./tools/ghost_relay_deploy.sh
|
||||
# GHOST_RELAY_VERBOSE=1 ./tools/ghost_relay_deploy.sh
|
||||
#
|
||||
# Afterwards: systemctl status ghost-relay, journalctl -u ghost-relay -f.
|
||||
# Open the UDP port (the script tries `ufw allow <port>/udp`).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${GHOST_RELAY_PORT:-40001}"
|
||||
VERBOSE_FLAG=""
|
||||
if [[ "${GHOST_RELAY_VERBOSE:-0}" == "1" ]]; then
|
||||
VERBOSE_FLAG="--verbose"
|
||||
fi
|
||||
SERVICE_NAME="ghost-relay"
|
||||
BIN_PATH="/usr/local/bin/ghost_relay"
|
||||
SERVICE_PATH="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
echo "==> Building ghost_relay"
|
||||
g++ -std=c++23 -O2 -DSKATE3_RELAY_STANDALONE \
|
||||
-I"${REPO_ROOT}/src/native" \
|
||||
"${REPO_ROOT}/tools/ghost_relay_main.cpp" \
|
||||
"${REPO_ROOT}/src/native/skate3_native_ghost_relay.cpp" \
|
||||
-o /tmp/ghost_relay
|
||||
/tmp/ghost_relay --help >/dev/null 2>&1 || {
|
||||
echo "ERROR: build produced a binary that does not run (missing C++23?)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "==> Installing to ${BIN_PATH}"
|
||||
sudo install -m 0755 /tmp/ghost_relay "${BIN_PATH}"
|
||||
rm -f /tmp/ghost_relay
|
||||
|
||||
echo "==> Writing ${SERVICE_PATH}"
|
||||
sudo tee "${SERVICE_PATH}" >/dev/null <<EOF
|
||||
[Unit]
|
||||
Description=Skate 3 recomp ghost multiplayer relay
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=${BIN_PATH} --port ${PORT} --max-clients 64 ${VERBOSE_FLAG}
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
echo "==> Enabling + starting ${SERVICE_NAME}"
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable "${SERVICE_NAME}" >/dev/null
|
||||
sudo systemctl restart "${SERVICE_NAME}"
|
||||
|
||||
echo "==> Opening UDP ${PORT}"
|
||||
sudo ufw allow "${PORT}/udp" >/dev/null 2>&1 || \
|
||||
echo " (ufw not available or not permitted; open UDP ${PORT} yourself)"
|
||||
|
||||
echo "==> Status"
|
||||
sleep 1
|
||||
sudo systemctl status "${SERVICE_NAME}" --no-pager || true
|
||||
|
||||
IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
if [[ -z "${IP}" ]]; then IP="<this-server-ip>"; fi
|
||||
echo
|
||||
echo "Done. Clients connect to ${IP}:${PORT}"
|
||||
echo "Logs: journalctl -u ${SERVICE_NAME} -f"
|
||||
echo "Set the compiled-in default in src/native/skate3_native_ghost_proto.h"
|
||||
echo " (kGhostDefaultServer / kGhostDefaultServerPort), or tell players to"
|
||||
echo " set skate3_ghost_server=${IP}:${PORT} in the M menu."
|
||||
Loading…
Add table
Reference in a new issue