Set up basic deterministic lockstep

This commit is contained in:
Vladislav Khorev committed 2026-11-05 23:02:53 +06:00
1 parent 28c6e83eda
commit 23fef6f349
26 files changed
+1172 -239

No files matched your search

+69 -50
View File
@@ -1,13 +1,15 @@
# WebSocket Networking: First Iteration
# WebSocket Deterministic Lockstep Networking
## Scope
The first networking iteration adds a Windows-only WebSocket transport using
Boost.Asio and Boost.Beast.
The Windows WebSocket transport uses Boost.Asio and Boost.Beast to run a
server-sequenced deterministic lockstep simulation. A client receives one
authoritative state at a completed tick boundary. After that, the server sends
only numbered tick messages containing ordered player commands (or an empty
command list).
The goal of this iteration is connection and replication stability. It does not
yet implement multiplayer commands, authentication, per-connection players,
state deltas, or presentation-event replication.
The current command surface is intentionally limited to
`SetCharacterDestinationCommand`.
## Runtime Architecture
@@ -15,16 +17,17 @@ state deltas, or presentation-event replication.
`WebsocketGameServer` wraps the headless `InternalGameServer`.
- It accepts any WebSocket connection.
- It sends the current complete game state immediately after connection.
- It broadcasts the current complete state every five seconds.
- Incoming WebSocket messages are read and discarded.
- The authoritative simulation remains on the main server thread.
- Network sessions only receive serialized state and never access
`InternalGameServer` directly.
The standalone server creates the existing fixed player ID `"player"` and does
not enable the battle-zone test bot.
- The simulation tick is fixed at 100 ms.
- Each accepted connection is assigned the first free character ID from the
campaign's `networkPlayerSlots`; the connection is rejected when all slots
are occupied.
- The initial message contains the state, completed snapshot tick, assigned
player ID, tick duration, two-tick input delay, and compatibility fingerprint.
- Destination inputs cross from the Asio thread through a queue. The simulation
thread validates client sequence numbers, replaces client identity with the
assigned slot, and schedules commands two ticks in the future.
- Every tick is broadcast, including ticks with no commands. No periodic state
snapshots are sent.
### Client
@@ -37,14 +40,19 @@ not enable the battle-zone test bot.
consumers and transitional runtime-state bindings continue to work.
- Camera orientation lives in host-owned `LocationLocalState`, so incoming
authoritative snapshots cannot overwrite camera movement.
- `submit()` intentionally discards all commands.
- `drainEvents()` returns no events.
- `submit()` serializes destination intent only. Other command variants remain
local no-ops for a remote connection.
- Tick bundles are buffered by ID. The replica advances only while the next
contiguous tick is available, applying its commands before one exact 100 ms
simulation step.
- Completed simulation events are passed to the runtime `Location` for
presentation, and ordinary server events are drainable through `IGameServer`.
- Client-side save and load calls are ignored because persistence remains
server-side.
`RemoteGameServerHost` prepares local presentation resources and runtime
locations against the replicated state. It does not advance authoritative
simulation or write Lua/dialogue auxiliary state back to the server.
locations against the replica and presents completed-tick events. It never
advances simulation from a render-frame delta.
## Campaign Routing
@@ -63,37 +71,53 @@ multiplayer Battle Zone entries share location content and the start-state path,
so the state can be regenerated through the single-player entry. At startup, the
standalone server loads the authoritative half of that shared save and rewrites
its campaign ID to `battle_zone_multiplayer`; each remote client restores only
the local presentation half.
the local presentation half. The multiplayer campaign currently declares the
pre-existing `player` and `player2` state entries as connection
slots; the random test-bot controller is disabled.
The campaign's `networkCompatibility` token is the fingerprint; release builds
must bump it whenever deterministic content changes.
If DNS resolution, TCP connection, the WebSocket handshake, or the active socket
fails, the client unloads the remote campaign and returns to the campaign
selection screen.
## JSON Protocol
## JSON Protocol v2
Messages use a versioned JSON envelope:
Every message uses a versioned JSON envelope:
```json
{
"protocolVersion": 1,
"type": "state",
"protocolVersion": 2,
"type": "tick",
"payload": {
"schemaVersion": 2
"tickId": 81,
"commands": [],
"stateHash": "0123456789abcdef"
}
}
```
The payload uses the authoritative `GameState::save()` representation and excludes
the client-local location map, including camera orientation.
The client applies it through `InternalGameServer::load()`.
Message types are:
Malformed JSON, unsupported protocol versions, unsupported message types, and
invalid state payloads are logged and ignored.
- `initial_state` (server to client): one state snapshot and lockstep metadata;
- `destination_input` (client to server): monotonic client sequence and a
centimetre-normalized finite target;
- `tick` (server to client): tick ID, globally sequenced commands, and
post-tick diagnostic state hash;
- `error` (server to client): invalid input, incompatibility, or full server.
Single-player persistence uses a separate envelope that combines this authoritative
document with client-local location state. The temporary full-state network format
is still expected to be replaced by a dedicated replication schema or deltas as
command handling and production replication are implemented.
Protocol v1 is not compatible. Malformed payloads, non-finite or out-of-range
coordinates, duplicate client sequences, and unsupported commands cannot enter
the authoritative command stream.
WebSocket ordering is not used as simulation time. Integer tick IDs are the
only authoritative clock. The server uses `steady_clock` only to pace its loop,
and clients may have different wall clocks without affecting the result. A
client stalls on a missing tick instead of predicting or rolling back.
Single-player persistence remains a separate envelope combining authoritative
and client-local state. `savedAt`, camera state, and presentation objects are
excluded from the diagnostic deterministic hash.
## Build Configuration
@@ -135,23 +159,18 @@ Launch the Windows game and select `battle_zone_multiplayer`. The client connect
## Verification
The implementation was verified with:
- A successful Windows graphical-game build.
- A successful Windows headless-server build.
- Server state and protocol tests.
- A WebSocket smoke test receiving the initial state and the next five-second
broadcast.
- A check that the shared `"player"` exists and the battle-zone test bot is not
created for the network server.
CTest covers protocol-v2 round trips and validation, input-delay ordering,
fixed-tick replica behavior, campaign slot metadata, initial connection and
slot assignment, destination submission, and contiguous tick consumption.
## Current Limitations
- All clients share the fixed player ID `"player"`.
- Client commands are not sent to the server.
- The server does not process commands.
- State is sent as a complete snapshot rather than a delta.
- There is no authentication, authorization, reconnect loop, or session identity.
- Presentation events are not replicated.
- Only preconfigured player slots can connect; dynamic join/spawn commands are
not implemented.
- There is no authentication or reconnect/replay history.
- A hash mismatch is logged but does not yet trigger checkpoint resynchronization.
- The endpoint is hardcoded in the graphical client.
- Multiplayer Lua and dialogue behavior remains unsupported.
- Determinism is supported for identical executable/content builds. The
floating-point simulation is not guaranteed bit-identical across platforms
or compilers.