diff --git a/.gitignore b/.gitignore index 7097c38..d4f1fed 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 2994c4b..5cb97d3 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/ghost-multiplayer.md b/docs/ghost-multiplayer.md new file mode 100644 index 0000000..8fdc345 --- /dev/null +++ b/docs/ghost-multiplayer.md @@ -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 40001 # LIST probe +# expect a kTypeRooms reply (starts 'S G H T' + type 9 + payload) +``` + +## Logging & troubleshooting + +- **Game logs:** `/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 \ No newline at end of file diff --git a/tools/ghost_relay.service b/tools/ghost_relay.service new file mode 100644 index 0000000..00bed9e --- /dev/null +++ b/tools/ghost_relay.service @@ -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 \ No newline at end of file diff --git a/tools/ghost_relay_deploy.sh b/tools/ghost_relay_deploy.sh new file mode 100755 index 0000000..5588b1f --- /dev/null +++ b/tools/ghost_relay_deploy.sh @@ -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 /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 < 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=""; 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." \ No newline at end of file