235 lines
9.0 KiB
C++
235 lines
9.0 KiB
C++
#include "LocationGameState.h"
|
|
#include "game_server/InternalGameServer.h"
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
|
|
namespace {
|
|
|
|
int fail(const char* message)
|
|
{
|
|
std::cout << "LocationGameStateTests: " << message << std::endl;
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main()
|
|
{
|
|
FRG::ClientLocationState localState;
|
|
auto camera = localState.getOrCreate("test_location");
|
|
camera->cameraAzimuth = 0.75f;
|
|
camera->cameraInclination = 1.25f;
|
|
nlohmann::json localJson;
|
|
localState.save(localJson);
|
|
FRG::ClientLocationState restoredLocalState;
|
|
restoredLocalState.load(localJson);
|
|
const auto restoredCamera =
|
|
restoredLocalState.getOrCreate("test_location");
|
|
if (restoredCamera->cameraAzimuth != 0.75f ||
|
|
restoredCamera->cameraInclination != 1.25f) {
|
|
return fail("Local camera state did not round trip");
|
|
}
|
|
|
|
FRG::LocationNetworkState networkLocationState;
|
|
nlohmann::json networkLocationJson;
|
|
networkLocationState.save(networkLocationJson);
|
|
if (networkLocationJson.contains("cameraAzimuth") ||
|
|
networkLocationJson.contains("cameraInclination")) {
|
|
return fail("Network location state contains local camera fields");
|
|
}
|
|
|
|
const nlohmann::json authoritativeJson{
|
|
{"schemaVersion", FRG::GameState::kSchemaVersion},
|
|
{"campaignId", "test"}
|
|
};
|
|
nlohmann::json persistenceJson;
|
|
FRG::saveSinglePlayerPersistence(
|
|
authoritativeJson, localState, persistenceJson);
|
|
nlohmann::json restoredAuthoritativeJson;
|
|
FRG::ClientLocationState restoredPersistenceLocalState;
|
|
FRG::loadSinglePlayerPersistence(
|
|
persistenceJson, restoredAuthoritativeJson,
|
|
restoredPersistenceLocalState);
|
|
if (restoredAuthoritativeJson != authoritativeJson ||
|
|
restoredPersistenceLocalState.getOrCreate("test_location")
|
|
->cameraAzimuth != 0.75f) {
|
|
return fail("Composed single-player state did not round trip");
|
|
}
|
|
if (persistenceJson.at("networkState").contains("localState")) {
|
|
return fail("Local state leaked into authoritative state");
|
|
}
|
|
|
|
const std::filesystem::path repositoryRoot =
|
|
std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
|
|
std::filesystem::current_path(repositoryRoot);
|
|
|
|
FRG::CampaignRegistry::instance().loadFromJson(
|
|
"resources/config/campaigns.json");
|
|
const FRG::CampaignConfig* singlePlayerBattleZone =
|
|
FRG::CampaignRegistry::instance().find("battle_zone");
|
|
const FRG::CampaignConfig* multiplayerBattleZoneConfig =
|
|
FRG::CampaignRegistry::instance().find("battle_zone_multiplayer");
|
|
if (FRG::CampaignRegistry::instance().all().size() != 3 ||
|
|
!singlePlayerBattleZone || !multiplayerBattleZoneConfig) {
|
|
return fail("Expected three campaigns including both Battle Zone modes");
|
|
}
|
|
if (singlePlayerBattleZone->startStatePath !=
|
|
multiplayerBattleZoneConfig->startStatePath) {
|
|
return fail("Battle Zone modes do not share their initial state");
|
|
}
|
|
if (multiplayerBattleZoneConfig->networkPlayerSlots.size() != 2 ||
|
|
multiplayerBattleZoneConfig->networkPlayerSlots[0] != "player" ||
|
|
multiplayerBattleZoneConfig->networkPlayerSlots[1] !=
|
|
"player2" ||
|
|
multiplayerBattleZoneConfig->networkCompatibility.empty()) {
|
|
return fail("Multiplayer player slots were not loaded");
|
|
}
|
|
|
|
FRG::LocationDefinition definition;
|
|
definition.config.id = "test_location";
|
|
|
|
FRG::NpcData npc;
|
|
npc.id = "npc";
|
|
npc.name = "Test NPC";
|
|
npc.hp = 75.f;
|
|
definition.npcs.push_back(npc);
|
|
|
|
FRG::InteractiveObjectData object;
|
|
object.id = "object";
|
|
object.base.name = "Test Object";
|
|
object.base.meshPath = "unused.mesh";
|
|
object.isActive = true;
|
|
definition.interactiveObjects.push_back(object);
|
|
|
|
FRG::TriggerZoneDefinition trigger;
|
|
trigger.id = "trigger";
|
|
trigger.enabled = true;
|
|
definition.triggerZones.push_back(trigger);
|
|
|
|
FRG::TeleportDefinition teleport;
|
|
teleport.id = "teleport";
|
|
teleport.active = false;
|
|
definition.teleports.push_back(teleport);
|
|
|
|
FRG::LocationGameState state;
|
|
if (!state.setup(definition, {}))
|
|
return fail("First setup must initialize runtime fields");
|
|
if (state.npcs.size() != 1 || state.interactiveObjects.size() != 1)
|
|
return fail("Definitions were not applied");
|
|
|
|
state.npcs.at("npc")->hp = 12.f;
|
|
state.interactiveObjects.at("object")->isActive = false;
|
|
state.triggerZones.at("trigger").enabled = false;
|
|
state.teleportZones.at("teleport") = true;
|
|
state.hasPersistentState = true;
|
|
|
|
if (state.setup(definition, {}))
|
|
return fail("Persistent setup must not reinitialize runtime fields");
|
|
if (state.npcs.at("npc")->hp != 12.f ||
|
|
state.interactiveObjects.at("object")->isActive ||
|
|
state.triggerZones.at("trigger").enabled ||
|
|
!state.teleportZones.at("teleport")) {
|
|
return fail("Repeated setup did not preserve runtime state");
|
|
}
|
|
|
|
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";
|
|
battleZone.questsPath = "resources/config/quests_battle_zone.json";
|
|
FRG::InternalGameServer server;
|
|
server.startCampaign(battleZone, {}, false, true);
|
|
const auto snapshot = server.snapshot();
|
|
const auto bot = snapshot->players.find("player2");
|
|
if (bot == snapshot->players.end())
|
|
return fail("Battle Zone test bot was not created");
|
|
const auto stand = static_cast<std::size_t>(FRG::AnimationState::STAND);
|
|
const auto walk = static_cast<std::size_t>(FRG::AnimationState::WALK);
|
|
if (bot->second.state.animationFrameCounts[stand] <= 1 ||
|
|
bot->second.state.animationFrameCounts[walk] <= 1) {
|
|
return fail("Battle Zone test bot animation metadata was not loaded");
|
|
}
|
|
|
|
FRG::InternalGameServer networkServer;
|
|
FRG::CampaignConfig multiplayerBattleZone = battleZone;
|
|
multiplayerBattleZone.id = "battle_zone_multiplayer";
|
|
multiplayerBattleZone.networkPlayerSlots = {
|
|
"player", "player2"};
|
|
networkServer.startCampaign(multiplayerBattleZone, {}, true, false);
|
|
const auto networkSnapshot = networkServer.snapshot();
|
|
if (networkSnapshot->campaignId != "battle_zone_multiplayer")
|
|
return fail("Network campaign ID was not retained");
|
|
if (networkSnapshot->players.count("player") != 1)
|
|
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);
|
|
loadedNetworkState["players"]["player2"] =
|
|
loadedNetworkState["players"]["player"];
|
|
loadedNetworkState["players"]["player2"]["position"] =
|
|
{4.f, 0.f, 5.f};
|
|
networkServer.load(loadedNetworkState);
|
|
const auto loadedNetworkSnapshot = networkServer.snapshot();
|
|
const auto loadedNetworkBot =
|
|
loadedNetworkSnapshot->players.find("player2");
|
|
if (loadedNetworkBot == loadedNetworkSnapshot->players.end() ||
|
|
loadedNetworkBot->second.state.animationFrameCounts[stand] <= 1 ||
|
|
loadedNetworkBot->second.state.animationFrameCounts[walk] <= 1) {
|
|
return fail(
|
|
"Loaded multiplayer slot did not restore animation metadata");
|
|
}
|
|
if (!loadedNetworkBot->second.state.position.isApprox(
|
|
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;
|
|
}
|