skate3recomp-multiplayer/src/native/skate3_native_ghost_relay.h
MiTHRAL 269fbcee32 Add ghost multiplayer: UDP relay + pose broadcasting + placeholder cubes
Bolt-on multiplayer layer confined to src/native and hand-written code (no
codegen output touched):

- GhostRelay core (src/native/skate3_native_ghost_relay.*): small UDP relay
  matching clients into rooms by a shareable code; forwards each client's
  pose to the other room members. Used by both the standalone binary and
  the in-game LAN host fallback.
- Standalone server (tools/ghost_relay_main.cpp, ghost_relay target): runs
  on a public VPS or behind a free tunnel; clients connect OUTBOUND, so no
  port forwarding is needed on any client.
- In-game module (skate3_native_ghost.*): captures the local player's world
  transform at Sk8::UpdateBoneTransforms exit (pack-owner entity +416,
  verified layout) and broadcasts it at a fixed rate; auto-connects to the
  default public relay, falls back to LAN auto-host + UDP broadcast
  discovery when unreachable.
- Renderer: draws a solid-color cube per remote player in RenderScene via
  the existing scene PSO tint.a>0 branch (no new shader/SPIR-V).
- UI: GhostDialog (M key) with enable toggle, room code, relay override,
  status readout. Off by default.

Wire protocol in skate3_native_ghost_proto.h (JOIN/HELLO/POSE/PING/LEAVE).
2026-08-26 20:12:45 -04:00

72 lines
No EOL
2.2 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);
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};
struct Client {
GhostAddr addr;
uint32_t session_id = 0;
char room[kGhostMaxRoomLen] = {};
uint8_t room_len = 0;
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