Implement host-authoritative time/weather sync across server and clients. Adds a new worldState packet (and worldStateHost for host assignment) and server-side host tracking/reassignment to the lowest remaining playerId when the host disconnects. Plugin-side changes introduce F4T::WorldStateSync (new header + implementation), polling/apply hooks in the game-thread main loop, and Networking support to send/receive/parse worldState/worldStateHost packets. The server validates host-only sends, relays worldState to peers, broadcasts worldStateHost on host handoff, and augments logging/stats. Protocol and documentation files updated (protocol/world-state.md, docs/*) and server/fake_client.py extended to track/print host world-state snapshots.
78 lines
2.7 KiB
Markdown
78 lines
2.7 KiB
Markdown
# World State Packet
|
||
|
||
Host-authoritative time and weather sync for the Commonwealth Online prototype.
|
||
|
||
## Authority
|
||
|
||
- The server tracks a `worldStateHostPlayerId` for the session.
|
||
- The first connected client becomes host. When the host disconnects, the server
|
||
reassigns host to the lowest remaining `playerId` and broadcasts `worldStateHost`.
|
||
- Only the current host sends `worldState` packets.
|
||
- The Python relay validates the sender and broadcasts to all other clients.
|
||
|
||
Promoted hosts keep their already-synced local time and weather; reassignment should
|
||
not change world state for remaining clients.
|
||
|
||
## worldStateHost Packet
|
||
|
||
Sent in `welcome` (initial host) and broadcast when the host disconnects:
|
||
|
||
```json
|
||
{
|
||
"type": "worldStateHost",
|
||
"worldStateHostPlayerId": 2,
|
||
"serverTime": 1780212128.301
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `type` | string | yes | Must be `"worldStateHost"` |
|
||
| `worldStateHostPlayerId` | integer | yes | Connected player ID that is now world-state host |
|
||
| `serverTime` | number (double) | no | Relay timestamp |
|
||
|
||
The `welcome` packet also includes optional `worldStateHostPlayerId` so new joiners
|
||
know the current host immediately.
|
||
|
||
## Packet Format
|
||
|
||
```json
|
||
{
|
||
"type": "worldState",
|
||
"playerId": 1,
|
||
"gameHour": 14.25,
|
||
"gameDaysPassed": 12.5,
|
||
"weatherFormId": "00123456",
|
||
"clientTime": 1780212128.301,
|
||
"serverTime": 1780212128.301
|
||
}
|
||
```
|
||
|
||
## Field Definitions
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `type` | string | yes | Must be `"worldState"` |
|
||
| `playerId` | integer | yes (server) | Injected by server; identifies the sending host |
|
||
| `gameHour` | number (float) | yes | In-game hour of day, `0.0`–`24.0` |
|
||
| `gameDaysPassed` | number (float) | yes | In-game days passed global |
|
||
| `weatherFormId` | string | no | 8-character hex `TESWeather` form ID; omitted or `""` when host is indoors |
|
||
| `clientTime` | number (double) | no | Sender Unix epoch seconds |
|
||
| `serverTime` | number (double) | no | Relay timestamp added by server |
|
||
|
||
## Send Cadence (Host)
|
||
|
||
- About 1 Hz heartbeat while connected
|
||
- Immediate send when `weatherFormId` changes
|
||
- Immediate send when `gameHour` or `gameDaysPassed` jumps beyond a small threshold (wait, sleep, console time changes)
|
||
|
||
## Client Apply Rules
|
||
|
||
- **Time:** non-host clients snap local calendar globals when host values differ beyond threshold.
|
||
- **Weather:** non-host clients apply forced weather only when the local player is in an exterior cell.
|
||
- Missing optional fields use safe defaults on receive.
|
||
|
||
## Backward Compatibility
|
||
|
||
Older clients that do not send or parse `worldState` continue to work. Transform sync is unchanged.
|