player connect-disconnect

This commit is contained in:
Vladislav Khorev committed 2026-11-06 22:07:47 +06:00
1 parent 3f68a8ba07
commit 2e6b93f3ac
16 files changed
+305 -62

No files matched your search

+43
View File
@@ -135,6 +135,18 @@ int main()
FRG::PlayerStateMap noPlayers;
state.update(50, noPlayers, false);
auto disabledPlayer = std::make_shared<FRG::CharacterState>();
disabledPlayer->configureAsPlayer();
disabledPlayer->enabled = false;
state.location->targetInteractiveObjectId = "object";
state.location->targetInteractNpcId = "npc";
FRG::PlayerStateMap disabledPlayers{{"player", disabledPlayer}};
const FRG::LocationSimulationEvents disabledEvents =
state.update(50, disabledPlayers, false);
if (!disabledEvents.interactiveObjects.empty() ||
!disabledEvents.npcInteractionId.empty()) {
return fail("Disabled player interacted with location entities");
}
FRG::CampaignConfig battleZone;
battleZone.id = "battle_zone";
@@ -165,6 +177,8 @@ int main()
return fail("Network campaign shared player was not created");
if (networkSnapshot->players.count("player2") != 0)
return fail("Network campaign unexpectedly created the test bot");
if (networkSnapshot->players.at("player").state.enabled)
return fail("Network campaign player slot was enabled before connection");
nlohmann::json loadedNetworkState;
networkServer.save(loadedNetworkState);
@@ -186,6 +200,35 @@ int main()
Eigen::Vector3f(4.f, 0.f, 5.f))) {
return fail("Loaded multiplayer slot position was overwritten");
}
if (loadedNetworkBot->second.state.enabled)
return fail("Loaded multiplayer slot was enabled before connection");
const std::string locationId = loadedNetworkSnapshot->currentLocationId;
networkServer.submit(FRG::SetCharacterPositionCommand{
locationId, "player2", Eigen::Vector3f(7.f, 0.f, 8.f), true});
networkServer.submit(FRG::SetCharacterFacingAngleCommand{
locationId, "player2", 1.5f, true});
networkServer.submit(FRG::SetCharacterFaceTargetCommand{
locationId, "player2", "player"});
networkServer.submit(FRG::SetCharacterEnabledCommand{
locationId, "player2", false});
const auto commandSnapshot = networkServer.snapshot();
const auto commandCharacter = commandSnapshot->players.at("player2").state;
if (!commandCharacter.position.isApprox(Eigen::Vector3f(7.f, 0.f, 8.f)) ||
!commandCharacter.homePosition.isApprox(Eigen::Vector3f(7.f, 0.f, 8.f)) ||
commandCharacter.facingAngle != 1.5f ||
commandCharacter.targetFacingAngle != 1.5f ||
!commandCharacter.faceTargetId.empty() ||
commandCharacter.enabled) {
return fail("Semantic character commands were not applied");
}
networkServer.submit(FRG::SetCharacterEnabledCommand{
locationId, "player2", true});
const auto enabledPlayer =
networkServer.snapshot()->players.at("player2").state;
if (!enabledPlayer.enabled || !enabledPlayer.attackTargetId.empty()) {
return fail("Enabled player acquired an autonomous attack target");
}
return EXIT_SUCCESS;
}
+21 -4
View File
@@ -62,7 +62,10 @@ int main()
FRG::TickMessage tick;
tick.tickId = 43;
tick.commands.push_back({1, "player", parsedInput.target});
tick.commands.push_back(FRG::ScheduledDestinationCommand{
1, "player", parsedInput.target});
tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{
2, "uni_exterior", "player2", true});
tick.stateHash = "0123456789abcdef";
FRG::TickMessage parsedTick;
require(
@@ -70,7 +73,11 @@ int main()
FRG::makeTickMessage(tick), parsedTick, error),
"tick message did not round trip");
require(
parsedTick.tickId == 43 && parsedTick.commands.size() == 1,
parsedTick.tickId == 43 && parsedTick.commands.size() == 2 &&
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
parsedTick.commands[1]) &&
std::get<FRG::ScheduledCharacterEnabledCommand>(
parsedTick.commands[1]).enabled,
"tick contents did not round trip");
require(
@@ -120,9 +127,19 @@ int main()
const FRG::TickMessage commandTick = timeline.advance();
require(
commandTick.tickId == 2 && commandTick.commands.size() == 2 &&
commandTick.commands[0].commandSequence == firstSequence &&
commandTick.commands[1].commandSequence == secondSequence,
std::get<FRG::ScheduledDestinationCommand>(
commandTick.commands[0]).commandSequence == firstSequence &&
std::get<FRG::ScheduledDestinationCommand>(
commandTick.commands[1]).commandSequence == secondSequence,
"scheduled commands were not finalized in stable order");
timeline.scheduleCharacterEnabled("uni_exterior", "player2", true);
const FRG::TickMessage presenceTick = timeline.advance();
require(
presenceTick.commands.size() == 1 &&
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
presenceTick.commands[0]),
"character enabled command was not scheduled for the next tick");
return EXIT_SUCCESS;
}
+49 -2
View File
@@ -34,11 +34,16 @@ 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"}, "lockstep-v2:test_lockstep",
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()), "/");
@@ -54,6 +59,18 @@ int main()
"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(
@@ -63,6 +80,18 @@ int main()
}),
"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));
@@ -75,8 +104,17 @@ int main()
}),
"client did not consume finalized lockstep ticks");
client.reset();
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(
@@ -85,6 +123,15 @@ int main()
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;
}