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.
86 lines
No EOL
3.6 KiB
C++
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
|