473 lines
22 KiB
Markdown
473 lines
22 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 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.
|
|
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`.
|
|
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 `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.
|
|
|
|
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 `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
|
|
|
|
Runtime visual objects do not own their authoritative state objects.
|
|
|
|
- `Location` keeps only its `IGameServer` reference and stable location ID. It resolves its immutable `LocationState` view from `IGameServer::snapshot()`.
|
|
- `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 camera
|
|
orientation, pending interaction targets, navigation switching and area changes,
|
|
editor obstacles, and taxi reset. 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 `LocationState`; 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
|
|
|
|
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.
|
|
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/LocationGameState.h` / `src/LocationGameState.cpp` — 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.
|