232 lines
7.7 KiB
C++
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;
|
|
}
|