#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 "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(); // ---- 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 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" }; 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]); // ---- 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