skate3recomp-multiplayer/src/native/skate3_native_ghost.h
MiTHRAL bfb504db7d ghost multiplayer: full skater skeleton pose + character-model ghosts
Phase 2: replace the placeholder-cube ghost rendering with real character
models driven by the remote player's skeleton.

- Wire: POSE now carries the sender's skater skeleton (up to 64 bones, 3
  float4 rows each, index-ordered exactly like DrawItem::bones), padded with
  identity rows so every mesh's referenced bones stay in range. The relay
  forwards POSE payloads verbatim (re-stamping only the sender session id),
  so the protocol can grow without a relay update.
- Capture: OnFrameSceneBuilt (called from BuildFrameScene pre-publish)
  snapshots the local player's body item bone palette (highest-bone-count
  skinned non-ropa non-LivingWorld char item near the camera) as the
  broadcast pose; defaults the broadcast rate to 15 Hz.
- Render: in RenderScene, each remote ghost is drawn by cloning this
  player's own character pieces (body + nearby skinned char items) and
  substituting the remote skeleton into the bone ring - same meshes,
  textures and cached GPU decodes, no mesh transfer. Ghosts without a pose
  (older peers / capture not ready) fall back to the solid-color cube, which
  skate3_ghost_draw_local still uses for the self-check.

Known Phase-2 limitations (documented in code): ghosts render with this
player's own character model/outfit, cloth (ropa) garments are skipped (no
remote cloth sim), and hair/alpha pieces render opaque. All confined to
src/native and the hand-written scene hooks.
2026-08-26 20:16:42 -04:00

86 lines
No EOL
3.6 KiB
C++

#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 <cstdint>
#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