177 lines
6.9 KiB
Markdown
177 lines
6.9 KiB
Markdown
# WebSocket Deterministic Lockstep Networking
|
|
|
|
## Scope
|
|
|
|
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 current command surface is intentionally limited to
|
|
`SetCharacterDestinationCommand`.
|
|
|
|
## Runtime Architecture
|
|
|
|
### Server
|
|
|
|
`WebsocketGameServer` wraps the headless `InternalGameServer`.
|
|
|
|
- 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
|
|
|
|
`WebsocketGameClient` implements `IGameServer`.
|
|
|
|
- It connects asynchronously to `ws://127.0.0.1:8080/`.
|
|
- Received messages are queued by the network thread and applied on the game
|
|
thread from `update()`.
|
|
- It maintains an internal `InternalGameServer` replica so existing snapshot
|
|
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()` 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 replica and presents completed-tick events. It never
|
|
advances simulation from a render-frame delta.
|
|
|
|
## Campaign Routing
|
|
|
|
The graphical game uses stable router objects for `IGameServer` and
|
|
`IGameServerHost`, allowing existing UI and runtime references to remain valid.
|
|
|
|
When `NETWORK` is defined:
|
|
|
|
- `battle_zone_multiplayer` uses `WebsocketGameClient` and `RemoteGameServerHost`.
|
|
- `main`, `battle_zone`, and every other campaign use `InternalGameServer` and
|
|
`LocalGameServerHost`.
|
|
|
|
The WebSocket client is created only when `battle_zone_multiplayer` is selected
|
|
and is destroyed when that campaign is unloaded. The single-player and
|
|
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 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 v2
|
|
|
|
Every message uses a versioned JSON envelope:
|
|
|
|
```json
|
|
{
|
|
"protocolVersion": 2,
|
|
"type": "tick",
|
|
"payload": {
|
|
"tickId": 81,
|
|
"commands": [],
|
|
"stateHash": "0123456789abcdef"
|
|
}
|
|
}
|
|
```
|
|
|
|
Message types are:
|
|
|
|
- `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.
|
|
|
|
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
|
|
|
|
Networking is enabled only for Windows:
|
|
|
|
- `proj-windows/CMakeLists.txt` defines `NETWORK`, compiles the WebSocket client
|
|
and remote host, and links `ws2_32` and `mswsock`.
|
|
- `server/CMakeLists.txt` defines `NETWORK` for the Windows `GameServer`, compiles
|
|
the WebSocket server, and links the Windows socket libraries.
|
|
- Linux, Android, and Web builds do not define `NETWORK` and do not compile the
|
|
network transport.
|
|
|
|
The previous Web-build `NETWORK` definition was removed.
|
|
|
|
## Running
|
|
|
|
Build the server:
|
|
|
|
```powershell
|
|
cmake -S server -B server/build
|
|
cmake --build server/build --config Debug --target GameServer
|
|
```
|
|
|
|
Run it from the repository root:
|
|
|
|
```powershell
|
|
.\server\build\Debug\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
|
|
```
|
|
|
|
The listener defaults to `0.0.0.0:8080`. It can be changed with:
|
|
|
|
```text
|
|
--address IP
|
|
--port N
|
|
```
|
|
|
|
Launch the Windows game and select `battle_zone_multiplayer`. The client connects to
|
|
`127.0.0.1:8080`. Selecting another campaign continues to use the local server.
|
|
|
|
## Verification
|
|
|
|
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
|
|
|
|
- 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.
|