Separating state out of the location

This commit is contained in:
Vladislav Khorev committed 2026-10-30 14:56:24 +06:00
1 parent 5e9a663812
commit 297bd7377f
45 files changed
+2114 -778

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "IGameServer.h"
#include <memory>
#include <string>
#include <vector>
class AudioPlayerAsync;
namespace FRG {
class Location;
struct LocationConfig;
struct LocationSetup;
// Client-only lifecycle and presentation integration for the in-process server.
// The public gameplay contract remains IGameServer and is graphics-free.
class IGameServerHost {
public:
virtual ~IGameServerHost() = default;
virtual void initializeCampaign(
const std::string& campaignId, const std::string& questsPath,
const std::string& zipFile) = 0;
virtual void clearCampaign() = 0;
virtual void resetCampaign(
const std::string& questsPath, const std::string& zipFile) = 0;
virtual bool prepareLocation(const LocationConfig& config) = 0;
virtual void setupRuntimeLocation(
const std::string& id, const std::shared_ptr<Location>& location,
LocationSetup params, AudioPlayerAsync* audioPlayer) = 0;
virtual Location* runtimeLocation(const std::string& id) const = 0;
virtual bool hasRuntimeLocation(const std::string& id) const = 0;
virtual std::vector<std::string> loadedRuntimeLocationIds() const = 0;
virtual void clearRuntimeLocations(bool retainState) = 0;
virtual void bindRuntimeLocation(
const std::string& id, std::shared_ptr<Location> location) = 0;
virtual void update(std::int64_t deltaMs) = 0;
virtual void save(nlohmann::json& out) = 0;
virtual void load(const nlohmann::json& in) = 0;
};
} // namespace FRG
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <string>
namespace FRG {
struct CampaignConfig;
struct LocationConfig;
// Graphics-free administrative lifecycle used by local and standalone hosts.
class IGameServerLifecycle {
public:
virtual ~IGameServerLifecycle() = default;
virtual void startCampaign(
const CampaignConfig& campaign, const std::string& zipFile,
bool createLocalPlayer, bool enableTestBot) = 0;
virtual bool prepareLocation(const LocationConfig& config) = 0;
virtual void activateLocation(const std::string& locationId) = 0;
};
} // namespace FRG
+66 -120
View File
@@ -1,6 +1,6 @@
#include "InternalGameServer.h"
#include "../Campaign.h"
#include "../Location.h"
#include "../content/LocationDefinition.h"
#include <algorithm>
#include <random>
#include <type_traits>
@@ -8,8 +8,6 @@
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;
@@ -20,7 +18,7 @@ InternalGameServer::InternalGameServer() = default;
void InternalGameServer::ensureBattleZoneTestBot()
{
if (state_.campaignId != kBattleZoneCampaignId) return;
if (!kEnableBattleZoneTestBot) {
if (!battleZoneTestBotEnabled_) {
state_.players.erase(kBattleZoneTestBotId);
return;
}
@@ -33,7 +31,7 @@ void InternalGameServer::ensureBattleZoneTestBot()
void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
{
if (!kEnableBattleZoneTestBot ||
if (!battleZoneTestBotEnabled_ ||
state_.campaignId != kBattleZoneCampaignId ||
state_.currentLocationName.empty() ||
state_.players.find(kBattleZoneTestBotId) == state_.players.end()) {
@@ -57,24 +55,66 @@ void InternalGameServer::initializeCampaign(
const std::string& campaignId, const std::string& questsPath,
const std::string& zipFile)
{
state_.campaignId = campaignId;
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;
battleZoneTestBotEnabled_ = enableTestBot;
state_.campaignId = campaign.id;
state_.currentLocationName.clear();
state_.locations.clear();
state_.players.clear();
state_.questJournal.loadFromFile(questsPath, zipFile);
state_.questJournal.loadFromFile(campaign.questsPath, zipFile);
state_.globalFloats["player_hp"] = 200.f;
auto player = std::make_shared<CharacterState>();
player->configureAsPlayer();
state_.players.emplace(localPlayerId_, std::move(player));
if (createLocalPlayer) {
auto player = std::make_shared<CharacterState>();
player->configureAsPlayer();
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);
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()
{
runtimeLocations_.clear();
state_.currentLocationName.clear();
state_.players.clear();
state_.locations.clear();
@@ -117,76 +157,20 @@ void InternalGameServer::resetCampaign(
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();
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;
}
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;
@@ -472,11 +456,8 @@ void InternalGameServer::submit(const GameCommand& command)
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end()) {
location->second.triggerZones[value.zoneId].enabled = value.enabled;
auto runtime = runtimeLocations_.find(value.locationId);
if (runtime != runtimeLocations_.end() && runtime->second) {
for (auto& zone : runtime->second->triggerZones)
if (zone.id == value.zoneId) zone.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);
@@ -494,26 +475,22 @@ std::vector<GameServerEvent> InternalGameServer::drainEvents()
void InternalGameServer::update(std::int64_t deltaMs)
{
auto runtime = runtimeLocations_.find(state_.currentLocationName);
if (runtime == runtimeLocations_.end() || !runtime->second) return;
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;
LocationSimulationEvents events =
pendingSimulationLocationId_ = state_.currentLocationName;
pendingSimulationEvents_ =
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();
}
void InternalGameServer::save(nlohmann::json& out) const
{
const_cast<InternalGameServer*>(this)->synchronizeRuntimeAuxiliaryState();
state_.save(out);
}
@@ -523,40 +500,9 @@ void InternalGameServer::load(const nlohmann::json& in)
state_.load(in);
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
applyStateToRuntime();
publishMutation();
}
void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location)
{
runtimeLocations_[id] = std::move(location);
auto stored = state_.locations.find(id);
if (stored != state_.locations.end() && stored->second.hasPersistentState) {
runtimeLocations_[id]->applyAuxiliaryState(stored->second);
} else {
runtimeLocations_[id]->captureAuxiliaryState(state_.locations[id]);
}
publishMutation();
}
void InternalGameServer::synchronizeRuntimeAuxiliaryState()
{
for (const auto& [id, location] : runtimeLocations_) {
if (!location) continue;
location->captureAuxiliaryState(state_.locations[id]);
}
publishMutation();
}
void InternalGameServer::applyStateToRuntime()
{
for (const auto& [id, stored] : state_.locations) {
auto runtime = runtimeLocations_.find(id);
if (runtime == runtimeLocations_.end() || !runtime->second) continue;
runtime->second->applyAuxiliaryState(stored);
}
}
void InternalGameServer::publishMutation()
{
++revision_;
+15 -20
View File
@@ -1,20 +1,17 @@
#pragma once
#include "IGameServer.h"
class AudioPlayerAsync;
#include "IGameServerLifecycle.h"
namespace FRG {
class Game;
class Character;
class Location;
class LocalGameServerHost;
struct LocationConfig;
struct LocationSetup;
// In-process implementation used by single-player. GameState exists exactly
// once, here; clients see immutable snapshots and mutate it with typed commands.
class InternalGameServer final : public IGameServer {
class InternalGameServer final
: public IGameServer, public IGameServerLifecycle {
public:
InternalGameServer();
@@ -25,9 +22,14 @@ public:
void update(std::int64_t deltaMs) override;
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
void startCampaign(
const CampaignConfig& campaign, const std::string& zipFile,
bool createLocalPlayer, bool enableTestBot) override;
bool prepareLocation(const LocationConfig& config) override;
void activateLocation(const std::string& locationId) override;
private:
friend class Game;
friend class LocalGameServerHost;
// Host-only lifecycle operations. They never expose authoritative state and
// are intentionally absent from IGameServer.
@@ -36,17 +38,6 @@ private:
const std::string& zipFile);
void clearCampaign();
void resetCampaign(const std::string& questsPath, const std::string& zipFile);
bool prepareLocation(const LocationConfig& config);
void setupRuntimeLocation(
const std::string& id, const std::shared_ptr<Location>& location,
LocationSetup params, AudioPlayerAsync* audioPlayer);
Location* runtimeLocation(const std::string& id) const;
bool hasRuntimeLocation(const std::string& id) const;
std::vector<std::string> loadedRuntimeLocationIds() const;
void clearRuntimeLocations(bool retainState);
void bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location);
void synchronizeRuntimeAuxiliaryState();
void applyStateToRuntime();
void publishMutation();
void ensureBattleZoneTestBot();
void updateBattleZoneTestBot(std::int64_t deltaMs);
@@ -55,8 +46,12 @@ private:
GameState state_;
std::string localPlayerId_ = "player";
std::string resourceZipFile_;
std::int64_t battleZoneTestBotElapsedMs_ = 0;
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
bool battleZoneTestBotEnabled_ = false;
std::string pendingSimulationLocationId_;
LocationSimulationEvents pendingSimulationEvents_;
std::vector<std::pair<std::string, std::string>> pendingTriggerZoneUpdates_;
std::uint64_t revision_ = 1;
mutable std::uint64_t cachedRevision_ = 0;
mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_;
+152
View File
@@ -0,0 +1,152 @@
#include "LocalGameServerHost.h"
#include "InternalGameServer.h"
#include "../Campaign.h"
#include "../Location.h"
#include <stdexcept>
namespace FRG {
LocalGameServerHost::LocalGameServerHost(InternalGameServer& server)
: server_(server)
{
}
void LocalGameServerHost::initializeCampaign(
const std::string& campaignId, const std::string& questsPath,
const std::string& zipFile)
{
server_.initializeCampaign(campaignId, questsPath, zipFile);
}
void LocalGameServerHost::clearCampaign()
{
runtimeLocations_.clear();
server_.clearCampaign();
}
void LocalGameServerHost::resetCampaign(
const std::string& questsPath, const std::string& zipFile)
{
server_.resetCampaign(questsPath, zipFile);
}
bool LocalGameServerHost::prepareLocation(const LocationConfig& config)
{
return server_.prepareLocation(config);
}
void LocalGameServerHost::setupRuntimeLocation(
const std::string& id, const std::shared_ptr<Location>& location,
LocationSetup params, AudioPlayerAsync* audioPlayer)
{
auto stored = server_.state_.locations.find(id);
if (stored == server_.state_.locations.end())
throw std::runtime_error("Location state was not prepared");
params.definition = stored->second.definition;
params.gameState = &stored->second;
location->setup(params, audioPlayer);
server_.publishMutation();
}
Location* LocalGameServerHost::runtimeLocation(const std::string& id) const
{
auto it = runtimeLocations_.find(id);
return it != runtimeLocations_.end() ? it->second.get() : nullptr;
}
bool LocalGameServerHost::hasRuntimeLocation(const std::string& id) const
{
return runtimeLocations_.count(id) > 0;
}
std::vector<std::string> LocalGameServerHost::loadedRuntimeLocationIds() const
{
std::vector<std::string> result;
result.reserve(runtimeLocations_.size());
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
return result;
}
void LocalGameServerHost::clearRuntimeLocations(bool retainState)
{
if (retainState) {
synchronizeRuntimeAuxiliaryState();
} else {
for (const auto& [id, location] : runtimeLocations_)
server_.state_.locations.erase(id);
server_.publishMutation();
}
runtimeLocations_.clear();
}
void LocalGameServerHost::bindRuntimeLocation(
const std::string& id, std::shared_ptr<Location> location)
{
runtimeLocations_[id] = std::move(location);
auto stored = server_.state_.locations.find(id);
if (stored != server_.state_.locations.end() &&
stored->second.hasPersistentState) {
runtimeLocations_[id]->applyAuxiliaryState(stored->second);
} else {
runtimeLocations_[id]->captureAuxiliaryState(server_.state_.locations[id]);
}
server_.publishMutation();
}
void LocalGameServerHost::update(std::int64_t deltaMs)
{
server_.update(deltaMs);
for (const auto& [locationId, zoneId] : server_.pendingTriggerZoneUpdates_) {
auto runtime = runtimeLocations_.find(locationId);
auto stored = server_.state_.locations.find(locationId);
if (runtime == runtimeLocations_.end() || !runtime->second ||
stored == server_.state_.locations.end()) {
continue;
}
auto zoneState = stored->second.triggerZones.find(zoneId);
if (zoneState == stored->second.triggerZones.end()) continue;
for (auto& zone : runtime->second->triggerZones)
if (zone.id == zoneId) zone.enabled = zoneState->second.enabled;
}
server_.pendingTriggerZoneUpdates_.clear();
if (server_.pendingSimulationLocationId_.empty()) return;
auto runtime =
runtimeLocations_.find(server_.pendingSimulationLocationId_);
if (runtime == runtimeLocations_.end() || !runtime->second) return;
runtime->second->update(deltaMs, server_.pendingSimulationEvents_);
synchronizeRuntimeAuxiliaryState();
}
void LocalGameServerHost::save(nlohmann::json& out)
{
synchronizeRuntimeAuxiliaryState();
server_.save(out);
}
void LocalGameServerHost::load(const nlohmann::json& in)
{
server_.load(in);
applyStateToRuntime();
}
void LocalGameServerHost::synchronizeRuntimeAuxiliaryState()
{
for (const auto& [id, location] : runtimeLocations_) {
if (!location) continue;
location->captureAuxiliaryState(server_.state_.locations[id]);
}
server_.publishMutation();
}
void LocalGameServerHost::applyStateToRuntime()
{
for (const auto& [id, stored] : server_.state_.locations) {
auto runtime = runtimeLocations_.find(id);
if (runtime == runtimeLocations_.end() || !runtime->second) continue;
runtime->second->applyAuxiliaryState(stored);
}
}
} // namespace FRG
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "IGameServerHost.h"
#include <unordered_map>
namespace FRG {
class InternalGameServer;
// Adapts the headless authoritative server to the graphical in-process client.
class LocalGameServerHost final : public IGameServerHost {
public:
explicit LocalGameServerHost(InternalGameServer& server);
void initializeCampaign(
const std::string& campaignId, const std::string& questsPath,
const std::string& zipFile) override;
void clearCampaign() override;
void resetCampaign(
const std::string& questsPath, const std::string& zipFile) override;
bool prepareLocation(const LocationConfig& config) override;
void setupRuntimeLocation(
const std::string& id, const std::shared_ptr<Location>& location,
LocationSetup params, AudioPlayerAsync* audioPlayer) override;
Location* runtimeLocation(const std::string& id) const override;
bool hasRuntimeLocation(const std::string& id) const override;
std::vector<std::string> loadedRuntimeLocationIds() const override;
void clearRuntimeLocations(bool retainState) override;
void bindRuntimeLocation(
const std::string& id, std::shared_ptr<Location> location) override;
void update(std::int64_t deltaMs) override;
void save(nlohmann::json& out) override;
void load(const nlohmann::json& in) override;
private:
void synchronizeRuntimeAuxiliaryState();
void applyStateToRuntime();
InternalGameServer& server_;
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
};
} // namespace FRG