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:
+38
-26
@@ -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
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user