Files
shadow-over-bishkek001/GAME_STATE_ARCHITECTURE.md
T

393 lines
18 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 four 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. Runtime objects such as `Location`, `Character`, and `InteractiveObject` hold rendering resources and non-owning bindings to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
4. 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`.
### 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.
`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 `LocationState`.
- 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.
State objects use `shared_ptr` internally to keep their addresses stable while local runtime objects are bound to them. 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.
- 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.
## Internal Server
`InternalGameServer` is the current `IGameServer` implementation. It:
- Exclusively owns the aggregate `GameState`.
- Privately stores loaded runtime `Location` objects.
- Executes typed commands against authoritative state.
- Advances the active location simulation through `update(deltaMs)`.
- Synchronizes non-bound auxiliary state such as dialogue, Lua globals, triggers, and teleports.
- Builds and caches immutable snapshots.
- Delegates save/load serialization to the aggregate state model.
`Game` remains a privileged host during this migration. It can access private
internal-server operations for campaign loading, location construction, callback
wiring, and persistence restoration. These host operations are not exposed by
`IGameServer`.
### Animation and combat
Logical animation frame clocks live in `CharacterState`. Animation frame counts are
copied from the loaded clip metadata during runtime construction, after which state
transitions and frame-19 combat impacts are deterministic authoritative simulation.
`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 `LocationState` 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
`Location`, `Character`, and `InteractiveObject` no longer own their authoritative state objects.
- `Location` keeps a private non-owning reference to its `LocationState` and a stable location ID.
- `Character` keeps a private non-owning **const** reference named `characterState`, plus stable entity and location IDs.
- `InteractiveObject` keeps a private non-owning reference to its `InteractiveObjectState`; its configured object name is its stable ID.
The bound state objects are owned by `GameState` or `LocationGameState`. The internal
server advances them directly before runtime presentation. Rendering reads the bound
state; runtime input, scripts, and collision handling submit semantic commands that
invoke `CharacterState` operations against the authoritative `LocationState`.
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. Its only state-facing setup responsibility is reporting frame ranges from
loaded animation assets through `SetCharacterAnimationMetadataCommand`; otherwise 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.
`GameObjectLoader` now:
1. Loads static creation data from location configuration.
2. Creates or reuses server-owned state objects.
3. Applies saved runtime fields over the configuration defaults.
4. Constructs visual objects bound to those state objects.
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 the player and each NPC.
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 `LocationState`; arrival
function names are resolved through `ScriptEngine`.
6. Runtime characters apply the authoritative animation frame to loaded models and
play audio, particles, and presentation callbacks.
7. Auxiliary dialogue, Lua, trigger, and teleport state is captured into `LocationGameState`.
8. The server increments its revision and invalidates the cached snapshot.
9. Rendering reads the bound local state; UI and external-style consumers read an immutable snapshot.
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.
- A campaign-level player state is created under the `"player"` ID.
- The visual player is created and bound to that state.
### Location loading
When a location is loaded:
- A `LocationGameState` record is created or reused.
- Static configuration is loaded.
- NPC and interactive-object visuals are created against state owned by the location record.
- The shared campaign player is attached.
- Dialogue, Lua, navigation, trigger, teleport, and presentation callbacks are initialized.
- The runtime `Location` is registered privately with `InternalGameServer`.
### 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
Persistence uses one strict, versioned schema. `schemaVersion` is required and
unsupported versions are rejected rather than migrated.
- 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, so
runtime render bindings keep stable addresses.
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 bindings submit semantic
commands through `Location`; the server invokes `CharacterState` operations on the
centrally owned state. NPC, interactive-object, trigger-zone, and teleport-zone
bindings address entities by configured ID. Object, inventory, quest, 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 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 systems remain part of `GameState` for single-player compatibility. UI access now crosses 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.
- Server tick scheduling independent of rendering.
- 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 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.
- `Game` currently uses privileged internal-server APIs for local content construction. A remote client implementation will need a client-side runtime-location factory driven by configuration and snapshots.
## Main Files
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence.
- `src/LocationGameState.h` / `src/LocationGameState.cpp` — typed location state, simulation, and ID-keyed persistence.
- `src/game_server/IGameServer.h` — public snapshot and command contract.
- `src/game_server/InternalGameServer.h` / `.cpp` — in-process authoritative implementation.
- `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 bound to
a const authoritative `CharacterState`.
- `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` — configuration/state overlay and runtime construction.
- `src/MenuManager.h` / `src/MenuManager.cpp` — snapshot-driven UI and command submission.