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.
115 lines
No EOL
4.9 KiB
C++
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
|