Add appearance sync and late-join replay

Adds optional `appearance` snapshots to transform packets, applies supported appearance data to runtime proxies, and has the server replay stored transforms to newly connected clients so late joiners receive current visuals immediately.
This commit is contained in:
2026-06-29 16:46:50 +12:00
parent ad7f287899
commit 4b8f5246c4
15 changed files with 772 additions and 38 deletions
+38 -26
View File
@@ -12,9 +12,9 @@ The current system is still a local prototype, but the Fallout 4 plugin now has
both send and receive paths:
1. The Fallout 4 plugin reads local player transform data.
2. The plugin derives basic movement state and captures a visible equipped
apparel snapshot on the game-thread polling path, then sends both as optional
data on transform packets.
2. The plugin derives basic movement state and captures visible equipped
apparel and player appearance snapshots on the game-thread polling path, then
sends them as optional data on transform packets.
3. The plugin sends transform packets to the Python relay server. The default
dev setup connects to `127.0.0.1:7777` on the same PC; the server itself
listens on `0.0.0.0:7777` so other machines can connect using the host
@@ -24,11 +24,15 @@ both send and receive paths:
5. The plugin receives its `welcome` packet and stores its assigned `playerId`.
6. The server adds `playerId` and `serverTime` to transform packets.
7. The server broadcasts transform packets to other connected clients.
8. The plugin and `server/fake_client.py` store remote player state by `playerId`.
9. The server broadcasts `disconnect` packets when clients disconnect.
10. The plugin and fake client remove disconnected players from their remote
8. When a new client connects, the server immediately sends the last stored
transform snapshot for each already-connected player to that new client so
late joiners receive current position, equipment, and appearance state
without waiting for each sender's next heartbeat.
9. The plugin and `server/fake_client.py` store remote player state by `playerId`.
10. The server broadcasts `disconnect` packets when clients disconnect.
11. The plugin and fake client remove disconnected players from their remote
player tables.
11. The server tracks a world-state host (`worldStateHostPlayerId`). The first
12. 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 `worldState` packets with `gameHour`, `gameDaysPassed`, optional
@@ -38,29 +42,29 @@ both send and receive paths:
`serverWorldState` to all clients for admin weather (`fw`) and time
(`set gamehour to HHmm`) commands. Promoted hosts continue with their
already-synced local time and weather so remaining clients should not see a jump.
12. The plugin treats `COVault109` as a solo cell for first-time character
13. The plugin treats `COVault109` as a solo cell for first-time character
creation. On entry it can send one location update so other clients hold any
stale proxy, then outgoing transform packets are suppressed and remote proxy
representation is skipped while the local player remains inside. Looks and
SPECIAL setup therefore stay visually single-player. The relay protocol is
unchanged; the rule is enforced client-side by the plugin.
13. On the game-thread update path, the proxy controller reads a copied
14. 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.
14. For each eligible remote `playerId`, the controller maintains a
15. 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.
15. Runtime proxies are demand-driven. A slot executes the Stage 3.7
16. 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.
16. Runtime proxies receive a small game-thread neutralization pass when a
17. 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
@@ -70,61 +74,62 @@ 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.
17. Proxy visual updates are independent per slot. Normal movement is smoothed
18. Proxy visual updates are tracked per slot. Normal movement is smoothed
toward that player’s latest target, while `cell_change`, `worldspace_change`,
and `teleport` snap directly. Curated locomotion animation graph variables
and optional equipped apparel snapshots are applied on the game thread after
the proxy has loaded 3D. Visual crouch, full weapon model state, power armor,
and raw actor-state bitfield application remain outside the current sync path.
18. When a remote player leaves the cell or disconnects, only that player’s
plus optional equipped apparel and appearance snapshots are applied on the
game thread after the proxy has loaded 3D. Gender switching, visual crouch,
full weapon model state, power armor, and raw actor-state bitfield
application remain outside the current sync path.
19. 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.
19. The placed fallback reference `F4TProxyRemotePlayer01REF` remains a single
20. 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.
20. As part of the staged runtime proxy actor manager work, the same game-thread
21. 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.
21. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy:
22. 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.
22. Stage 3.2 adds a temporary near-player visibility hold for that same single
23. 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.
23. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
24. 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.
24. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic
25. 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.
25. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
26. 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.
26. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots
27. 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.
27. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the
28. 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
@@ -167,6 +172,9 @@ The native plugin is responsible for:
local player is in solo cells such as `COVault109`
- Adding basic data-only movement state to transform packets
- Adding optional visible equipped apparel snapshots to transform packets
- Adding optional male-proxy-compatible appearance snapshots to transform
packets for body tint, height/body morph weight, hair colors, head parts, and
morph slider values
- Throttling normal transform sends separately from readable local movement logs
- Receiving server packets on a background thread
- Storing assigned and remote `playerId` state internally
@@ -201,6 +209,9 @@ The native plugin is responsible for:
- Applying optional `equippedItems` apparel snapshots to runtime proxies after
proxy 3D is ready, including explicit tracked-slot unequips for removed
clothing, hats, and eyewear
- Applying optional `appearance` snapshots to runtime proxies after proxy 3D is
ready, without switching gender and with race switching deferred until proxy
base compatibility is available
- Suppressing exposed AI movement intent for runtime proxies by clearing safe
process target handles and using validated package interruption/do-nothing
calls when combat, pathing, flee/alarm, bump, or package movement state is
@@ -235,6 +246,7 @@ The server is responsible for:
- Accepting client connections
- Assigning player IDs
- Sending welcome packets
- Sending existing players' last transform snapshots to newly connected clients
- Receiving player state
- Broadcasting player state
- Tracking disconnects
+83
View File
@@ -9,6 +9,89 @@ failed experiments, successful tests, and next steps.
---
## 2026-06-29 - Late-Join Appearance Snapshot Fix
### Summary
Fixed an asymmetric appearance case where the second client could see only the default proxy for an already-connected first client.
### Files Changed
- `server/server_core.py`
- `protocol/player-sync.md`
- `docs/architecture.md`
- `changelog.md`
- `docs/dev-log.md`
### Details
- The server already stored each client's last transform, but only broadcast future transform packets.
- Added an immediate replay of existing clients' last transform snapshots after a new client's `welcome` packet.
- The replay preserves optional transform fields such as `equippedItems` and `appearance`, so late joiners seed proxy visuals before the sender's next heartbeat.
### Testing
- Ran `python -m py_compile server\server_core.py server\fake_client.py server\fake_player.py`; syntax check passed.
- Recommended manual check: start Client 1, wait for a transform with appearance to reach the server, then connect Client 2 and confirm Client 2's proxy for Client 1 updates appearance immediately.
### Known Issues
- In-game confirmation is still needed.
### Next Steps
- Re-run the two-client appearance checklist in both join orders.
---
## 2026-06-29 - Proxy Appearance Sync
### Summary
Added a versioned optional `appearance` transform snapshot so runtime proxies can mirror supported remote player body and face appearance fields without introducing gender switching yet.
### Files Changed
- `plugin/include/F4TRemotePlayerState.h`
- `plugin/include/F4TNetworking.h`
- `plugin/src/F4TNetworking.cpp`
- `plugin/src/main.cpp`
- `plugin/src/F4TProxyActorController.cpp`
- `server/fake_client.py`
- `server/fake_player.py`
- `protocol/packets.md`
- `protocol/player-sync.md`
- `docs/protocol.md`
- `docs/architecture.md`
- `changelog.md`
- `docs/dev-log.md`
### Details
- Added optional `appearance` transform data with a payload version, race form ID for diagnostics/future use, height, body morph weight, body tint, hair/facial hair color forms, head-part IDs, and morph slider values.
- Captured local player appearance on the game-thread polling path and made appearance changes trigger an immediate transform send.
- Parsed remote appearance defensively on the networking thread so older packets without the field still work and malformed subfields do not reject the transform.
- Applied supported appearance fields on the game-thread proxy path after loaded 3D is available, while deferring race switching and omitting gender until female proxy support exists.
- Updated fake clients so relay tests can display and synthesize appearance payloads.
### Testing
- Ran `python -m py_compile server\fake_client.py server\fake_player.py`; syntax check passed.
- Ran `build.bat`; plugin build succeeded. The build still reports pre-existing-style unreachable-code warnings in proxy/local animation debug code.
- In-game testing still needs to be run.
- Expected relay result: `server/fake_client.py` prints `appearance` from transform packets and old packets without the field still parse.
- Manual multiplayer checklist:
- Start the Python server and connect one Fallout 4 instance.
- Connect a second Fallout 4 instance in the same playable cell.
- Verify outgoing plugin transform packets include `appearance` after connect and after Looks/body changes.
- Verify the server receives and relays the field without dropping movement or `equippedItems`.
- Verify the receiving plugin parses remote appearance without warnings for valid vanilla forms.
- Verify the proxy updates supported body tint, hair color, weight/body morph, head-part, and morph slider fields where the receiver can resolve the forms.
- Check `CommonwealthOnline.log` for appearance apply, deferred race, missing-form, and proxy 3D refresh messages.
### Known Issues
- Gender switching is intentionally not included yet.
- Race switching is captured but deferred until proxy-base compatibility is validated.
- Head-part application only updates same-count head-part arrays in this first pass; mismatched counts are logged and skipped.
- In-game confirmation is still needed to verify all applied `TESNPC` appearance fields refresh reliably on runtime proxies.
### Next Steps
- Run the two-client in-game checklist.
- Add female proxy support and decide how to map race/gender/base compatibility before enabling runtime race switching.
- If same-count head-part replacement is too limited, add a safer per-proxy actor-base clone or validated head-part allocation path.
---
## 2026-06-29 - Clothing Sync
### Summary
+3 -3
View File
@@ -60,7 +60,7 @@ If UDP discovery is unavailable, the plugin may fall back to probing
The protocol should be testable outside Fallout 4 before it is used inside the F4SE plugin.
`transform` packets carry position, movement state, and optional
`equippedItems` apparel snapshots for visible clothing/armor proxy sync. The
canonical field list and compatibility rules live in
`transform` packets carry position, movement state, optional `equippedItems`
apparel snapshots, and optional `appearance` snapshots for best-effort proxy
body/face visuals. The canonical field list and compatibility rules live in
[`protocol/packets.md`](../protocol/packets.md).