Files
shadow-over-bishkek001/server/tests/WebsocketLockstepTests.cpp
T

232 lines
7.7 KiB
C++

#include "game_server/InternalGameServer.h"
#include "game_server/WebsocketGameClient.h"
#include "game_server/WebsocketGameServer.h"
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <thread>
namespace {
void require(bool condition, const char* message)
{
if (condition) return;
std::cerr << "WebsocketLockstepTests: " << message << '\n';
std::exit(EXIT_FAILURE);
}
template <class Predicate>
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"];
FRG::LocationGameState testLocation;
auto hostileNpc = std::make_shared<FRG::CharacterState>();
hostileNpc->enabled = true;
hostileNpc->hp = 200.f;
testLocation.npcs.emplace("hostile_npc", std::move(hostileNpc));
nlohmann::json testLocationState;
testLocation.save(testLocationState);
initialState["currentLocationId"] = "test_location";
initialState["locations"]["test_location"] = std::move(testLocationState);
authoritative.load(initialState);
FRG::WebsocketGameServer server(
authoritative, {"player", "player2"}, "lockstep-v2:test_lockstep",
"127.0.0.1", 0);
auto client = std::make_unique<FRG::WebsocketGameClient>(
"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<FRG::WebsocketGameClient>(
"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"});
secondClient->submit(FRG::SetCharacterDestinationCommand{
{}, "spoofed", Eigen::Vector3f(-1.234f, 0.f, 3.456f), "ignored"});
std::this_thread::sleep_for(std::chrono::milliseconds(50));
server.update();
server.update();
server.update();
const bool destinationsConverged = waitFor([&]() {
client->update(0);
secondClient->update(0);
const auto first = client->snapshot();
const auto second = secondClient->snapshot();
const auto host = authoritative.snapshot();
return first->players.at("player").state.requestedWalkTarget.isApprox(
Eigen::Vector3f(1.23f, 0.f, 2.35f)) &&
second->players.at("player2").state.requestedWalkTarget.isApprox(
Eigen::Vector3f(-1.23f, 0.f, 3.46f)) &&
first->players.at("player2").state.requestedWalkTarget.isApprox(
second->players.at("player2").state.requestedWalkTarget) &&
second->players.at("player").state.requestedWalkTarget.isApprox(
first->players.at("player").state.requestedWalkTarget) &&
host->players.at("player").state.requestedWalkTarget.isApprox(
first->players.at("player").state.requestedWalkTarget) &&
host->players.at("player2").state.requestedWalkTarget.isApprox(
second->players.at("player2").state.requestedWalkTarget);
});
if (!destinationsConverged) {
const auto first = client->snapshot();
const auto second = secondClient->snapshot();
const auto host = authoritative.snapshot();
std::cerr << "destination revisions " << first->revision << ", "
<< second->revision << ", " << host->revision << "\n"
<< "player targets "
<< first->players.at("player").state.requestedWalkTarget.transpose()
<< " / "
<< host->players.at("player").state.requestedWalkTarget.transpose()
<< "\nplayer2 targets "
<< second->players.at("player2").state.requestedWalkTarget.transpose()
<< " / "
<< host->players.at("player2").state.requestedWalkTarget.transpose()
<< std::endl;
}
require(
destinationsConverged,
"simultaneous destinations did not converge across lockstep replicas");
client->submit(FRG::SetCharacterTargetCommand{
"stale_location", "spoofed_character", "hostile_npc"});
std::this_thread::sleep_for(std::chrono::milliseconds(50));
server.update();
server.update();
server.update();
require(
waitFor([&]() {
client->update(0);
secondClient->update(0);
return client->snapshot()->players.at("player").state.attackTargetId ==
"hostile_npc" &&
secondClient->snapshot()->players.at("player").state.attackTargetId ==
"hostile_npc" &&
authoritative.snapshot()->players.at("player").state.attackTargetId ==
"hostile_npc";
}),
"target command was not delayed and rebroadcast");
client->submit(FRG::ClearCharacterAttackTargetCommand{
"stale_location", "spoofed_character"});
std::this_thread::sleep_for(std::chrono::milliseconds(50));
server.update();
server.update();
server.update();
require(
waitFor([&]() {
client->update(0);
secondClient->update(0);
return client->snapshot()->players.at("player").state.attackTargetId.empty() &&
secondClient->snapshot()->players.at("player").state.attackTargetId.empty() &&
authoritative.snapshot()->players.at("player").state.attackTargetId.empty();
}),
"clear attack target command was not delayed and rebroadcast");
client->submit(FRG::TryCharacterAttackComboCommand{
"stale_location", "spoofed_character"});
std::this_thread::sleep_for(std::chrono::milliseconds(50));
server.update();
server.update();
server.update();
require(
waitFor([&]() {
client->update(0);
secondClient->update(0);
return client->snapshot()->revision ==
secondClient->snapshot()->revision;
}),
"attack combo command was not rebroadcast in lockstep");
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;
}