Set up basic deterministic lockstep
This commit is contained in:
1 parent
28c6e83eda
commit
23fef6f349
26 files changed
+1172
-239
No files matched your search
+73
-52
@@ -2,19 +2,21 @@
|
||||
|
||||
## 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.
|
||||
The game separates authoritative gameplay state from rendering and runtime
|
||||
resources. The same snapshot-and-command boundary supports the in-process
|
||||
single-player server and a WebSocket deterministic-lockstep client/server.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The architecture has five layers:
|
||||
The architecture has six 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`.
|
||||
6. Protocol-v2 WebSocket hosts assign preconfigured player slots and exchange
|
||||
one initial state followed by ordered 100 ms tick bundles.
|
||||
|
||||
```text
|
||||
Input / UI
|
||||
@@ -103,7 +105,7 @@ array positions.
|
||||
|
||||
- `snapshot()` for an immutable, revisioned `GameSnapshot`.
|
||||
- `submit()` for typed `GameCommand` values.
|
||||
- `update()` for advancing or polling the implementation.
|
||||
- `update()` for advancing a local fixed tick or polling a remote implementation.
|
||||
- `save()` and `load()` for server-side persistence.
|
||||
|
||||
No public method returns a mutable state pointer or reference.
|
||||
@@ -143,7 +145,10 @@ Snapshots have a monotonically increasing revision. `InternalGameServer` caches
|
||||
- Trigger-zone state.
|
||||
- Quest-definition reloads.
|
||||
|
||||
A future network implementation can serialize equivalent requests instead of mutating local state.
|
||||
The remote implementation currently serializes only
|
||||
`SetCharacterDestinationCommand` as player intent. The server derives the
|
||||
character and location from the assigned connection slot; arbitrary state
|
||||
mutation commands are not accepted from the network.
|
||||
|
||||
### Presentation events
|
||||
|
||||
@@ -159,7 +164,7 @@ Loading and persistence restoration clear pending events and remain silent.
|
||||
|
||||
- Exclusively owns the aggregate `GameState`.
|
||||
- Executes typed commands against authoritative state.
|
||||
- Advances the active location simulation through `update(deltaMs)`.
|
||||
- Advances the active location simulation in exact 100 ms ticks.
|
||||
- Builds and caches immutable snapshots.
|
||||
- Delegates save/load serialization to the aggregate state model.
|
||||
|
||||
@@ -184,9 +189,12 @@ 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.
|
||||
names, attack impacts, navigation requests, and death-animation completion.
|
||||
`LocationGameState` applies queued damage after every character has advanced.
|
||||
Navigation replans are consumed by `InternalGameServer` as deterministic
|
||||
simulation consequences rather than being resubmitted by presentation code.
|
||||
Runtime `Character` objects only pose loaded models and produce audio, particles,
|
||||
and presentation callbacks.
|
||||
|
||||
### Character state operations
|
||||
|
||||
@@ -263,26 +271,23 @@ Configuration remains the source of visual assets, animation paths, meshes, text
|
||||
|
||||
## State Update Flow
|
||||
|
||||
The normal frame flow is:
|
||||
Render frames and simulation ticks are separate:
|
||||
|
||||
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.
|
||||
1. SDL input and UI presentation use the real elapsed render-frame time.
|
||||
2. A local host accumulates `steady_clock` elapsed time and runs zero or more
|
||||
exact 100 ms simulation ticks. A remote host polls transport every frame but
|
||||
advances only from contiguous server tick messages.
|
||||
3. Commands assigned to tick `N` are applied in server sequence order before
|
||||
simulation tick `N`.
|
||||
4. The server freezes location character observations and updates every player,
|
||||
NPC, taxi, and interactive object with exactly 100 ms. An empty player
|
||||
collection remains valid.
|
||||
5. Attack impacts are queued and applied only after all characters advance.
|
||||
Navigation recovery is applied inside the deterministic state path.
|
||||
6. The server invalidates its cached snapshot and advances its revision.
|
||||
7. Runtime locations consume the completed tick's value-only events and perform
|
||||
rendering, audio, particles, and supported presentation callbacks.
|
||||
8. Rendering can occur between ticks and never changes authoritative tick time.
|
||||
|
||||
Commands that update bound character or object state do not reload the whole location. This avoids replaying dialogue-node effects or Lua callbacks.
|
||||
|
||||
@@ -335,7 +340,8 @@ 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.
|
||||
map). Lockstep sends that authoritative document only in `initial_state`; later
|
||||
messages contain numbered command bundles.
|
||||
|
||||
- Campaign players are stored once in an ID-keyed `players` object.
|
||||
- Locations are keyed by location ID.
|
||||
@@ -394,29 +400,35 @@ 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
|
||||
## Multiplayer Lockstep
|
||||
|
||||
The refactor provides:
|
||||
Protocol v2 uses server-sequenced deterministic lockstep:
|
||||
|
||||
- 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.
|
||||
- The server assigns a connection to a preconfigured campaign player slot.
|
||||
- Connection receives one authoritative state at a completed tick plus its
|
||||
assigned player ID, tick duration, input delay, and compatibility fingerprint.
|
||||
- Destination input is normalized and scheduled two ticks in the future.
|
||||
- The server broadcasts every numbered tick, including empty ticks. Clients
|
||||
buffer out-of-order messages and advance only contiguous tick IDs.
|
||||
- All replicas apply the same globally sequenced commands before one exact
|
||||
100 ms update.
|
||||
- Post-tick canonical hashes exclude persistence timestamps and local
|
||||
presentation state. A mismatch is diagnosed but not automatically repaired.
|
||||
|
||||
Still required for real multiplayer:
|
||||
No wall-clock timestamp participates in simulation. `steady_clock` paces the
|
||||
server and local render accumulator only, so different machine clocks do not
|
||||
affect command placement or state. The current missing-packet policy is to stall;
|
||||
there is no prediction or rollback.
|
||||
|
||||
- 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.
|
||||
Current boundaries remain:
|
||||
|
||||
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.
|
||||
- only `SetCharacterDestinationCommand` crosses the network;
|
||||
- player characters must already exist in the initial state and campaign slot
|
||||
list; dynamic spawning and reconnect history are not implemented;
|
||||
- authentication and automatic checkpoint resynchronization are absent;
|
||||
- multiplayer Lua/dialogue shared behavior is unsupported;
|
||||
- deterministic compatibility targets identical executable and content builds.
|
||||
Floating-point simulation is not guaranteed bit-identical across platforms.
|
||||
|
||||
## Platform Build Integration
|
||||
|
||||
@@ -432,8 +444,9 @@ 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.
|
||||
a selected campaign/location with zero players. The Windows server adds
|
||||
Boost.Beast transport and runs a fixed 100 ms lockstep loop; other platforms
|
||||
continue to compile the portable state core without that transport.
|
||||
|
||||
The Windows Debug target was verified with:
|
||||
|
||||
@@ -455,8 +468,11 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
|
||||
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.
|
||||
- Pathfinding priority queues use cell index as a stable secondary key.
|
||||
- Gameplay randomness must be seeded and serialized; the `random_device` battle
|
||||
bot is disabled for multiplayer sessions.
|
||||
- A lockstep compatibility fingerprint must match before the client accepts the
|
||||
initial state.
|
||||
|
||||
## Main Files
|
||||
|
||||
@@ -469,6 +485,11 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
|
||||
- `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_server/NetworkProtocol.h` / `.cpp` — protocol-v2 messages,
|
||||
lockstep timeline, validation, and canonical hashes.
|
||||
- `src/game_server/WebsocketGameServer.h` / `.cpp` and
|
||||
`WebsocketGameClient.h` / `.cpp` — slot assignment, input scheduling, tick
|
||||
broadcasting, buffering, and replica advancement.
|
||||
- `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.
|
||||
|
||||
Reference in new issue
Block a user