Major refactoring - internal server state machine implemented
This commit is contained in:
1 parent
71c654e5b6
commit
66e93d46f4
48 files changed
+3948
-3696
No files matched your search
@@ -0,0 +1,227 @@
|
||||
#pragma once
|
||||
|
||||
#include "../GameState.h"
|
||||
#include "../external/nlohmann/json.hpp"
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// Value-only view of authoritative state. It deliberately contains no runtime
|
||||
// pointers, callbacks, renderer objects, or mutable references, so the same
|
||||
// contract can later be transported over a network.
|
||||
struct ChatMessageSnapshot {
|
||||
std::string text;
|
||||
bool incoming = false;
|
||||
};
|
||||
|
||||
struct CharacterSnapshot {
|
||||
std::string id;
|
||||
CharacterState state;
|
||||
};
|
||||
|
||||
struct InteractiveObjectSnapshot {
|
||||
std::string id;
|
||||
InteractiveObjectState state;
|
||||
};
|
||||
|
||||
struct LocationSnapshot {
|
||||
std::string id;
|
||||
LocationState state;
|
||||
std::unordered_map<std::string, CharacterSnapshot> npcs;
|
||||
std::unordered_map<std::string, InteractiveObjectSnapshot> interactiveObjects;
|
||||
std::unordered_map<std::string, TriggerZoneGameState> triggerZones;
|
||||
std::unordered_map<std::string, bool> teleportZones;
|
||||
nlohmann::json dialogue;
|
||||
nlohmann::json scriptGlobals;
|
||||
};
|
||||
|
||||
struct GameSnapshot {
|
||||
std::uint64_t revision = 0;
|
||||
std::string campaignId;
|
||||
std::string currentLocationId;
|
||||
bool isDarklands = false;
|
||||
bool isNight = false;
|
||||
bool isDawn = false;
|
||||
float playerHp = 0.f;
|
||||
float playerMaxHp = 0.f;
|
||||
int money = 0;
|
||||
TutorialStep tutorialStep = TutorialStep::Completed;
|
||||
bool tutorialPhoneChatScreenOpened = false;
|
||||
bool tutorialJournalScreenOpened = false;
|
||||
bool tutorialNeedOpenTaxiScreen = false;
|
||||
bool tutorialTaxiHintDone = false;
|
||||
bool taxiIsCalled = false;
|
||||
bool chatUnread[3] = {};
|
||||
std::string chatPreview[3];
|
||||
std::vector<ChatMessageSnapshot> chatHistory[3];
|
||||
std::vector<std::string> inventoryItemIds;
|
||||
nlohmann::json questJournal;
|
||||
std::unordered_map<std::string, int> globalInts;
|
||||
std::unordered_map<std::string, float> globalFloats;
|
||||
std::unordered_map<std::string, CharacterSnapshot> players;
|
||||
std::unordered_map<std::string, LocationSnapshot> locations;
|
||||
std::vector<Quest::QuestState> quests;
|
||||
|
||||
bool isTutorialActive() const { return tutorialStep != TutorialStep::Completed; }
|
||||
};
|
||||
|
||||
struct SetWorldModeCommand { bool darklands; bool night; bool dawn; };
|
||||
struct SetCurrentLocationCommand { std::string locationId; };
|
||||
struct SetPlayerHealthCommand { float hp; float maxHp; };
|
||||
struct SetMoneyCommand { int amount; };
|
||||
struct SpendMoneyCommand { int amount; };
|
||||
struct CompleteTutorialStepCommand { TutorialStep step; };
|
||||
struct SetTutorialStepCommand { TutorialStep step; };
|
||||
struct SetTutorialFlagsCommand {
|
||||
bool phoneChatOpened;
|
||||
bool journalOpened;
|
||||
bool needTaxiHint;
|
||||
bool taxiHintDone;
|
||||
};
|
||||
struct SetChatCommand {
|
||||
int index;
|
||||
bool unread;
|
||||
std::string preview;
|
||||
};
|
||||
struct AppendChatMessageCommand {
|
||||
int index;
|
||||
std::string text;
|
||||
bool incoming;
|
||||
};
|
||||
struct SetTaxiCalledCommand { bool called; };
|
||||
struct StartTaxiCommand { std::string locationId; };
|
||||
struct AddInventoryItemCommand { std::string itemId; };
|
||||
struct RemoveInventoryItemCommand { std::string itemId; };
|
||||
struct SetGlobalIntCommand { std::string key; int value; };
|
||||
struct SetGlobalFloatCommand { std::string key; float value; };
|
||||
struct SetCharacterTransformCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f position;
|
||||
float facingAngle;
|
||||
};
|
||||
struct SetCharacterTargetCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
std::string targetId;
|
||||
};
|
||||
struct ClearCharacterAttackTargetCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct SetCharacterDestinationCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f target;
|
||||
std::string callbackName;
|
||||
};
|
||||
struct StopCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct FaceCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
std::string targetId;
|
||||
};
|
||||
struct ReplanCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct ResetCharacterAfterDeathCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct TryCharacterAttackComboCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct SetCharacterHealthCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
float hp;
|
||||
};
|
||||
struct SetCharacterAnimationMetadataCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
AnimationState animationState;
|
||||
int startFrame;
|
||||
int frameCount;
|
||||
};
|
||||
struct DamageCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
float amount;
|
||||
std::string attackerId;
|
||||
};
|
||||
struct CommitCharacterStateCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
CharacterState state;
|
||||
};
|
||||
struct SetInteractiveObjectActiveCommand {
|
||||
std::string locationId;
|
||||
std::string objectId;
|
||||
bool active;
|
||||
};
|
||||
struct SetTriggerZoneEnabledCommand {
|
||||
std::string locationId;
|
||||
std::string zoneId;
|
||||
bool enabled;
|
||||
};
|
||||
struct ReloadQuestDefinitionsCommand { std::string path; std::string zipFile; };
|
||||
|
||||
using GameCommand = std::variant<
|
||||
SetWorldModeCommand,
|
||||
SetCurrentLocationCommand,
|
||||
SetPlayerHealthCommand,
|
||||
SetMoneyCommand,
|
||||
SpendMoneyCommand,
|
||||
CompleteTutorialStepCommand,
|
||||
SetTutorialStepCommand,
|
||||
SetTutorialFlagsCommand,
|
||||
SetChatCommand,
|
||||
AppendChatMessageCommand,
|
||||
SetTaxiCalledCommand,
|
||||
StartTaxiCommand,
|
||||
AddInventoryItemCommand,
|
||||
RemoveInventoryItemCommand,
|
||||
SetGlobalIntCommand,
|
||||
SetGlobalFloatCommand,
|
||||
SetCharacterTransformCommand,
|
||||
SetCharacterTargetCommand,
|
||||
ClearCharacterAttackTargetCommand,
|
||||
SetCharacterDestinationCommand,
|
||||
StopCharacterCommand,
|
||||
FaceCharacterCommand,
|
||||
ReplanCharacterCommand,
|
||||
ResetCharacterAfterDeathCommand,
|
||||
TryCharacterAttackComboCommand,
|
||||
SetCharacterHealthCommand,
|
||||
SetCharacterAnimationMetadataCommand,
|
||||
DamageCharacterCommand,
|
||||
CommitCharacterStateCommand,
|
||||
SetInteractiveObjectActiveCommand,
|
||||
SetTriggerZoneEnabledCommand,
|
||||
ReloadQuestDefinitionsCommand>;
|
||||
|
||||
class IGameServer {
|
||||
public:
|
||||
virtual ~IGameServer() = default;
|
||||
|
||||
// Each returned object is immutable and remains valid after later commands.
|
||||
virtual std::shared_ptr<const GameSnapshot> snapshot() const = 0;
|
||||
virtual void submit(const GameCommand& command) = 0;
|
||||
virtual void update(std::int64_t deltaMs) = 0;
|
||||
|
||||
// Persistence stays server-side. The JSON shape is GameState's established
|
||||
// save schema and is intentionally not the wire snapshot shape.
|
||||
virtual void save(nlohmann::json& out) const = 0;
|
||||
virtual void load(const nlohmann::json& in) = 0;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,295 @@
|
||||
#include "InternalGameServer.h"
|
||||
#include "../Location.h"
|
||||
#include <algorithm>
|
||||
#include <type_traits>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
InternalGameServer::InternalGameServer() = default;
|
||||
|
||||
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)
|
||||
{
|
||||
std::visit([this](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, AddInventoryItemCommand>) {
|
||||
state_.inventory.addItem(value.itemId);
|
||||
} else if constexpr (std::is_same_v<T, RemoveInventoryItemCommand>) {
|
||||
state_.inventory.removeItem(value.itemId);
|
||||
} 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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, ReloadQuestDefinitionsCommand>) {
|
||||
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
|
||||
}
|
||||
}, command);
|
||||
markHostMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::update(std::int64_t deltaMs)
|
||||
{
|
||||
auto runtime = runtimeLocations_.find(state_.currentLocationName);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) return;
|
||||
auto stored = state_.locations.find(state_.currentLocationName);
|
||||
if (stored == state_.locations.end()) return;
|
||||
const bool interactiveObjectsLocked =
|
||||
stored->second.lockInteractiveObjectsDuringTutorial &&
|
||||
state_.tutorialStep < TutorialStep::PickUpItem;
|
||||
LocationSimulationEvents events =
|
||||
stored->second.update(deltaMs, state_.players, interactiveObjectsLocked);
|
||||
runtime->second->update(deltaMs, events);
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
}
|
||||
|
||||
void InternalGameServer::save(nlohmann::json& out) const
|
||||
{
|
||||
const_cast<InternalGameServer*>(this)->synchronizeRuntimeAuxiliaryState();
|
||||
state_.save(out);
|
||||
}
|
||||
|
||||
void InternalGameServer::load(const nlohmann::json& in)
|
||||
{
|
||||
state_.load(in);
|
||||
applyStateToRuntime();
|
||||
markHostMutation();
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
markHostMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::synchronizeRuntimeAuxiliaryState()
|
||||
{
|
||||
for (const auto& [id, location] : runtimeLocations_) {
|
||||
if (!location) continue;
|
||||
location->captureAuxiliaryState(state_.locations[id]);
|
||||
}
|
||||
markHostMutation();
|
||||
}
|
||||
|
||||
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::markHostMutation()
|
||||
{
|
||||
++revision_;
|
||||
cachedSnapshot_.reset();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class Game;
|
||||
class Character;
|
||||
class Location;
|
||||
|
||||
// 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 {
|
||||
public:
|
||||
InternalGameServer();
|
||||
|
||||
std::shared_ptr<const GameSnapshot> snapshot() const override;
|
||||
void submit(const GameCommand& command) override;
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
private:
|
||||
friend class Game;
|
||||
|
||||
// Transitional host operations are intentionally available only to Game,
|
||||
// which owns location loading and old-save migration. They are never part of
|
||||
// IGameServer and therefore cannot leak mutable state to UI/network clients.
|
||||
GameState& hostState() { return state_; }
|
||||
const GameState& hostState() const { return state_; }
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() { return runtimeLocations_; }
|
||||
const std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() const { return runtimeLocations_; }
|
||||
void bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location);
|
||||
void synchronizeRuntimeAuxiliaryState();
|
||||
void applyStateToRuntime();
|
||||
void markHostMutation();
|
||||
|
||||
GameSnapshot buildSnapshot() const;
|
||||
|
||||
GameState state_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
|
||||
std::uint64_t revision_ = 1;
|
||||
mutable std::uint64_t cachedRevision_ = 0;
|
||||
mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
Reference in new issue
Block a user