Files
shadow-over-bishkek001/GAME_STATE_ARCHITECTURE.md
T

482 lines
23 KiB
Markdown

# Game State Architecture
## Purpose
The game now separates authoritative gameplay state from rendering and runtime resources. The current implementation is an in-process server, but public consumers use a snapshot-and-command API designed to support a future network client.
This refactor does **not** add multiplayer networking. It establishes the state ownership and API boundaries needed before networking, replication, multiple local player representations, or server-side synchronization are implemented.
## Architecture Overview
The architecture has five layers:
1. `GameState` is the aggregate root for persistent, authoritative state.
2. `InternalGameServer` owns `GameState`, advances the local simulation, handles commands, and produces immutable snapshots.
3. `ClientLocationState` owns per-client `LocationLocalState` records, including camera orientation. Graphical hosts own this state; it is never replicated.
4. Runtime objects hold rendering resources. `Character` and `Location` read immutable server snapshots by stable ID, while `InteractiveObject` still uses a transitional non-owning binding to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
5. Client-facing systems such as `MenuManager` read immutable snapshots and submit typed commands through `IGameServer`.
```text
Input / UI
| typed GameCommand
v
IGameServer
|
v
InternalGameServer ---- owns ----> GameState
| |
| update | stable state objects
v v
Location / Character / InteractiveObject runtime
|
v
Renderer
IGameServer ---- immutable GameSnapshot ----> UI and other clients
```
## Authoritative State Model
### `GameState`
`GameState` is the top-level state object. It contains:
- Campaign ID and current location ID.
- Day, night, dawn, and darklands flags.
- A map of campaign-level players keyed by stable player ID.
- A map of typed `LocationGameState` records keyed by location ID.
- Inventory, money, player-health HUD values, quests, tutorial progress, chat data, taxi state, and script-global values.
Live `Location` pointers and rendering objects are no longer stored in `GameState`.
Embedded state models such as `Inventory` and `QuestJournal` contain only value
data and deterministic operations; they do not retain runtime callbacks.
### Campaign-level players
Player `CharacterState` objects are stored in `GameState::players`. The current single-player game uses the ID `"player"`.
The player state is no longer owned by a `Character` or duplicated as a separately authoritative state for each loaded location. This permits additional player IDs to be introduced later without changing the aggregate ownership model.
Every player, including the one controlled by the current client, remains in this
same authoritative collection. `IGameServer::localPlayerId()` identifies which
entry is local to a particular client connection; it is not a separate state
object.
`LocationGameState` does not retain a player pointer or back-reference to `GameState`.
Its update boundary receives `GameState::players` by reference, allowing character
lookup and simulation to treat every campaign player by stable ID.
### `LocationGameState`
Each location record owns:
- One `LocationNetworkState`.
- NPC `CharacterState` objects keyed by configured ID.
- `InteractiveObjectState` objects keyed by configured ID.
- Trigger-zone enabled/inside flags.
- Teleport-zone active flags.
- Serialized dialogue runtime state.
- Serialized Lua runtime globals.
World-mode flags (`darklands`, `night`, and `dawn`) exist only at the aggregate
`GameState` level. Location rendering and scripts read those values from the
top-level snapshot rather than maintaining per-location mirrors.
State objects use `shared_ptr` internally to keep addresses stable for the
remaining local runtime bindings. These pointers never appear in public snapshots.
### Stable identity
- Locations use campaign configuration IDs.
- Players use keys from `GameState::players`.
- NPCs use required location-local configuration IDs.
- Interactive objects use required location-local configuration IDs.
- Trigger and teleport zones use their configured IDs.
Missing, empty, duplicate, and reserved IDs are content errors. Runtime references,
Lua APIs, snapshots, and persistence all use these IDs rather than configuration
array positions.
## Server Interface
`IGameServer` is the public state boundary. It exposes:
- `snapshot()` for an immutable, revisioned `GameSnapshot`.
- `submit()` for typed `GameCommand` values.
- `update()` for advancing or polling the implementation.
- `save()` and `load()` for server-side persistence.
No public method returns a mutable state pointer or reference.
### Snapshots
`GameSnapshot` and its nested snapshot types are value-only read models. They contain no renderer objects, runtime `Location` pointers, mutable references, or active callbacks.
Snapshots include:
- Campaign and world-mode state.
- All players.
- Locations, NPCs, interactive objects, triggers, and teleports.
- Inventory item IDs.
- Quest views.
- Tutorial, chat, taxi, money, and health data.
- Script-global values.
Snapshots have a monotonically increasing revision. `InternalGameServer` caches the current immutable snapshot and rebuilds it only after the authoritative state changes.
### Commands
`GameCommand` is a C++17 `std::variant` of semantic command types. Current commands cover:
- World mode and current location.
- Player health and money.
- Tutorial progression and flags.
- Chat history and unread state.
- Taxi state.
- Inventory additions and removals.
- Quest status, objective completion/visibility, and active-objective changes.
- Script globals.
- Character transforms, attack targets, navigation destinations, stopping,
facing, replanning, health, damage, death reset, attack combos, and animation
metadata.
- Interactive-object activation.
- Trigger-zone state.
- Quest-definition reloads.
A future network implementation can serialize equivalent requests instead of mutating local state.
### Presentation events
`IGameServer::drainEvents()` returns value-only events produced by successful
authoritative mutations. Inventory add/remove and quest unlock/complete/fail/
objective-complete events contain stable IDs only. `Game` drains these events and
performs local tutorial, toast, and audio presentation through `MenuManager`.
Loading and persistence restoration clear pending events and remain silent.
## Internal Server
`InternalGameServer` is the current `IGameServer` implementation. It:
- Exclusively owns the aggregate `GameState`.
- Executes typed commands against authoritative state.
- Advances the active location simulation through `update(deltaMs)`.
- Builds and caches immutable snapshots.
- Delegates save/load serialization to the aggregate state model.
`LocalGameServerHost` is the graphical client's integration adapter. It owns loaded
runtime `Location` objects, forwards simulation events to them, and synchronizes
dialogue, Lua, trigger, and teleport auxiliary state. `Game` uses the explicit
`IGameServerHost` lifecycle interface and is no longer a friend of
`InternalGameServer`. The standalone server does not compile this adapter.
Location loading has two phases. `LocationContentLoader` parses renderer-free
`LocationDefinition` data, and `LocationGameState::setup()` creates/reconciles
authoritative NPC, interactive-object, navigation, taxi, trigger, teleport, and
animation state. The graphical `Location::setup()` runs afterward and only builds
presentation objects from that definition and the prepared state. Lua and dialogue
runtime behavior remains client-side.
### Animation and combat
Logical animation frame clocks live in `CharacterState`. The renderer-free content
loader reads animation frame metadata before authoritative entities are created.
Runtime `Character` construction only loads models for presentation and does not
submit metadata mutations back to the server.
`CharacterState::update()` returns value-only events containing arrival Lua function
names, attack impacts, navigation requests, and death-animation completion. `LocationGameState` applies
queued damage after every character has advanced. Runtime `Character` objects only
pose loaded models and produce audio, particles, and presentation callbacks.
### Character state operations
Gameplay operations mutate the authoritative `CharacterState`, not the runtime
`Character`:
- `setTarget()` normalizes the destination, avoids redundant requests, stores the
optional Lua arrival function name, and asks the authoritative `LocationNetworkState` to
call `planPathExceptMyself()`.
- `forceReplan()` recalculates the current requested destination using the same
location navigation state.
- `stopInPlace()` clears navigation and any pending arrival function.
- `stopAndRotateToTarget()`, `resetPlayerAfterDeath()`, `setHp()`, and
`tryAttackCombo()` update their respective state directly.
`InternalGameServer::submit()` resolves the character and location, then invokes
these operations for the corresponding semantic command. The runtime `Location`
provides convenience methods for submitting those commands from input, scripts,
collision handling, and presentation-event processing.
## Runtime and Visual Object Bindings
Runtime visual objects do not own their authoritative state objects.
- `Location` keeps its `IGameServer` reference, stable location ID, and a shared
`LocationLocalState` supplied by the graphical host. It resolves its immutable
`LocationNetworkState` view from `IGameServer::snapshot()`, while rendering and
camera input access the local state directly.
- `Character` keeps only its `IGameServer` reference plus stable entity and location IDs. It resolves the player-first character view from `IGameServer::snapshot()`.
- `InteractiveObject` keeps a private non-owning reference to its `InteractiveObjectState`; its configured object name is its stable ID.
`Character::stateSnapshot()` and `Location::stateSnapshot()` return aliasing
`shared_ptr` views. Each pointer addresses a state copy inside a `GameSnapshot`,
while shared ownership keeps that whole immutable snapshot alive. Update, input,
editor, and draw operations capture one such pointer and use it consistently
throughout the operation. A missing or unloaded entity produces an empty pointer
and presentation or input work is skipped.
The authoritative state objects are owned by `GameState` or `LocationGameState`.
The internal server advances them before publishing a new snapshot revision.
Runtime input, scripts, and collision handling submit semantic commands that invoke
authoritative character and location operations. Location commands cover pending
interaction targets, navigation switching and area changes, editor obstacles, and
taxi reset. Camera orientation is deliberately not a server command. No
whole-location commit command is exposed.
Navigation target selection, replanning, stopping, facing, death reset, health
assignment, and attack-combo clock changes are implemented by `CharacterState`.
`Character` no longer exposes gameplay mutation helpers or a generic state mutation
callback. Character construction is presentation-only; frame ranges are already
present in `CharacterState` after content setup. It focuses on model posing,
rendering, audio, particles, and runtime callbacks.
Death-animation and health-change callbacks remain runtime-only on `Character`.
Arrival behavior is represented solely by the serialized Lua function name in
`CharacterState`; `Location` resolves and invokes that name through `ScriptEngine`
when it consumes the arrival event.
`Location` owns presentation-only runtime characters for all non-local entries in
`GameSnapshot::players`. It reconciles them by stable ID as snapshots change,
reuses the local-player assets and animation set, and includes them in all render,
shadow, lighting, animation, damage, and name-tag passes. The local entry is
excluded using `IGameServer::localPlayerId()` because its visual is already owned
by `Game`.
`GameObjectLoader` now:
1. Receives immutable object/NPC definitions parsed by `LocationContentLoader`.
2. Resolves already-prepared authoritative entities by stable ID.
3. Constructs meshes, textures, VBOs, `InteractiveObject`, and `Character`
presentation wrappers without creating or overwriting authoritative state.
Configuration remains the source of visual assets, animation paths, meshes, textures, and fixed tuning data.
## State Update Flow
The normal frame flow is:
1. SDL input is processed.
2. UI code submits semantic commands through `IGameServer`.
3. The internal server freezes location character observations and calls
`CharacterState::update()` for every present player and NPC. An empty player
collection is valid.
4. Attack impacts are queued, then damage is applied after all characters advance.
5. The runtime location consumes value-only events. Navigation requests are sent
back as semantic destination commands, whose server handler calls
`CharacterState::setTarget()` with the authoritative `LocationNetworkState`; arrival
function names are resolved through `ScriptEngine`.
6. The server invalidates its cached snapshot and advances the revision so runtime
presentation can observe the completed simulation tick.
7. Runtime characters read one immutable snapshot per operation, apply the
authoritative animation frame to loaded models, and
play audio, particles, and presentation callbacks.
8. Auxiliary dialogue, Lua, trigger, and teleport state is captured into `LocationGameState`.
9. The server publishes those auxiliary changes in a later revision.
10. Rendering, UI, and external-style consumers read immutable snapshots.
Commands that update bound character or object state do not reload the whole location. This avoids replaying dialogue-node effects or Lua callbacks.
## Campaign and Location Lifecycle
Only one location is normally loaded as a runtime object.
### Campaign loading
When a campaign starts:
- Existing campaign state and runtime locations are cleared.
- Quest definitions are loaded.
- The graphical single-player host requests a campaign-level `"player"` state and
creates its visual wrapper.
- A standalone host may deliberately create no players; locations continue to tick.
- The battle-zone test bot is an explicit host option rather than an unconditional
campaign side effect.
### Location loading
When a location is loaded:
- `LocationContentLoader` parses a renderer-free `LocationDefinition`.
- `LocationGameState::setup()` creates or reconciles navigation, taxi, NPC,
interactive-object, trigger, teleport, and animation state.
- Saved mutable fields are overlaid on configuration defaults by stable ID.
- `Location::setup()` builds static and entity visuals from the prepared definition
without mutating authoritative state.
- The shared campaign player is attached.
- Runtime visuals are created for other player IDs currently present in the
snapshot and are subsequently spawned or removed as snapshots change.
- Dialogue, Lua, trigger/teleport visuals, and presentation callbacks are initialized.
- The runtime `Location` is owned and registered by `LocalGameServerHost`;
`InternalGameServer` remains headless.
### Location unloading
When leaving a location:
- Auxiliary runtime state is captured.
- The visual/runtime `Location` is destroyed.
- Its typed `LocationGameState` remains in `GameState`.
The player state remains campaign-level and survives location changes.
## Save and Load Schema
Authoritative persistence uses one strict, versioned schema. `schemaVersion` is
required and unsupported versions are rejected rather than migrated. A
single-player save is a separate versioned envelope containing `networkState`
(the authoritative `GameState` JSON) and `localState` (the client-local location
map). WebSocket replication sends only the authoritative document.
- Campaign players are stored once in an ID-keyed `players` object.
- Locations are keyed by location ID.
- Each location stores NPC and interactive-object state in ID-keyed objects.
- Pending interaction targets and character references use stable IDs.
- Immutable content such as meshes, initial taxi configuration, and object
definitions is reconstructed from campaign configuration.
- Configuration is loaded first and mutable saved state is overlaid in place.
Snapshot-backed runtime objects then resolve the resulting state by stable ID.
Old user save slots are intentionally unsupported. Bundled start-state snapshots
must be regenerated with `Game::saveInitialState()` whenever the schema changes.
## UI, Scripts, Dialogue, and Quests
### UI
`MenuManager` no longer holds `GameState&` or a mutable `Inventory*`.
It now:
- Reads inventory, quests, tutorial state, chat, money, health, location, and world mode from `GameSnapshot`.
- Submits typed commands for mutations.
- Continues to own presentation-only UI trees, screen state, textures, and callbacks.
### Scripts
Lua remains an in-process single-player runtime. Character, inventory, quest, and
campaign-global bindings submit semantic commands through `Location`; the server
invokes operations on centrally owned state. Snapshot queries provide global
values, inventory counts, item presence, and synchronous quest validation for the
existing Lua API. NPC,
interactive-object, trigger-zone, and teleport-zone bindings address entities by
configured ID. Other object and global-state bindings remain trusted local
integrations.
Each location script registers initialization, enter, and exit callbacks.
Initialization runs only when the location has no retained state; loading the Lua
file again for rendering does not repeat state-resetting setup. Enter and exit
callbacks continue to run for every location transition.
The scalar Lua runtime state for each location is stored under `LocationGameState::scriptGlobals`. Multiplayer location scripts are expected to be empty, so remote script execution is intentionally outside the current scope.
### Dialogue and cutscenes
Dialogue runtime state is captured in each `LocationGameState`. Dialogue global
flags use snapshot getter and command setter callbacks rather than retaining a
mutable map pointer. Dialogue and cutscene rendering remain local presentation
systems.
Dialogues and cutscenes are outside the planned multiplayer gameplay mode, but their existing single-player save and callback behavior is retained.
### Quests, inventory, and tutorial
These callback-free state systems remain part of `GameState` for single-player
compatibility. UI access, mutations, and presentation notifications cross the
server interface. They can remain empty or disabled in multiplayer mode as planned.
## Multiplayer Readiness
The refactor provides:
- One authoritative aggregate state owner.
- Multiple player slots keyed by stable ID.
- Stable location, character, object, and zone identities.
- Immutable state snapshots suitable for replication.
- Semantic commands suitable for request serialization.
- A replaceable public `IGameServer` contract.
- A single active-location model matching the initial multiplayer scope.
Still required for real multiplayer:
- Network transport and command serialization.
- Authentication and player/session assignment.
- Snapshot delta generation or other replication strategy.
- Latency handling, interpolation, reconciliation, and prediction.
- Command validation, authorization, and anti-cheat rules.
- Multiplayer spawning/despawning and location membership.
- Presentation events delivered over the network.
The current local implementation intentionally preserves synchronous Lua and animation behavior. A remote implementation must define which of those behaviors remain server-authoritative and which become client presentation.
## Platform Build Integration
The new state and server files are registered in:
- `proj-windows/CMakeLists.txt`
- `proj-linux/CMakeLists.txt`
- `proj-web/CMakeLists.txt`
- `proj-android/app/jni/src/CMakeLists.txt`
The refactor uses portable C++17 and introduces no Windows-only gameplay code.
The standalone target lives under `server/`. It links the authoritative state,
campaign, content-definition, and portable resource sources only; SDL, OpenGL,
audio, Lua, dialogue presentation, and rendering sources are absent. It can prepare
a selected campaign/location with zero players and run the 50 ms fixed-step loop.
Networking remains outside this milestone.
The Windows Debug target was verified with:
```powershell
cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -- /m
```
## Important Constraints
- Runtime C++ callbacks are not part of `CharacterState` or public snapshots;
serializable Lua arrival function names are state data.
- `InteractiveObjectState` animation callbacks are also removed from public snapshots.
- Character target and attacker references use stable IDs (`"player"` or a unique
location-local NPC ID).
- Some top-level values remain mirrors for compatibility, notably player health used by the HUD and scripts.
- Dialogue and Lua are synchronized as auxiliary location state rather than fully networked subsystems.
- `IGameServerLifecycle` is the graphics-free campaign/location administration
boundary. `LocalGameServerHost` adds client-only runtime construction and
auxiliary-state synchronization without making `Game` a server friend.
- Empty player maps are valid. Player-specific interactions remain inactive until
a player entity is added.
- A remote client still needs a client-side runtime-location factory driven by
configuration/definitions and replicated snapshots.
## Main Files
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence.
- `src/LocationState.h` / `src/LocationState.cpp` — separate local camera and authoritative network location records, plus single-player persistence composition.
- `src/LocationGameState.h` / `src/LocationGameState.cpp` — authoritative typed location state, simulation, and ID-keyed persistence.
- `src/content/LocationDefinition.h` / `.cpp` — renderer-free content definitions,
parsing, and animation metadata loading.
- `src/game_server/IGameServer.h` — public snapshot and command contract.
- `src/game_server/IGameServerLifecycle.h` — graphics-free campaign/location lifecycle.
- `src/game_server/InternalGameServer.h` / `.cpp` — in-process authoritative implementation.
- `src/game_server/LocalGameServerHost.h` / `.cpp` — graphical-client runtime adapter.
- `src/Game.h` / `src/Game.cpp` — application host, campaign lifecycle, and runtime wiring.
- `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
interactions, collisions, scripts, and presentation-event consumption.
- `src/Character.h` / `src/Character.cpp` — passive character presentation resolving
immutable `CharacterState` snapshots by stable ID.
- `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character
simulation, navigation operations, combat state, and logical animation clock.
- `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state.
- `src/items/GameObjectLoader.h` / `.cpp` — presentation builders for prepared definitions.
- `src/MenuManager.h` / `src/MenuManager.cpp` — snapshot-driven UI and command submission.