#include "InternalGameServer.h" #include "../Campaign.h" #include "../content/LocationDefinition.h" #include #include #include namespace FRG { namespace { constexpr const char* kBattleZoneCampaignId = "battle_zone"; constexpr const char* kBattleZonePlayer2Id = "player2"; constexpr std::int64_t kBattleZoneTestBotIntervalMs = 5000; void configurePlayerAnimations( CharacterState& player, const std::string& zipFile) { player.creationInfo.animationPaths = { {AnimationState::STAND, "resources/w/gg/new2/gg_stand_idle001_small.anim"}, {AnimationState::WALK, "resources/w/gg/new2/gg_walk001_small.anim"}, {AnimationState::STAND_TO_ACTION, "resources/w/gg/new2/gg_stand_to_action001_small.anim"}, {AnimationState::ACTION_ATTACK, "resources/w/gg/new2/gg_action_chop001_small.anim"}, {AnimationState::ACTION_ATTACK_2, "resources/w/gg/new2/gg_action_stab001_small.anim"}, {AnimationState::ACTION_IDLE, "resources/w/gg/new2/gg_action_idle002_small.anim"}, {AnimationState::ACTION_TO_STAND, "resources/w/gg/new2/gg_action_to_stand001_small.anim"}, {AnimationState::ACTION_TO_DEATH, "resources/w/gg/new2/gg_die001_small.anim"}, {AnimationState::DEATH_IDLE, "resources/w/gg/new2/gg_die_idle001_small.anim"}}; loadCharacterAnimationMetadata(player, zipFile); } } InternalGameServer::InternalGameServer() = default; void InternalGameServer::ensureBattleZoneTestBot() { if (state_.campaignId != kBattleZoneCampaignId) return; if (!battleZoneTestBotEnabled_) { state_.players.erase(kBattleZonePlayer2Id); return; } auto& bot = state_.players[kBattleZonePlayer2Id]; if (!bot) bot = std::make_shared(); bot->configureAsPlayer(); configurePlayerAnimations(*bot, resourceZipFile_); bot->npcName = "Test Bot"; } void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs) { if (!battleZoneTestBotEnabled_ || state_.campaignId != kBattleZoneCampaignId || state_.currentLocationName.empty() || state_.players.find(kBattleZonePlayer2Id) == state_.players.end()) { return; } battleZoneTestBotElapsedMs_ += deltaMs; if (battleZoneTestBotElapsedMs_ < kBattleZoneTestBotIntervalMs) return; battleZoneTestBotElapsedMs_ %= kBattleZoneTestBotIntervalMs; static std::mt19937 randomEngine(std::random_device{}()); static std::uniform_real_distribution coordinate(-10.0f, 10.0f); submit(SetCharacterDestinationCommand{ state_.currentLocationName, kBattleZonePlayer2Id, Eigen::Vector3f(coordinate(randomEngine), 0.f, coordinate(randomEngine)), std::string()}); } void InternalGameServer::initializeCampaign( const std::string& campaignId, const std::string& questsPath, const std::string& zipFile) { CampaignConfig campaign; campaign.id = campaignId; campaign.questsPath = questsPath; startCampaign(campaign, zipFile, true, true); } void InternalGameServer::startCampaign( const CampaignConfig& campaign, const std::string& zipFile, bool createLocalPlayer, bool enableTestBot) { resourceZipFile_ = zipFile; networkPlayerSlots_ = campaign.networkPlayerSlots; battleZoneTestBotEnabled_ = enableTestBot; state_.campaignId = campaign.id; state_.currentLocationName.clear(); state_.locations.clear(); state_.players.clear(); state_.questJournal.loadFromFile(campaign.questsPath, zipFile); state_.globalFloats["player_hp"] = 200.f; if (createLocalPlayer) { auto player = std::make_shared(); player->configureAsPlayer(); configurePlayerAnimations(*player, zipFile); state_.players.emplace(localPlayerId_, std::move(player)); } battleZoneTestBotElapsedMs_ = 0; ensureBattleZoneTestBot(); publishMutation(); } void InternalGameServer::activateLocation(const std::string& locationId) { if (state_.locations.count(locationId) == 0) throw std::runtime_error( "Cannot activate unprepared location '" + locationId + "'"); state_.currentLocationName = locationId; publishMutation(); } void InternalGameServer::clearCampaign() { state_.currentLocationName.clear(); state_.players.clear(); state_.locations.clear(); networkPlayerSlots_.clear(); battleZoneTestBotElapsedMs_ = 0; publishMutation(); } void InternalGameServer::resetCampaign( const std::string& questsPath, const std::string& zipFile) { pendingEvents_.clear(); state_.currentLocationName.clear(); state_.isDarklands = false; state_.isNight = false; state_.isDawn = false; state_.playerHp = 200.f; state_.playerMaxHp = 200.f; state_.money = 5500; state_.globalInts.clear(); state_.globalFloats.clear(); state_.globalFloats["player_hp"] = 200.f; state_.tutorialStep = TutorialStep::AdvanceDialogue; state_.tutorialPhoneChatScreenOpened = false; state_.tutorialJournalScreenOpened = false; state_.tutorialNeedOpenTaxiScreen = false; state_.tutorialTaxiHintDone = false; for (int i = 0; i < 3; ++i) { state_.chatUnread[i] = true; state_.chatPreviewMsg[i].clear(); state_.chatHistory[i].clear(); } state_.inventory.clear(); state_.questJournal.loadFromFile(questsPath, zipFile); state_.locations.clear(); battleZoneTestBotElapsedMs_ = 0; ensureBattleZoneTestBot(); publishMutation(); } bool InternalGameServer::prepareLocation(const LocationConfig& config) { LocationGameState& authoritative = state_.locations[config.id]; const bool initializeLocation = authoritative.setup( LocationContentLoader::load(config, resourceZipFile_), resourceZipFile_); auto player = state_.players.find(localPlayerId_); if (player != state_.players.end() && player->second) { player->second->position = config.playerPosition; player->second->setTarget( config.playerPosition, std::string(), *authoritative.location, localPlayerId_); } publishMutation(); return initializeLocation; } GameSnapshot InternalGameServer::buildSnapshot() const { GameSnapshot result; result.revision = revision_; result.campaignId = state_.campaignId; result.currentLocationId = state_.currentLocationName; result.isDarklands = state_.isDarklands; result.isNight = state_.isNight; result.isDawn = state_.isDawn; result.playerHp = state_.playerHp; result.playerMaxHp = state_.playerMaxHp; result.money = state_.money; result.tutorialStep = state_.tutorialStep; result.tutorialPhoneChatScreenOpened = state_.tutorialPhoneChatScreenOpened; result.tutorialJournalScreenOpened = state_.tutorialJournalScreenOpened; result.tutorialNeedOpenTaxiScreen = state_.tutorialNeedOpenTaxiScreen; result.tutorialTaxiHintDone = state_.tutorialTaxiHintDone; result.taxiIsCalled = state_.taxiIsCalled; for (int i = 0; i < 3; ++i) { result.chatUnread[i] = state_.chatUnread[i]; result.chatPreview[i] = state_.chatPreviewMsg[i]; result.chatHistory[i].reserve(state_.chatHistory[i].size()); for (const StoredChatMessage& message : state_.chatHistory[i]) result.chatHistory[i].push_back({message.text, message.incoming}); } nlohmann::json inventoryJson; state_.inventory.save(inventoryJson); if (inventoryJson.contains("items") && inventoryJson["items"].is_array()) { for (const auto& item : inventoryJson["items"]) if (item.is_string()) result.inventoryItemIds.push_back(item.get()); } state_.questJournal.save(result.questJournal); result.globalInts = state_.globalInts; result.globalFloats = state_.globalFloats; for (const Quest::QuestState* quest : state_.questJournal.getVisibleQuests()) if (quest) result.quests.push_back(*quest); for (const auto& [playerId, source] : state_.players) { if (!source) continue; CharacterState clean = *source; result.players.emplace(playerId, CharacterSnapshot{playerId, std::move(clean)}); } for (const auto& [locationId, source] : state_.locations) { LocationSnapshot location; location.id = locationId; location.state = *source.location; for (const auto& [id, sourceNpc] : source.npcs) { if (!sourceNpc) continue; CharacterState clean = *sourceNpc; location.npcs.emplace( id, CharacterSnapshot{id, std::move(clean)}); } for (const auto& [id, sourceObject] : source.interactiveObjects) { if (!sourceObject) continue; InteractiveObjectState clean = *sourceObject; location.interactiveObjects.emplace( id, InteractiveObjectSnapshot{id, std::move(clean)}); } location.triggerZones = source.triggerZones; location.teleportZones = source.teleportZones; location.dialogue = source.dialogue; location.scriptGlobals = source.scriptGlobals; result.locations.emplace(locationId, std::move(location)); } return result; } std::shared_ptr InternalGameServer::snapshot() const { if (!cachedSnapshot_ || cachedRevision_ != revision_) { cachedSnapshot_ = std::make_shared(buildSnapshot()); cachedRevision_ = revision_; } return cachedSnapshot_; } void InternalGameServer::submit(const GameCommand& command) { auto enqueueQuestEvents = [this](Quest::QuestMutationResult result) { for (Quest::QuestEvent& event : result.events) pendingEvents_.emplace_back(std::move(event)); }; std::visit([this, &enqueueQuestEvents](const auto& value) { using T = std::decay_t; if constexpr (std::is_same_v) { state_.isDarklands = value.darklands; state_.isNight = value.night; state_.isDawn = value.dawn; } else if constexpr (std::is_same_v) { state_.currentLocationName = value.locationId; } else if constexpr (std::is_same_v) { state_.playerHp = value.hp; state_.playerMaxHp = value.maxHp; } else if constexpr (std::is_same_v) { state_.money = value.amount; } else if constexpr (std::is_same_v) { state_.money -= value.amount; } else if constexpr (std::is_same_v) { state_.completeTutorialStep(value.step); } else if constexpr (std::is_same_v) { state_.tutorialStep = value.step; } else if constexpr (std::is_same_v) { state_.tutorialPhoneChatScreenOpened = value.phoneChatOpened; state_.tutorialJournalScreenOpened = value.journalOpened; state_.tutorialNeedOpenTaxiScreen = value.needTaxiHint; state_.tutorialTaxiHintDone = value.taxiHintDone; } else if constexpr (std::is_same_v) { if (value.index >= 0 && value.index < 3) { state_.chatUnread[value.index] = value.unread; state_.chatPreviewMsg[value.index] = value.preview; } } else if constexpr (std::is_same_v) { if (value.index >= 0 && value.index < 3) { if (state_.chatHistory[value.index].size() >= 5) state_.chatHistory[value.index].erase(state_.chatHistory[value.index].begin()); state_.chatHistory[value.index].push_back({value.text, value.incoming}); } } else if constexpr (std::is_same_v) { state_.taxiIsCalled = value.called; } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); if (location != state_.locations.end()) location->second.startTaxi(); } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); if (location != state_.locations.end()) location->second.resetTaxi(); } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); if (location != state_.locations.end() && location->second.location) { location->second.location->targetInteractiveObjectId = value.interactiveObjectId; location->second.location->targetInteractNpcId = value.npcId; location->second.location->targetTeleportZoneId = value.teleportZoneId; } } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); if (location != state_.locations.end() && location->second.location && value.index >= 0 && value.index < static_cast( location->second.location->navigationMaps.size())) { LocationNetworkState& locationState = *location->second.location; locationState.activeNavigationIndex = value.index; for (const auto& [id, player] : state_.players) if (player) player->forceReplan(locationState, id); for (const auto& [id, npc] : location->second.npcs) if (npc) npc->forceReplan(locationState, id); } } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); if (location != state_.locations.end() && location->second.location) { LocationNetworkState& locationState = *location->second.location; const int index = locationState.activeNavigationIndex; if (index >= 0 && index < static_cast(locationState.navigationMaps.size())) { locationState.navigationMaps[index].setAreaAvailable( value.areaName, value.available); } } } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); if (location != state_.locations.end() && location->second.location) { LocationNetworkState& locationState = *location->second.location; const int index = locationState.activeNavigationIndex; if (index >= 0 && index < static_cast(locationState.navigationMaps.size())) { PathFinder::ObstaclePolygon polygon; polygon.name = value.name; polygon.polygon = value.polygon; locationState.navigationMaps[index].addObstaclePolygon(polygon); } } } else if constexpr (std::is_same_v) { if (state_.inventory.addItem(value.itemId)) pendingEvents_.emplace_back(InventoryItemAddedEvent{value.itemId}); } else if constexpr (std::is_same_v) { if (state_.inventory.removeItem(value.itemId)) pendingEvents_.emplace_back(InventoryItemRemovedEvent{value.itemId}); } else if constexpr (std::is_same_v) { enqueueQuestEvents(state_.questJournal.unlockQuest(value.questId)); } else if constexpr (std::is_same_v) { enqueueQuestEvents(state_.questJournal.completeQuest(value.questId)); } else if constexpr (std::is_same_v) { enqueueQuestEvents(state_.questJournal.failQuest(value.questId)); } else if constexpr (std::is_same_v) { enqueueQuestEvents(state_.questJournal.setObjectiveCompleted( value.questId, value.objectiveId, value.completed)); } else if constexpr (std::is_same_v) { enqueueQuestEvents(state_.questJournal.setObjectiveVisible( value.questId, value.objectiveId, value.visible)); } else if constexpr (std::is_same_v) { enqueueQuestEvents(state_.questJournal.setActiveObjective( value.questId, value.objectiveIndex)); } else if constexpr (std::is_same_v) { state_.globalInts[value.key] = value.value; } else if constexpr (std::is_same_v) { state_.globalFloats[value.key] = value.value; } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) { character->position = value.position; character->facingAngle = value.facingAngle; character->targetFacingAngle = value.facingAngle; } } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->attackTargetId = value.targetId; } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->attackTargetId.clear(); } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character && location != state_.locations.end() && location->second.location) { character->setTarget( value.target, value.callbackName, *location->second.location, value.characterId); } } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->stopInPlace(); } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->stopAndRotateToTarget(value.targetId); } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character && location != state_.locations.end() && location->second.location) { character->forceReplan( *location->second.location, value.characterId); } } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->resetPlayerAfterDeath(); } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->tryAttackCombo(); } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->setHp(value.hp); } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) { character->setAnimationMetadata( value.animationState, value.startFrame, value.frameCount); } } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->hp = (std::max)(0.f, character->hp - (std::max)(0.f, value.amount)); } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) *character = value.state; } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); if (location == state_.locations.end()) return; auto object = location->second.interactiveObjects.find(value.objectId); if (object != location->second.interactiveObjects.end() && object->second) object->second->isActive = value.active; } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); if (location != state_.locations.end()) { location->second.triggerZones[value.zoneId].enabled = value.enabled; pendingTriggerZoneUpdates_.emplace_back( value.locationId, value.zoneId); } } else if constexpr (std::is_same_v) { state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile); } }, command); publishMutation(); } std::vector InternalGameServer::drainEvents() { std::vector result; result.swap(pendingEvents_); return result; } void InternalGameServer::update(std::int64_t deltaMs) { pendingSimulationLocationId_.clear(); pendingSimulationEvents_ = {}; auto stored = state_.locations.find(state_.currentLocationName); if (stored == state_.locations.end()) return; updateBattleZoneTestBot(deltaMs); const bool interactiveObjectsLocked = stored->second.lockInteractiveObjectsDuringTutorial && state_.tutorialStep < TutorialStep::PickUpItem; pendingSimulationLocationId_ = state_.currentLocationName; pendingSimulationEvents_ = stored->second.update(deltaMs, state_.players, interactiveObjectsLocked); // Navigation recovery is a simulation consequence, not presentation input. // Applying it here makes every lockstep replica produce the same follow-up. for (const CharacterSimulationEvent& event : pendingSimulationEvents_.characters) { if (!event.events.navigationRequested) continue; CharacterState* character = state_.findCharacter( state_.currentLocationName, event.characterId); if (character && stored->second.location) { character->setTarget( event.events.navigationTarget, {}, *stored->second.location, event.characterId); } } publishMutation(); } void InternalGameServer::save(nlohmann::json& out) const { state_.save(out); } void InternalGameServer::load(const nlohmann::json& in) { pendingEvents_.clear(); state_.load(in); for (const std::string& playerId : networkPlayerSlots_) { auto player = state_.players.find(playerId); if (player == state_.players.end() || !player->second) continue; player->second->configureAsPlayer(); configurePlayerAnimations(*player->second, resourceZipFile_); } battleZoneTestBotElapsedMs_ = 0; ensureBattleZoneTestBot(); publishMutation(); } void InternalGameServer::publishMutation() { ++revision_; cachedSnapshot_.reset(); } } // namespace FRG