skate3recomp-multiplayer/src/native/skate3_native_ghost.h
MiTHRAL d7c9cbcb70 ghost multiplayer: lobby browser (LIST/ROOMS)
The M menu now has a room browser: a Refresh button asks the relay for its
room list, and the list (room code, player count, up to 4 member names) is
shown as clickable rows - clicking one sets the room code and joins on the
next heartbeat.

- Protocol: kTypeList (client -> relay, empty payload) and kTypeRooms
  (relay -> client, RoomsPayload: up to 16 rooms, code + count + names).
  LIST works from unjoined addresses so the browser can query before
  joining; a joined client's LIST is answered too. Version bumped to 4.
- Relay: BuildRoomList aggregates the client table by room (refused clients
  excluded), most-populated first, capped at kGhostMaxRooms.
- Client: RequestRooms()/GetRooms() - the network thread sends LIST to the
  best current peer (public relay, LAN peer, or the local host when
  hosting) and stores the reply under the shared lock; ROOMS replies from
  any other address are ignored. The list is cleared on disable.
- Dialog: CollapsingHeader 'Browse rooms (lobby)' with refresh + selectable
  rows.

New relay test (ghost_relay_test6) covers the lobby path; all suites pass.
2026-08-26 22:19:34 -04:00

115 lines
No EOL
4.9 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 <filesystem>
#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