Add host-authoritative world-state sync

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.
This commit is contained in:
2026-06-22 21:50:59 +12:00
parent 9ceb6601ae
commit 99595ef87c
15 changed files with 1042 additions and 33 deletions
+21 -15
View File
@@ -27,23 +27,29 @@ both send and receive paths:
9. The server broadcasts `disconnect` packets when clients disconnect.
10. The plugin and fake client remove disconnected players from their remote
player tables.
11. On the game-thread update path, the proxy controller reads a copied
11. The server tracks a world-state host (`worldStateHostPlayerId`). The first
connected client becomes host; when the host disconnects, the server reassigns
host to the lowest remaining `playerId` and broadcasts `worldStateHost`. The
host sends low-frequency `worldState` packets with `gameHour`, `gameDaysPassed`,
and optional `weatherFormId`. Promoted hosts continue with their already-synced
local time and weather so remaining clients should not see a jump.
12. On the game-thread update path, the proxy controller reads a copied
remote-player snapshot before attempting any actor work. The snapshot is
sorted by `playerId`, and only remote players with valid transform data in
the same cell are eligible for runtime proxy representation.
12. For each eligible remote `playerId`, the controller maintains a
13. For each eligible remote `playerId`, the controller maintains a
`remotePlayerId -> runtime proxy slot` mapping. Each slot stores its
`ObjectRefHandle`, lifecycle state, PlaceAtMe-backed spawn/candidate state,
movement smoothing state, and data-only animation debug state. Stage 4 caps runtime
proxy slots at four actors for testing.
13. Runtime proxies are demand-driven. A slot executes the Stage 3.7
14. Runtime proxies are demand-driven. A slot executes the Stage 3.7
PlaceAtMe-backed spawn flow only when that remote player needs a same-cell
representation, and only one PlaceAtMe spawn attempt may run at a time so
candidate isolation remains unambiguous. Stage 4 captures the remote
player's current valid transform before spawning and moves the isolated
runtime actor to that transform immediately, so normal remote representation
does not visually start at the local player offset.
14. Runtime proxies receive a small game-thread neutralization pass when a
15. Runtime proxies receive a small game-thread neutralization pass when a
PlaceAtMe-backed actor is promoted, when a reusable slot is reassigned, while
active on a throttle, and when moved or kept in holding. The pass only uses
confirmed CommonLibF4 actor APIs/fields to stop combat, clear combat target
@@ -53,60 +59,60 @@ both send and receive paths:
alarm, bump, or package movement intent is observed. It does not disable,
hide, delete, despawn, alpha-fade, or force animation graph events on the
actor.
15. Proxy visual updates are independent per slot. Normal movement is smoothed
16. Proxy visual updates are independent per slot. Normal movement is smoothed
toward that player’s latest target, while `cell_change`, `worldspace_change`,
and `teleport` snap directly. Movement and animation-related remote state is
still observed and logged only; no visual crouch/sneak, jump, weapon drawn,
locomotion, animation graph, or actor-state application is attempted.
16. When a remote player leaves the cell or disconnects, only that player’s
17. When a remote player leaves the cell or disconnects, only that player’s
runtime proxy is moved to the hidden holding position inside
`F4TTestCell01`. Runtime actors are not disabled or deleted. Connected
players who leave the cell keep their slot reserved, while disconnected
held slots become reusable only after the configured grace period. Runtime
slots use per-slot holding positions with spacing and a game-thread
correction check so held actors do not stack or drift back into view.
17. The placed fallback reference `F4TProxyRemotePlayer01REF` remains a single
18. The placed fallback reference `F4TProxyRemotePlayer01REF` remains a single
backup representation. If runtime spawning fails for one deterministic
selected remote player, the fallback can represent that one player only; it
is not used for multiple remote players.
18. As part of the staged runtime proxy actor manager work, the same game-thread
19. As part of the staged runtime proxy actor manager work, the same game-thread
proxy controller also logs whether `Fallout4Together_Test.esp` is loaded and
whether the actor base `F4T_RemotePlayerProxy` resolves by editor ID.
19. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy:
20. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy:
post-spawn visibility data, loaded-3D pointer checks where safe, console
helper logs, active source logs, and throttled post-movement diagnostics.
The diagnostic mode can force the runtime proxy near the local player to
prove whether the created actor is visibly renderable.
20. Stage 3.2 adds a temporary near-player visibility hold for that same single
21. Stage 3.2 adds a temporary near-player visibility hold for that same single
runtime proxy. During the hold, remote state is still selected and validated,
but runtime proxy restore and remote movement are deferred long enough to
test whether a runtime-created actor ref can load visible 3D while kept near
the local player.
21. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
22. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
to resolve Codsworth as a visible vanilla `TESNPC` candidate, then passes
that actor base into the same runtime spawn, handle validation, near-player
hold, and visibility diagnostic path. This isolates custom actor-base issues
from runtime-spawn/init issues without changing the final architecture.
22. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic
23. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic
layer. It compares Codsworth base-form runtime spawning against moving the
existing placed Codsworth reference near the local player as a visibility
control. This separates runtime actor creation/init problems from vanilla
actor-base or placed-reference movement problems and does not change the
final architecture.
23. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
24. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
console validation proved `player.placeatme 000179FF 1` can spawn visible
Codsworth in `F4TTestCell01`. The plugin executes that console placement path
from the local player, resolves the newly created Codsworth actor near the
player, and holds/logs it separately from the `CreateReferenceAtLocation`
runtime proxy path.
24. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots
25. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots
known Codsworth refs before executing `player.placeatme 000179FF 1`, accepts
only a newly isolated Codsworth ref for the primary diagnostic, and preserves
that actor at the original console-spawned location during a short settle
window. This tests whether the actor becomes visible naturally or requires a
render/process refresh such as the manually observed pause-menu or alt-tab
behavior. Stage 3.6 does not change the final architecture.
25. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the
26. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the
preferred single runtime proxy spawn path. The proxy controller uses the
resolved runtime FormID for `F4T_RemotePlayerProxy` when available, falls
back to Codsworth only for diagnostics, defers active proxy selection during
+77
View File
@@ -7,6 +7,83 @@ failed experiments, successful tests, and next steps.
---
---
## 2026-06-22 - World-State Host Reassignment
### Summary
When the world-state host disconnects, the server reassigns host to the lowest remaining `playerId` and broadcasts `worldStateHost`. Promoted clients seed host capture from their already-synced local calendar and weather so time and weather should not jump for remaining players.
### Files Changed
- `plugin/include/F4TWorldStateSync.h`
- `plugin/src/F4TWorldStateSync.cpp`
- `plugin/src/F4TNetworking.cpp`
- `server/server_core.py`
- `server/fake_client.py`
- `protocol/world-state.md`
- `protocol/message-types.md`
- `protocol/world-sync.md`
- `docs/architecture.md`
### Details
- Server tracks `_world_state_host_player_id` and includes `worldStateHostPlayerId` in `welcome`.
- New `worldStateHost` packet notifies clients when host changes after disconnect.
- Plugin `IsWorldStateHost()` compares assigned `playerId` to tracked host id.
- On promotion, pending apply state is cleared and `lastSent` is seeded from local time/weather to avoid an immediate disruptive resync.
### Testing
- Not run in-game in this session.
- Recommended: connect three clients, disconnect player 1, verify server reassigns to player 2 and remaining clients keep the same time/weather.
### Known Issues
- None currently known.
### Next Steps
- In-game verification of host handoff with two or three Fallout 4 instances.
---
## 2026-06-22 - Time and Weather Sync MVP
### Summary
Added host-authoritative time and weather sync via a new `worldState` packet. The first connected client (`playerId == 1`) publishes `gameHour`, `gameDaysPassed`, and optional `weatherFormId`; the Python relay validates and broadcasts to other clients; non-host plugin clients apply time and exterior weather on the game thread.
### Files Changed
- `plugin/include/F4TWorldStateSync.h`
- `plugin/include/F4TNetworking.h`
- `plugin/src/F4TWorldStateSync.cpp`
- `plugin/src/F4TNetworking.cpp`
- `plugin/src/main.cpp`
- `server/server_core.py`
- `server/fake_client.py`
- `protocol/world-state.md`
- `protocol/message-types.md`
- `protocol/world-sync.md`
- `docs/protocol.md`
- `docs/protocol-phase2-extensions.md`
- `docs/architecture.md`
### Details
- New `worldState` packet type with ~1 Hz host heartbeat plus immediate sends on weather or large time jumps.
- Server drops `worldState` from non-host clients and relays host packets to peers only.
- Non-host clients queue pending state on the network thread and apply calendar globals plus `Sky::ForceWeather` on the game thread (weather apply is exterior-only).
- `fake_client.py` tracks and prints the latest host world-state snapshot.
### Testing
- Not run in-game in this session.
- Recommended: start `server/server.py`, connect host first, connect `fake_client.py` or second game instance, change host time/weather outdoors, verify relay logs and client apply logs in `CommonwealthOnline.log`.
### Known Issues
- Host reassignment on disconnect is not implemented; clients keep the last applied state.
- Full calendar fields (`gameYear`, `gameMonth`, `gameDay`, `timeScale`) are out of scope for this MVP.
### Next Steps
- In-game verification with two Fallout 4 instances outdoors.
- Consider host handoff when player 1 disconnects.
- Add optional `fake_player.py` host simulation flag if useful for protocol testing without the game.
---
## 2026-06-18 - LAN scan fixes for cross-device discovery
### Summary
+19
View File
@@ -93,6 +93,25 @@ This document describes the Phase 2 protocol extensions to support actor state f
---
## World State Packet
Host-authoritative time and weather sync uses a separate `worldState` packet.
Full field definitions live in [`protocol/world-state.md`](../protocol/world-state.md).
```json
{
"type": "worldState",
"playerId": 1,
"gameHour": 14.25,
"gameDaysPassed": 12.5,
"weatherFormId": "00123456",
"clientTime": 1717424000.123,
"serverTime": 1717424000.456
}
```
---
## Backward Compatibility
**Important:** The protocol is **backward compatible**. Existing clients that don't send Phase 2 fields will continue to work:
+2
View File
@@ -14,6 +14,8 @@ Long-term versions may move to a binary protocol if performance becomes a proble
- `welcome`
- `transform`
- `worldState`
- `worldStateHost`
- `disconnect`
- `ping`
- `pong`