Production-readiness pass toward 1.0:
- Protocol handshake: JOIN carries kGhostVersion; a mismatched client is
REFUSED by the relay (HELLO session 0) and its poses are never forwarded,
so two builds can never silently corrupt each other's pose stream. The
client surfaces the refusal in the menu and skips refused LAN hosts during
discovery.
- Crash-proofing: every received payload is bounds-checked against its
packet size before parsing (HELLO / PoseChunk / JOIN); both the client
network thread and the relay thread are exception-guarded (a throw logs
and keeps the loop alive instead of hitting std::terminate). The relay no
longer creates client records for unknown-address POSE/LEAVE floods and
enforces a hard client cap (--max-clients, default 64).
- Logging: rate-limited error logging throughout (send failures, resolve
failures, refusals, thread exceptions, client-cap drops). The relay core
uses a build-flag shim (REXLOG in-game, a tiny {} formatter in the
dependency-free standalone), with --verbose join/leave lines for the
server. Game crashes themselves are already handled by the SDK's crash
handlers + file logs; this layer now avoids triggering them and explains
itself when it can't connect.
- Settings persistence: server/room/name/port/rate save into the app's cvar
config on change, so a player's multiplayer setup survives a restart.
- Menu: status now shows protocol refusals and the last error.
Tests updated for the handshake (version-refusal case added); all suites
pass.
Phase 4:
- Cloth: remote ghosts now wear this player's cloth garments (tee/pants).
The local player's rigid character.cloth_ropa items (CPU-sim deformed
root-local VB) are cloned per ghost and drawn with the REMOTE entity world
from the pose, so each ghost is draped in its own root frame. No mesh or
cloth data is transmitted; the shader already applies item.world to the
rigid cloth path (zero blend weights). Sim-inactive (skinned) garments and
garments not near the local body are skipped.
- Nameplates: display names ride the wire. JOIN carries a display name
(skate3_ghost_name cvar, default the OS user name); the relay stores it
per client and echoes {session_id, name} pairs in the HELLO roster
(RosterEntry). Clients re-JOIN every 5 s so the roster/names stay current
as members join and leave. A GhostNameplateOverlay (always registered,
draws only while enabled) projects each ghost's entity origin +1.75 m
through the published camera and draws a colored name label above it.
Uses skate3::native_scene::GetPublishedCamera (new thread-safe snapshot of
the published view-proj/cam) so the UI thread can project world points.
- GhostDialog gains a display-name field. Protocol version bumped to 3.
Bolt-on multiplayer layer confined to src/native and hand-written code (no
codegen output touched):
- GhostRelay core (src/native/skate3_native_ghost_relay.*): small UDP relay
matching clients into rooms by a shareable code; forwards each client's
pose to the other room members. Used by both the standalone binary and
the in-game LAN host fallback.
- Standalone server (tools/ghost_relay_main.cpp, ghost_relay target): runs
on a public VPS or behind a free tunnel; clients connect OUTBOUND, so no
port forwarding is needed on any client.
- In-game module (skate3_native_ghost.*): captures the local player's world
transform at Sk8::UpdateBoneTransforms exit (pack-owner entity +416,
verified layout) and broadcasts it at a fixed rate; auto-connects to the
default public relay, falls back to LAN auto-host + UDP broadcast
discovery when unreachable.
- Renderer: draws a solid-color cube per remote player in RenderScene via
the existing scene PSO tint.a>0 branch (no new shader/SPIR-V).
- UI: GhostDialog (M key) with enable toggle, room code, relay override,
status readout. Off by default.
Wire protocol in skate3_native_ghost_proto.h (JOIN/HELLO/POSE/PING/LEAVE).