#include "game_server/InternalGameServer.h" #include "game_server/NetworkProtocol.h" #if defined(NETWORK) #include "game_server/WebsocketGameServer.h" #endif #include "Campaign.h" #include "LocationState.h" #include "utils/Utils.h" #include #include #include #include #include #include #include #include #include #include #include namespace { std::atomic_bool stopRequested{false}; void requestStop(int) { stopRequested.store(true); } std::int64_t parsePositiveNumber(const char* value, const char* option) { try { const std::string text(value); std::size_t consumed = 0; const long long parsed = std::stoll(text, &consumed); if (consumed != text.size() || parsed <= 0) throw std::invalid_argument("not positive"); return static_cast(parsed); } catch (const std::exception&) { throw std::runtime_error( std::string("Invalid value for ") + option + ": " + value); } } } // namespace int main(int argc, char** argv) { std::int64_t tickMs = FRG::kSimulationTickMs; std::int64_t maxTicks = (std::numeric_limits::max)(); bool sleepBetweenTicks = true; std::string campaignId; std::string locationId; std::string campaignsPath = "resources/config/campaigns.json"; std::string listenAddress = "0.0.0.0"; std::int64_t listenPort = 8080; try { for (int index = 1; index < argc; ++index) { const std::string option(argv[index]); if (option == "--ticks" && index + 1 < argc) { maxTicks = parsePositiveNumber(argv[++index], "--ticks"); } else if (option == "--tick-ms" && index + 1 < argc) { tickMs = parsePositiveNumber(argv[++index], "--tick-ms"); } else if (option == "--no-sleep") { sleepBetweenTicks = false; } else if (option == "--campaign" && index + 1 < argc) { campaignId = argv[++index]; } else if (option == "--location" && index + 1 < argc) { locationId = argv[++index]; } else if (option == "--campaigns" && index + 1 < argc) { campaignsPath = argv[++index]; } else if (option == "--address" && index + 1 < argc) { listenAddress = argv[++index]; } else if (option == "--port" && index + 1 < argc) { listenPort = parsePositiveNumber(argv[++index], "--port"); if (listenPort > 65535) throw std::runtime_error("Invalid value for --port"); } else if (option == "--help") { std::cout << "Usage: GameServer [--ticks N] [--tick-ms N] " "[--no-sleep] [--campaign ID] [--location ID] " "[--campaigns PATH] [--address IP] [--port N]\n"; return EXIT_SUCCESS; } else { throw std::runtime_error("Unknown or incomplete option: " + option); } } } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return EXIT_FAILURE; } if (tickMs != FRG::kSimulationTickMs) { std::cerr << "Network lockstep requires --tick-ms " << FRG::kSimulationTickMs << '\n'; return EXIT_FAILURE; } std::signal(SIGINT, requestStop); std::signal(SIGTERM, requestStop); FRG::InternalGameServer server; std::vector playerSlots; std::string compatibilityFingerprint = "lockstep-v2"; if (!campaignId.empty()) { try { FRG::CampaignRegistry::instance().loadFromJson(campaignsPath); std::cout << "[server] Loading...\n" << std::flush; const FRG::CampaignConfig* campaign = FRG::CampaignRegistry::instance().find(campaignId); if (!campaign) throw std::runtime_error( "Unknown campaign '" + campaignId + "'"); const std::string selectedLocation = locationId.empty() ? campaign->startLocation : locationId; const FRG::LocationConfig* location = campaign->findLocation(selectedLocation); if (!location) throw std::runtime_error( "Campaign '" + campaignId + "' has no location '" + selectedLocation + "'"); server.startCampaign(*campaign, std::string(), true, false); server.prepareLocation(*location); const std::string startState = FRG::readTextFile(campaign->startStatePath); if (!startState.empty()) { try { nlohmann::json persisted = nlohmann::json::parse(startState); nlohmann::json networkState; FRG::ClientLocationState ignoredLocalState; FRG::loadSinglePlayerPersistence( persisted, networkState, ignoredLocalState); networkState["campaignId"] = campaignId; networkState["currentLocationId"] = selectedLocation; server.load(networkState); std::cout << "Loaded initial state '" << campaign->startStatePath << "'\n"; } catch (const std::exception& error) { std::cerr << "Ignoring invalid initial state '" << campaign->startStatePath << "': " << error.what() << '\n'; } } server.activateLocation(selectedLocation); playerSlots = campaign->networkPlayerSlots; for (const std::string& slot : playerSlots) { if (server.snapshot()->players.count(slot) == 0) throw std::runtime_error( "Multiplayer slot '" + slot + "' is missing from the initial state"); } compatibilityFingerprint = campaign->networkCompatibility.empty() ? "lockstep-v2:" + campaignId : campaign->networkCompatibility; if (!campaign->tutorial) { server.submit(FRG::SetTutorialStepCommand{ FRG::TutorialStep::Completed}); } std::cout << "Loaded campaign '" << campaignId << "', location '" << selectedLocation << "' with shared player '" << server.localPlayerId() << "'\n"; } catch (const std::exception& error) { std::cerr << "Server content setup failed: " << error.what() << '\n'; return EXIT_FAILURE; } } const auto tickDuration = std::chrono::milliseconds(tickMs); auto nextTick = std::chrono::steady_clock::now(); #if defined(NETWORK) std::unique_ptr websocketServer; try { websocketServer = std::make_unique( server, playerSlots, compatibilityFingerprint, listenAddress, static_cast(listenPort)); } catch (const std::exception& error) { std::cerr << "WebSocket listener setup failed: " << error.what() << '\n'; return EXIT_FAILURE; } std::cout << "GameServer listening on ws://" << listenAddress << ':' << websocketServer->port() << " (" << tickMs << " ms tick)\n"; #else std::cout << "GameServer started (network disabled, " << tickMs << " ms tick)\n"; #endif for (std::int64_t tick = 0; tick < maxTicks && !stopRequested.load(); ++tick) { nextTick += tickDuration; #if defined(NETWORK) websocketServer->update(); #else server.update(tickMs); #endif if (sleepBetweenTicks) std::this_thread::sleep_until(nextTick); } std::cout << "GameServer stopped\n"; return EXIT_SUCCESS; }