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).
72 lines
No EOL
2.2 KiB
C++
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
|