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,392 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user