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
+69
-50
@@ -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.
|
||||
Reference in new issue
Block a user