More work on states, added bot

This commit is contained in:
Vladislav Khorev committed 2026-10-29 22:37:30 +06:00
1 parent 66e93d46f4
commit 5e9a663812
25 files changed
+1595 -740

No files matched your search

+279 -8
View File
@@ -1,12 +1,192 @@
#include "InternalGameServer.h"
#include "../Campaign.h"
#include "../Location.h"
#include <algorithm>
#include <random>
#include <type_traits>
namespace FRG {
namespace {
// Set this to false to disable the battle-zone remote-player test bot.
constexpr bool kEnableBattleZoneTestBot = true;
constexpr const char* kBattleZoneCampaignId = "battle_zone";
constexpr const char* kBattleZoneTestBotId = "battle_zone_test_bot";
constexpr std::int64_t kBattleZoneTestBotIntervalMs = 5000;
}
InternalGameServer::InternalGameServer() = default;
void InternalGameServer::ensureBattleZoneTestBot()
{
if (state_.campaignId != kBattleZoneCampaignId) return;
if (!kEnableBattleZoneTestBot) {
state_.players.erase(kBattleZoneTestBotId);
return;
}
auto& bot = state_.players[kBattleZoneTestBotId];
if (!bot) bot = std::make_shared<CharacterState>();
bot->configureAsPlayer();
bot->npcName = "Test Bot";
}
void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
{
if (!kEnableBattleZoneTestBot ||
state_.campaignId != kBattleZoneCampaignId ||
state_.currentLocationName.empty() ||
state_.players.find(kBattleZoneTestBotId) == 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,
kBattleZoneTestBotId,
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)
{
state_.campaignId = campaignId;
state_.currentLocationName.clear();
state_.locations.clear();
state_.players.clear();
state_.questJournal.loadFromFile(questsPath, zipFile);
state_.globalFloats["player_hp"] = 200.f;
auto player = std::make_shared<CharacterState>();
player->configureAsPlayer();
state_.players.emplace(localPlayerId_, std::move(player));
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
publishMutation();
}
void InternalGameServer::clearCampaign()
{
runtimeLocations_.clear();
state_.currentLocationName.clear();
state_.players.clear();
state_.locations.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.hasPersistentState;
if (!authoritative.location)
authoritative.location = std::make_shared<LocationState>();
if (authoritative.location->navigationMaps.empty())
authoritative.location->initNavigation(config.navigationJsonPaths);
authoritative.lockInteractiveObjectsDuringTutorial =
config.lockInteractiveObjectsDuringTutorial;
if (config.taxi) {
const LocationConfig::Taxi& taxi = *config.taxi;
LocationState& locationState = *authoritative.location;
locationState.taxiCarTriggerPosition = taxi.triggerPosition;
locationState.taxiCarWaypoints = taxi.waypoints;
locationState.taxiCarInitialState.position = taxi.initialPosition;
locationState.taxiCarInitialState.rotation = taxi.initialRotation;
locationState.taxiCarInitialState.mode = NpcCar::Mode::NONE;
locationState.taxiCarInitialState.velocity = 0.f;
locationState.taxiCarInitialState.steeringAngle = 0.f;
locationState.taxiCarInitialState.waypointReachRadius =
taxi.waypointReachRadius;
locationState.taxiCar.waypointReachRadius = taxi.waypointReachRadius;
if (initializeLocation) authoritative.resetTaxi();
}
publishMutation();
return initializeLocation;
}
void InternalGameServer::setupRuntimeLocation(
const std::string& id, const std::shared_ptr<Location>& location,
LocationSetup params, AudioPlayerAsync* audioPlayer)
{
auto stored = state_.locations.find(id);
if (stored == state_.locations.end())
throw std::runtime_error("Location state was not prepared");
params.gameState = &stored->second;
location->setup(params, audioPlayer);
publishMutation();
}
Location* InternalGameServer::runtimeLocation(const std::string& id) const
{
auto it = runtimeLocations_.find(id);
return it != runtimeLocations_.end() ? it->second.get() : nullptr;
}
bool InternalGameServer::hasRuntimeLocation(const std::string& id) const
{
return runtimeLocations_.count(id) > 0;
}
std::vector<std::string> InternalGameServer::loadedRuntimeLocationIds() const
{
std::vector<std::string> result;
result.reserve(runtimeLocations_.size());
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
return result;
}
void InternalGameServer::clearRuntimeLocations(bool retainState)
{
if (retainState) {
synchronizeRuntimeAuxiliaryState();
} else {
for (const auto& [id, location] : runtimeLocations_)
state_.locations.erase(id);
publishMutation();
}
runtimeLocations_.clear();
}
GameSnapshot InternalGameServer::buildSnapshot() const
{
GameSnapshot result;
@@ -85,7 +265,11 @@ std::shared_ptr<const GameSnapshot> InternalGameServer::snapshot() const
void InternalGameServer::submit(const GameCommand& command)
{
std::visit([this](const auto& value) {
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;
@@ -126,10 +310,83 @@ void InternalGameServer::submit(const GameCommand& command)
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, SetLocationCameraCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
location->second.location->cameraAzimuth = value.azimuth;
location->second.location->cameraInclination = value.inclination;
}
} 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())) {
LocationState& 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) {
LocationState& 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) {
LocationState& 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>) {
state_.inventory.addItem(value.itemId);
if (state_.inventory.addItem(value.itemId))
pendingEvents_.emplace_back(InventoryItemAddedEvent{value.itemId});
} else if constexpr (std::is_same_v<T, RemoveInventoryItemCommand>) {
state_.inventory.removeItem(value.itemId);
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>) {
@@ -225,7 +482,14 @@ void InternalGameServer::submit(const GameCommand& command)
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
}
}, command);
markHostMutation();
publishMutation();
}
std::vector<GameServerEvent> InternalGameServer::drainEvents()
{
std::vector<GameServerEvent> result;
result.swap(pendingEvents_);
return result;
}
void InternalGameServer::update(std::int64_t deltaMs)
@@ -234,11 +498,15 @@ void InternalGameServer::update(std::int64_t deltaMs)
if (runtime == runtimeLocations_.end() || !runtime->second) return;
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;
LocationSimulationEvents events =
stored->second.update(deltaMs, state_.players, interactiveObjectsLocked);
// Runtime presentation reads through snapshots, so publish the simulation
// tick before forwarding its value-only events to the loaded location.
publishMutation();
runtime->second->update(deltaMs, events);
synchronizeRuntimeAuxiliaryState();
}
@@ -251,9 +519,12 @@ void InternalGameServer::save(nlohmann::json& out) const
void InternalGameServer::load(const nlohmann::json& in)
{
pendingEvents_.clear();
state_.load(in);
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
applyStateToRuntime();
markHostMutation();
publishMutation();
}
void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location)
@@ -265,7 +536,7 @@ void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_
} else {
runtimeLocations_[id]->captureAuxiliaryState(state_.locations[id]);
}
markHostMutation();
publishMutation();
}
void InternalGameServer::synchronizeRuntimeAuxiliaryState()
@@ -274,7 +545,7 @@ void InternalGameServer::synchronizeRuntimeAuxiliaryState()
if (!location) continue;
location->captureAuxiliaryState(state_.locations[id]);
}
markHostMutation();
publishMutation();
}
void InternalGameServer::applyStateToRuntime()
@@ -286,7 +557,7 @@ void InternalGameServer::applyStateToRuntime()
}
}
void InternalGameServer::markHostMutation()
void InternalGameServer::publishMutation()
{
++revision_;
cachedSnapshot_.reset();