skate3recomp-multiplayer/tools/ghost_relay_main.cpp
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

99 lines
No EOL
3.1 KiB
C++

// ghost_relay: standalone ghost multiplayer relay server.
//
// Run on a public VPS (or your own machine behind a free tunnel like
// playit.gg / ngrok / Tailscale Funnel) so players connect OUTBOUND to it
// and need no port forwarding:
//
// ghost_relay [--port 40001] [--room CODE]
//
// --room optionally locks every client into one room (ignores JOIN rooms).
// The server is stateless across restarts: clients re-send their JOIN every
// ~5 s, so a redeployed relay re-gathers the room automatically.
//
// Build: cmake target `ghost_relay` (no rexglue dependency; plain sockets).
#include <chrono>
#include <cstdio>
#include <cstring>
#include <string>
#include <thread>
#include "skate3_native_ghost_relay.h"
#if !defined(_WIN32)
#include <csignal>
namespace {
volatile std::sig_atomic_t g_stop = 0;
void OnSignal(int) { g_stop = 1; }
} // namespace
#endif
int main(int argc, char** argv) {
uint16_t port = skate3::native_ghost::kGhostDefaultPort;
std::string locked_room;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
auto next = [&](const char* name) -> std::string {
if (i + 1 < argc) {
return argv[++i];
}
std::fprintf(stderr, "ghost_relay: %s requires a value\n", name);
std::exit(2);
};
if (arg == "--port") {
port = uint16_t(std::stoi(next("--port")));
} else if (arg == "--room") {
locked_room = next("--room");
} else if (arg == "--help" || arg == "-h") {
std::printf(
"ghost_relay - ghost multiplayer UDP relay\n"
"usage: ghost_relay [--port 40001] [--room CODE]\n"
" --port UDP port to listen on (clients connect here; allow this\n"
" port through any firewall, e.g. `ufw allow 40001/udp`)\n"
" --room optional: force every client into one room code\n");
return 0;
} else {
std::fprintf(stderr, "ghost_relay: unknown argument '%s'\n", arg.c_str());
return 2;
}
}
#if !defined(_WIN32)
std::signal(SIGINT, OnSignal);
std::signal(SIGTERM, OnSignal);
#endif
skate3::native_ghost::GhostRelay relay(port);
if (!locked_room.empty()) {
relay.SetLockedRoom(locked_room.c_str());
}
if (!relay.Start()) {
std::fprintf(stderr, "ghost_relay: failed to bind UDP port %u\n", port);
return 1;
}
const std::string lock_note =
locked_room.empty() ? "" : (" (locked room: " + locked_room + ")");
std::printf("ghost_relay: listening on UDP %u%s\n", port, lock_note.c_str());
std::printf("ghost_relay: clients connect outbound; no client-side port "
"forwarding needed\n");
// Status line every 5 s; on POSIX exit on SIGINT/SIGTERM.
auto last_log = std::chrono::steady_clock::now();
while (true) {
#if !defined(_WIN32)
if (g_stop != 0) {
break;
}
#endif
const auto now = std::chrono::steady_clock::now();
if (now - last_log >= std::chrono::seconds(5)) {
std::printf("ghost_relay: %zu client(s)\n", relay.ClientCount());
std::fflush(stdout);
last_log = now;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
relay.Stop();
std::printf("ghost_relay: stopped\n");
return 0;
}