535 lines
21 KiB
C++
535 lines
21 KiB
C++
#include "InternalGameServer.h"
|
|
#include "../Campaign.h"
|
|
#include "../content/LocationDefinition.h"
|
|
#include <algorithm>
|
|
#include <random>
|
|
#include <type_traits>
|
|
|
|
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<CharacterState>();
|
|
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<float> 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<CharacterState>();
|
|
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<std::string>());
|
|
}
|
|
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<const GameSnapshot> InternalGameServer::snapshot() const
|
|
{
|
|
if (!cachedSnapshot_ || cachedRevision_ != revision_) {
|
|
cachedSnapshot_ = std::make_shared<const GameSnapshot>(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<decltype(value)>;
|
|
if constexpr (std::is_same_v<T, SetWorldModeCommand>) {
|
|
state_.isDarklands = value.darklands;
|
|
state_.isNight = value.night;
|
|
state_.isDawn = value.dawn;
|
|
} else if constexpr (std::is_same_v<T, SetCurrentLocationCommand>) {
|
|
state_.currentLocationName = value.locationId;
|
|
} else if constexpr (std::is_same_v<T, SetPlayerHealthCommand>) {
|
|
state_.playerHp = value.hp;
|
|
state_.playerMaxHp = value.maxHp;
|
|
} else if constexpr (std::is_same_v<T, SetMoneyCommand>) {
|
|
state_.money = value.amount;
|
|
} else if constexpr (std::is_same_v<T, SpendMoneyCommand>) {
|
|
state_.money -= value.amount;
|
|
} else if constexpr (std::is_same_v<T, CompleteTutorialStepCommand>) {
|
|
state_.completeTutorialStep(value.step);
|
|
} else if constexpr (std::is_same_v<T, SetTutorialStepCommand>) {
|
|
state_.tutorialStep = value.step;
|
|
} else if constexpr (std::is_same_v<T, SetTutorialFlagsCommand>) {
|
|
state_.tutorialPhoneChatScreenOpened = value.phoneChatOpened;
|
|
state_.tutorialJournalScreenOpened = value.journalOpened;
|
|
state_.tutorialNeedOpenTaxiScreen = value.needTaxiHint;
|
|
state_.tutorialTaxiHintDone = value.taxiHintDone;
|
|
} else if constexpr (std::is_same_v<T, SetChatCommand>) {
|
|
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<T, AppendChatMessageCommand>) {
|
|
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<T, SetTaxiCalledCommand>) {
|
|
state_.taxiIsCalled = value.called;
|
|
} else if constexpr (std::is_same_v<T, StartTaxiCommand>) {
|
|
auto location = state_.locations.find(value.locationId);
|
|
if (location != state_.locations.end())
|
|
location->second.startTaxi();
|
|
} else if constexpr (std::is_same_v<T, ResetTaxiCommand>) {
|
|
auto location = state_.locations.find(value.locationId);
|
|
if (location != state_.locations.end())
|
|
location->second.resetTaxi();
|
|
} else if constexpr (std::is_same_v<T, SetLocationTargetsCommand>) {
|
|
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<T, SwitchLocationNavigationCommand>) {
|
|
auto location = state_.locations.find(value.locationId);
|
|
if (location != state_.locations.end() && location->second.location &&
|
|
value.index >= 0 &&
|
|
value.index < static_cast<int>(
|
|
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<T, SetNavigationAreaAvailableCommand>) {
|
|
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<int>(locationState.navigationMaps.size())) {
|
|
locationState.navigationMaps[index].setAreaAvailable(
|
|
value.areaName, value.available);
|
|
}
|
|
}
|
|
} else if constexpr (std::is_same_v<T, AddNavigationObstacleCommand>) {
|
|
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<int>(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<T, AddInventoryItemCommand>) {
|
|
if (state_.inventory.addItem(value.itemId))
|
|
pendingEvents_.emplace_back(InventoryItemAddedEvent{value.itemId});
|
|
} else if constexpr (std::is_same_v<T, RemoveInventoryItemCommand>) {
|
|
if (state_.inventory.removeItem(value.itemId))
|
|
pendingEvents_.emplace_back(InventoryItemRemovedEvent{value.itemId});
|
|
} else if constexpr (std::is_same_v<T, UnlockQuestCommand>) {
|
|
enqueueQuestEvents(state_.questJournal.unlockQuest(value.questId));
|
|
} else if constexpr (std::is_same_v<T, CompleteQuestCommand>) {
|
|
enqueueQuestEvents(state_.questJournal.completeQuest(value.questId));
|
|
} else if constexpr (std::is_same_v<T, FailQuestCommand>) {
|
|
enqueueQuestEvents(state_.questJournal.failQuest(value.questId));
|
|
} else if constexpr (std::is_same_v<T, SetQuestObjectiveCompletedCommand>) {
|
|
enqueueQuestEvents(state_.questJournal.setObjectiveCompleted(
|
|
value.questId, value.objectiveId, value.completed));
|
|
} else if constexpr (std::is_same_v<T, SetQuestObjectiveVisibleCommand>) {
|
|
enqueueQuestEvents(state_.questJournal.setObjectiveVisible(
|
|
value.questId, value.objectiveId, value.visible));
|
|
} else if constexpr (std::is_same_v<T, SetQuestActiveObjectiveCommand>) {
|
|
enqueueQuestEvents(state_.questJournal.setActiveObjective(
|
|
value.questId, value.objectiveIndex));
|
|
} else if constexpr (std::is_same_v<T, SetGlobalIntCommand>) {
|
|
state_.globalInts[value.key] = value.value;
|
|
} else if constexpr (std::is_same_v<T, SetGlobalFloatCommand>) {
|
|
state_.globalFloats[value.key] = value.value;
|
|
} else if constexpr (std::is_same_v<T, SetCharacterTransformCommand>) {
|
|
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<T, SetCharacterTargetCommand>) {
|
|
CharacterState* character =
|
|
state_.findCharacter(value.locationId, value.characterId);
|
|
if (character) character->attackTargetId = value.targetId;
|
|
} else if constexpr (std::is_same_v<T, ClearCharacterAttackTargetCommand>) {
|
|
CharacterState* character =
|
|
state_.findCharacter(value.locationId, value.characterId);
|
|
if (character) character->attackTargetId.clear();
|
|
} else if constexpr (std::is_same_v<T, SetCharacterDestinationCommand>) {
|
|
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<T, StopCharacterCommand>) {
|
|
CharacterState* character =
|
|
state_.findCharacter(value.locationId, value.characterId);
|
|
if (character) character->stopInPlace();
|
|
} else if constexpr (std::is_same_v<T, FaceCharacterCommand>) {
|
|
CharacterState* character =
|
|
state_.findCharacter(value.locationId, value.characterId);
|
|
if (character)
|
|
character->stopAndRotateToTarget(value.targetId);
|
|
} else if constexpr (std::is_same_v<T, ReplanCharacterCommand>) {
|
|
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<T, ResetCharacterAfterDeathCommand>) {
|
|
CharacterState* character =
|
|
state_.findCharacter(value.locationId, value.characterId);
|
|
if (character) character->resetPlayerAfterDeath();
|
|
} else if constexpr (std::is_same_v<T, TryCharacterAttackComboCommand>) {
|
|
CharacterState* character =
|
|
state_.findCharacter(value.locationId, value.characterId);
|
|
if (character) character->tryAttackCombo();
|
|
} else if constexpr (std::is_same_v<T, SetCharacterHealthCommand>) {
|
|
CharacterState* character =
|
|
state_.findCharacter(value.locationId, value.characterId);
|
|
if (character) character->setHp(value.hp);
|
|
} else if constexpr (std::is_same_v<T, SetCharacterAnimationMetadataCommand>) {
|
|
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<T, DamageCharacterCommand>) {
|
|
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<T, CommitCharacterStateCommand>) {
|
|
CharacterState* character =
|
|
state_.findCharacter(value.locationId, value.characterId);
|
|
if (character) *character = value.state;
|
|
} else if constexpr (std::is_same_v<T, SetInteractiveObjectActiveCommand>) {
|
|
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<T, SetTriggerZoneEnabledCommand>) {
|
|
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<T, ReloadQuestDefinitionsCommand>) {
|
|
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
|
|
}
|
|
}, command);
|
|
publishMutation();
|
|
}
|
|
|
|
std::vector<GameServerEvent> InternalGameServer::drainEvents()
|
|
{
|
|
std::vector<GameServerEvent> 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
|