Introduce an optional timeSync field and event-driven time pushes so host time is only re-applied on connect, resume-from-freeze, or large jumps while weather continues on ~1 Hz heartbeats. Added IsGameTimeFrozen detection and client buffering of latest host time; clients apply host time only when timeSync is true or when gameplay just resumed. Updated networking to read/write timeSync, adjusted F4TWorldStateSync logic (separate time vs weather send/apply paths, changed function signatures and behavior), and updated protocol/docs/changelog and fake_client logging to reflect the new field and behavior.
83 lines
3.3 KiB
Markdown
83 lines
3.3 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",
|
||
"timeSync": true,
|
||
"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 |
|
||
| `timeSync` | boolean | no | When `true`, non-host clients should apply `gameHour` and `gameDaysPassed`. Omitted or `false` on weather-only heartbeats. |
|
||
| `clientTime` | number (double) | no | Sender Unix epoch seconds |
|
||
| `serverTime` | number (double) | no | Relay timestamp added by server |
|
||
|
||
## Send Cadence (Host)
|
||
|
||
- About 1 Hz heartbeat for **weather** while connected (`timeSync` omitted or `false`)
|
||
- Immediate send with `timeSync: true` when:
|
||
- First connected or promoted to host
|
||
- Game time resumes after a freeze (menus, pip-boy, dialogue, loading screens, save/load, wait/sleep)
|
||
- `gameHour` or `gameDaysPassed` jumps beyond a small threshold (wait, sleep, console time changes)
|
||
- Immediate send when `weatherFormId` changes (includes current time fields; sets `timeSync` only when time also changed or on resume)
|
||
|
||
## Client Apply Rules
|
||
|
||
- **Time:** non-host clients track the latest host time from every packet but only snap local calendar globals when `timeSync` is `true` and gameplay time is not frozen, or immediately when gameplay time resumes after a freeze (using the latest stored host values).
|
||
- **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.
|