#include "game_server/InternalGameServer.h" #include "game_server/WebsocketGameClient.h" #include "game_server/WebsocketGameServer.h" #include #include #include #include #include namespace { void require(bool condition, const char* message) { if (condition) return; std::cerr << "WebsocketLockstepTests: " << message << '\n'; std::exit(EXIT_FAILURE); } template bool waitFor(Predicate predicate) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); while (std::chrono::steady_clock::now() < deadline) { if (predicate()) return true; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } return false; } } // namespace int main() { FRG::CampaignConfig campaign; campaign.id = "test_lockstep"; campaign.networkPlayerSlots = {"player", "player2"}; FRG::InternalGameServer authoritative; authoritative.startCampaign(campaign, {}, true, false); nlohmann::json initialState; authoritative.save(initialState); initialState["players"]["player2"] = initialState["players"]["player"]; authoritative.load(initialState); FRG::WebsocketGameServer server( authoritative, {"player", "player2"}, "lockstep-v2:test_lockstep", "127.0.0.1", 0); auto client = std::make_unique( "127.0.0.1", std::to_string(server.port()), "/"); require( waitFor([&]() { return client->connected(); }), "client did not connect"); require( waitFor([&]() { client->update(0); return client->initialized(); }), "client did not receive initial state"); require(client->localPlayerId() == "player", "wrong player slot assigned"); auto secondClient = std::make_unique( "127.0.0.1", std::to_string(server.port()), "/"); require( waitFor([&]() { secondClient->update(0); return secondClient->initialized(); }), "second client did not receive initial state"); require( secondClient->localPlayerId() == "player2", "second client received the wrong player slot"); FRG::WebsocketGameClient overflow( "127.0.0.1", std::to_string(server.port()), "/"); require( waitFor([&]() { overflow.update(0); return overflow.rejected(); }), "connection beyond configured slots was not rejected"); server.update(); require( waitFor([&]() { client->update(0); secondClient->update(0); return client->snapshot()->players.at("player2").state.enabled && secondClient->snapshot()->players.at("player").state.enabled; }) && authoritative.snapshot()->players.at("player").state.enabled && authoritative.snapshot()->players.at("player2").state.enabled, "connection presence was not broadcast to all replicas"); client->submit(FRG::SetCharacterDestinationCommand{ {}, "spoofed", Eigen::Vector3f(1.234f, 0.f, 2.345f), "ignored"}); std::this_thread::sleep_for(std::chrono::milliseconds(20)); server.update(); server.update(); require( waitFor([&]() { client->update(0); return client->snapshot()->revision > 1; }), "client did not consume finalized lockstep ticks"); secondClient.reset(); std::this_thread::sleep_for(std::chrono::milliseconds(50)); server.update(); require( waitFor([&]() { client->update(0); return !client->snapshot()->players.at("player2").state.enabled; }) && !authoritative.snapshot()->players.at("player2").state.enabled, "disconnect presence was not propagated"); FRG::WebsocketGameClient replacement( "127.0.0.1", std::to_string(server.port()), "/"); require( waitFor([&]() { replacement.update(0); return replacement.initialized(); }), "disconnected player slot was not reusable"); server.update(); require( waitFor([&]() { client->update(0); replacement.update(0); return client->snapshot()->players.at("player2").state.enabled; }) && authoritative.snapshot()->players.at("player2").state.enabled, "reconnected player was not enabled"); return EXIT_SUCCESS; }