skate3recomp-multiplayer/src/native/skate3_native_ghost_relay.h
MiTHRAL 7a48eb9f35 ghost multiplayer: beta hardening - version handshake, crash-proofing, logging, persistence
Production-readiness pass toward 1.0:

- Protocol handshake: JOIN carries kGhostVersion; a mismatched client is
  REFUSED by the relay (HELLO session 0) and its poses are never forwarded,
  so two builds can never silently corrupt each other's pose stream. The
  client surfaces the refusal in the menu and skips refused LAN hosts during
  discovery.
- Crash-proofing: every received payload is bounds-checked against its
  packet size before parsing (HELLO / PoseChunk / JOIN); both the client
  network thread and the relay thread are exception-guarded (a throw logs
  and keeps the loop alive instead of hitting std::terminate). The relay no
  longer creates client records for unknown-address POSE/LEAVE floods and
  enforces a hard client cap (--max-clients, default 64).
- Logging: rate-limited error logging throughout (send failures, resolve
  failures, refusals, thread exceptions, client-cap drops). The relay core
  uses a build-flag shim (REXLOG in-game, a tiny {} formatter in the
  dependency-free standalone), with --verbose join/leave lines for the
  server. Game crashes themselves are already handled by the SDK's crash
  handlers + file logs; this layer now avoids triggering them and explains
  itself when it can't connect.
- Settings persistence: server/room/name/port/rate save into the app's cvar
  config on change, so a player's multiplayer setup survives a restart.
- Menu: status now shows protocol refusals and the last error.

Tests updated for the handshake (version-refusal case added); all suites
pass.
2026-08-26 22:07:09 -04:00

85 lines
No EOL
2.8 KiB
C++

#pragma once
// Ghost multiplayer relay core: a small UDP forwarder shared by the
// standalone ghost_relay binary (run on a public VPS) and the in-game host
// mode (a player's game relays its LAN room). Clients connect OUTBOUND only
// and are matched into rooms by a shareable code. The relay forwards each
// client's PosePayload to every OTHER member of the same room.
//
// Clients are keyed by source address; every packet refreshes last_seen and
// silent clients are dropped (their ghosts vanish on the other side). Room
// codes are uppercase alphanumeric; a JOIN with an empty room gets one
// assigned by the server and returned in the HELLO.
#include <atomic>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "skate3_native_ghost_net.h"
#include "skate3_native_ghost_proto.h"
namespace skate3::native_ghost {
class GhostRelay {
public:
explicit GhostRelay(uint16_t port = kGhostDefaultPort) : port_(port) {}
~GhostRelay();
GhostRelay(const GhostRelay&) = delete;
GhostRelay& operator=(const GhostRelay&) = delete;
// Bind the socket and start the receive thread. Returns false when the
// port cannot be bound (already in use / no permission).
bool Start();
void Stop();
bool running() const { return running_.load(std::memory_order_relaxed); }
uint16_t port() const { return port_; }
size_t ClientCount() const;
// When non-empty, every client is forced into this room (the standalone
// binary's --room lock); client JOINs and PINGs cannot override it.
void SetLockedRoom(const char* room);
// Hard cap on simultaneously tracked clients (default 64). New JOIN/PING
// sources past the cap are dropped (logged).
void SetMaxClients(uint32_t max_clients) { max_clients_ = max_clients; }
// Verbose per-event logging (join/leave/refusals); the standalone server's
// --verbose flag. Off by default to keep a busy relay quiet.
void SetVerbose(bool verbose) { verbose_ = verbose; }
private:
void Run();
void HandlePacket(const GhostAddr& from, const PacketBuf& p, double now);
void Sweep(double now);
GhostSock sock_ = GHOST_INVALID_SOCKET;
uint16_t port_;
std::thread thread_;
std::atomic<bool> running_{false};
std::atomic<bool> stop_{false};
uint32_t max_clients_ = 64;
std::atomic<bool> verbose_{false};
struct Client {
GhostAddr addr;
uint32_t session_id = 0;
char room[kGhostMaxRoomLen] = {};
uint8_t room_len = 0;
char name[kGhostMaxNameLen] = {};
uint8_t name_len = 0;
bool refused = false; // version mismatch: poses from this client dropped
double last_seen = 0.0;
};
mutable std::mutex mu_;
std::vector<Client> clients_;
uint32_t next_session_ = 1;
bool locked_ = false;
char locked_room_[kGhostMaxRoomLen] = {};
uint8_t locked_room_len_ = 0;
};
} // namespace skate3::native_ghost