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:
+21
-15
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user