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).
99 lines
No EOL
3.1 KiB
C++
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;
|
|
} |