#pragma once // Ghost multiplayer module (bolt-on, hand-written, confined to src/native): // a from-scratch "ghost player" layer that relays each player's world // transform (position + rotation) through a small UDP relay so other players // see a placeholder marker at the same spot. It deliberately does NOT touch // the game's own netcode, animation, physics or any codegen'd logic. // // Topology (automatic, zero client-side setup): // 1. Every client connects OUTBOUND to the default public relay // (kGhostDefaultServer / skate3_ghost_server cvar) and joins a room // code. No port forwarding on any client. // 2. If the public relay is unreachable, the game automatically becomes a // LAN host (GhostRelay in-process) and LAN peers auto-discover it via // UDP broadcast PING/HELLO. // // Local-player capture: at Sk8::UpdateBoneTransforms exit (the documented // player-only hook) the pack-owner entity is the player's SkaterPresEntity; // its verified world matrix lives at entity+416 (m_MatLtoWTrans). The // capture runs on the sim thread and copies HOST floats under a mutex; the // network thread never reads guest memory. #include #include #include "skate3_native_ghost_proto.h" namespace skate3::native_scene { struct FrameScene; } // namespace skate3::native_scene namespace skate3::native_ghost { // ---- host-side capture (called from skate3_native_render.cpp) ------------- void OnLocalSkaterTick(uint8_t* base); // ---- host-side pose capture (called from BuildFrameScene pre-publish) ----- // Snapshot the local player's skater skeleton (the highest-bone-count // skinned non-ropa character item near the camera) as the pose this client // broadcasts. Called every built frame; gated internally on the enable cvar. void OnFrameSceneBuilt(uint8_t* base, const skate3::native_scene::FrameScene& scene); // ---- network thread lifecycle ------------------------------------------------- void EnsureNetworkThread(); void Shutdown(); // Persist the multiplayer cvars (server/room/name/port/rate) into the app's // cvar config file whenever they change, so a player's settings survive a // restart. config_path is the same file the SDK loads at startup. Safe to // call once at startup. void InstallSettingsPersistence(const std::filesystem::path& config_path); // ---- status (UI / debug) -------------------------------------------------------- struct Status { bool enabled = false; bool connected = false; // joined to a relay (public or LAN) bool hosting = false; // this game is relaying a LAN room bool refused = false; // the relay refused us (protocol version mismatch) uint32_t session_id = 0; char room[kGhostMaxRoomLen] = {}; uint8_t room_len = 0; size_t roster = 0; // other members in our room size_t ghosts = 0; // remote ghosts currently drawn char peer[96] = {}; // active relay "ip:port" char last_error[160] = {}; // last non-fatal error, for the menu }; bool GetStatus(Status* out); // ---- render query (called from RenderScene, render thread) --------------------- size_t GhostCount(); // Row-vector world matrix (translation in row 3) + a stable color per ghost. bool GetGhost(uint32_t index, float world[16], float color[3]); // Full-pose variant: additionally copies up to max_bones 12-float skeleton // rows (the exact layout DrawItem::bones uses) so the renderer can drive a // real character mesh. bone_count receives the copied row count (0 = the // sender had no pose available; the caller should use the cube fallback). bool GetGhostPose(uint32_t index, float world[16], float color[3], uint16_t max_bones, float* bones, uint16_t* bone_count); // Self-check cube at the local captured pose (skate3_ghost_draw_local). bool GetLocalWorld(float world[16]); // Lightweight per-ghost query for the nameplate overlay: the ghost's world // position (entity world), marker color, and display name (from the room // roster, or a session fallback). Returns false if the ghost is gone. bool GetGhostLabel(uint32_t index, float pos[3], float color[3], char* name, size_t name_max); // ---- lobby browser ------------------------------------------------------------ // Ask the network thread to fetch the relay's room list (non-blocking; the // reply updates GetRooms asynchronously, a second or two later). void RequestRooms(); // Latest room list snapshot. count == 0 before the first ROOMS reply. struct RoomList { uint8_t version = 0; uint8_t count = 0; RoomEntry rooms[kGhostMaxRooms]; double received = 0.0; // steady seconds of the reply (0 = none yet) }; bool GetRooms(RoomList* out); // ---- cube geometry (56-byte records matching kSceneInputLayout) ----------------- struct GhostCubeVertex { float pos[3]; float uv[2]; float uv2[2]; uint8_t bw[4]; uint8_t bi[4]; float normal[3]; float uv3[2]; }; const GhostCubeVertex* CubeVertices(); uint32_t CubeVertexCount(); // 36 (12 triangles, non-indexed) } // namespace skate3::native_ghost