// 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 #include #include #include #include #include "skate3_native_ghost_relay.h" #if !defined(_WIN32) #include 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; }