#include "LocationGameState.h" #include "game_server/InternalGameServer.h" #include #include #include 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(); 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"); } auto& hostileNpc = *state.npcs.at("npc"); hostileNpc.enabled = true; hostileNpc.canAttack = true; hostileNpc.hp = hostileNpc.initialHp = 75.f; hostileNpc.position = Eigen::Vector3f::Zero(); hostileNpc.homePosition = {5.f, 0.f, 0.f}; hostileNpc.stopInPlace(); auto playerOne = std::make_shared(); playerOne->configureAsPlayer(); playerOne->enabled = true; playerOne->position = {3.f, 0.f, 0.f}; auto playerTwo = std::make_shared(); playerTwo->configureAsPlayer(); playerTwo->enabled = true; playerTwo->position = {2.f, 0.f, 0.f}; FRG::PlayerStateMap combatPlayers{ {"player", playerOne}, {"player2", playerTwo}}; state.update(100, combatPlayers, false); if (hostileNpc.attackTargetId != "player2") { return fail("Hostile NPC did not select the nearest player"); } playerOne->position = {1.f, 0.f, 0.f}; playerTwo->position = {4.f, 0.f, 0.f}; state.update(100, combatPlayers, false); if (hostileNpc.attackTargetId != "player2") { return fail("Hostile NPC did not retain its living target lock"); } playerTwo->hp = 0.f; state.update(100, combatPlayers, false); if (hostileNpc.attackTargetId != "player") { return fail("Hostile NPC did not acquire a replacement after target death"); } playerOne->enabled = false; state.update(100, combatPlayers, false); if (!hostileNpc.attackTargetId.empty() || hostileNpc.battle_state != 0 || (hostileNpc.requestedWalkTarget - hostileNpc.homePosition).norm() > FRG::CharacterState::kWalkThreshold) { return fail("Hostile NPC did not return home after losing its target"); } 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(FRG::AnimationState::STAND); const auto walk = static_cast(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; }