Implement proxy weapon animation sync for ranged guns
Milestone 1: Proxy actors now equip remote players' right-hand weapons and play armed idle/locomotion poses when weaponDrawn=true. Adds rightHand weapon slot to equippedItems protocol, weapon graph variable support (iSyncGunDown: 0=drawn, 1=holstered), and weapon FSM events (weaponDraw, readyStateEnter, gunDownStateEnter). Fixes two critical animation state churn bugs: frustum-visibility flicker no longer resets animation state on every update tick, and idle proxies no longer register as changed on every frame. Includes WeaponBehavior graph loading via AIProcess::RequestLoadAnimationsForWeaponChange() and manual weapon attachment fallback. New docs/weapon-animation-sync.md reference guide maps animation events and variables from F4-Animation-Research. Experimental alert-state reassertion for armed proxies to test NPC combat state gating of weapon-ready poses.
This commit is contained in:
@@ -10,6 +10,14 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Experimental proxy weapon alert forcing: while a remote player reports `weaponDrawn=true`, runtime proxies now periodically execute scoped `<proxyRef>.setalert 1` before weapon animation sync, then clear alert on holster, to test whether Fallout 4's NPC alert state is what keeps weapon-ready poses alive.
|
||||||
|
- One-time-per-proxy diagnostic log (`Runtime proxy weapon graph DIAGNOSTIC`) reporting the loaded animation-graph and bound-channel counts when a proxy first has a weapon equipped, to help determine why weapon FSM events are being rejected by the graph.
|
||||||
|
- Weapon animation sync for ranged guns: proxy actors now equip remote players' right-hand weapons and play armed idle/locomotion poses when `weaponDrawn=true`.
|
||||||
|
- `rightHand` weapon slot to `equippedItems` protocol: syncs equipped `WEAP` form IDs alongside apparel items. Optional field; unresolved weapons skip with throttled warning (no crash).
|
||||||
|
- Weapon-drawn animation state tracking via `iSyncGunDown` graph variable: `0` = drawn/ready, `1` = holstered/gun-down. Fires `weaponDraw` and `readyStateEnter` events on draw, `gunDownStateEnter` on holster.
|
||||||
|
- Local player right-hand weapon capture (`GetEquippedWeaponFormId`) and network transmission in transform packets.
|
||||||
|
- `docs/weapon-animation-sync.md` reference guide documenting WeaponBehavior graph events, variables, and clip naming from F4-Animation-Research unpacked XML.
|
||||||
|
- Weapon candidate graph variables to local animation graph debug probe: `iSyncGunDown`, `iRifleDrawnStateID`, `iAttackState`, `isFiring`, etc. for manual in-game validation.
|
||||||
- Quest auto-start on COVault109 exit: "Out of Time" quest (MQ102) now starts automatically via console `startquest`/`setstage` commands when the player leaves COVault109 for the first time.
|
- Quest auto-start on COVault109 exit: "Out of Time" quest (MQ102) now starts automatically via console `startquest`/`setstage` commands when the player leaves COVault109 for the first time.
|
||||||
- Complexion texture form reference (`complexionFormId`) to appearance payload so hands, arms, and body match the synced skin tone (follows the existing hair colour pattern).
|
- Complexion texture form reference (`complexionFormId`) to appearance payload so hands, arms, and body match the synced skin tone (follows the existing hair colour pattern).
|
||||||
- Appearance protocol version 3 with an optional `tints` field (character tint layers: skin tone, complexion, makeup, beard shade) so proxies can mirror the sender's skin colour and facial detail.
|
- Appearance protocol version 3 with an optional `tints` field (character tint layers: skin tone, complexion, makeup, beard shade) so proxies can mirror the sender's skin colour and facial detail.
|
||||||
@@ -37,6 +45,10 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
|
|||||||
- `server/fake_client.py` parsing and output for relayed `equippedItems` transform data.
|
- `server/fake_client.py` parsing and output for relayed `equippedItems` transform data.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- **Weapon animation graph now loads on proxies.** Proxy actors now trigger `AIProcess::RequestLoadAnimationsForWeaponChange()` after weapon equip/unequip, which loads the `WeaponBehavior` animation subgraph. This allows weapon events (`weaponDraw`, `readyStateEnter`, `gunDownStateEnter`) to be accepted by the animation system and enables proper weapon-drawn state animations to persist instead of immediately reverting to idle/holstered poses. Fixes: weapon briefly appears then holsters immediately on proxy.
|
||||||
|
- Proxy weapon attachment now avoids the generic queued equipment 3D refresh after a successful manual weapon attach, preventing the freshly attached weapon model from being immediately cleared again. Weapon draw/holster transitions and subsequent drawn-weapon updates also re-run weapon reparent/draw calls as a fallback while Fallout 4's `WeaponBehavior` graph is still not accepting the standard weapon animation events on proxies. Drawn-weapon states now use targeted graph writes instead of descriptor bulk writes so unrelated weapon graph variables are not clobbered back to idle/holstered defaults.
|
||||||
|
- Runtime proxy animation state no longer resets on every camera-frustum visibility flicker; only genuine 3D teardown/reload (already handled separately) now clears applied animation state. Previously, `IsVisible()` flapping on culled proxies caused the entire animation state to be treated as "just initialized" on effectively every update tick, forcing edge-triggered events (including `weaponDraw`/`readyStateEnter`/`gunDownStateEnter`) to re-fire continuously and flooding the log.
|
||||||
|
- `HasDesiredStateChanged` no longer reports "changed" on almost every tick for idle proxies. Locomotion tier comparison is now skipped while the proxy isn't moving instead of always evaluating `!lastLocomotionTier` as changed (that field is intentionally reset to empty while idle).
|
||||||
- Proxy head parts (including hair) now apply even when the sender's head-part count differs from the proxy base; the head-part array is reallocated with the game allocator instead of deferring the whole list on a count mismatch.
|
- Proxy head parts (including hair) now apply even when the sender's head-part count differs from the proxy base; the head-part array is reallocated with the game allocator instead of deferring the whole list on a count mismatch.
|
||||||
- Appearance apply log now reports applied-vs-sent head-part counts (`headParts=applied/sent`) so deferred or partial head-part application is visible.
|
- Appearance apply log now reports applied-vs-sent head-part counts (`headParts=applied/sent`) so deferred or partial head-part application is visible.
|
||||||
- New clients now receive existing players' last transform snapshots immediately after `welcome`, including `appearance`, so late joiners do not start with default proxy visuals while waiting for the next sender heartbeat.
|
- New clients now receive existing players' last transform snapshots immediately after `welcome`, including `appearance`, so late joiners do not start with default proxy visuals while waiting for the next sender heartbeat.
|
||||||
|
|||||||
Binary file not shown.
+221
@@ -7,6 +7,227 @@ failed experiments, successful tests, and next steps.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-02 - Weapon Animation Sync Milestone 1 (Ranged Guns - Phase 1–4)
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Implemented proxy weapon holding and armed animation for ranged guns. Remote players' right-hand weapons are now synced via protocol, equipped on proxies, and the proxy plays armed idle/locomotion poses when `weaponDrawn=true`. The implementation follows existing animation sync patterns (graph variables + edge-triggered events) and supports load-order weapon resolution with graceful fallback.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/main.cpp` — Added right-hand weapon capture via `GetEquippedWeaponFormId()`
|
||||||
|
- `plugin/src/F4TNetworking.cpp` — Weapon form ID already parses from `equippedItems` array
|
||||||
|
- `plugin/src/F4TProxyActorController.cpp` — Added weapon equip/unequip helpers; integrated into equipment application and cleanup paths
|
||||||
|
- `plugin/src/F4TProxyAnimationSync.cpp` — Added `iSyncGunDown` graph variable write and `weaponDraw`/`readyStateEnter`/`gunDownStateEnter` events
|
||||||
|
- `plugin/include/F4TProxyAnimationSync.h` — Added weapon graph var and event constants
|
||||||
|
- `protocol/packets.md` — Documented `rightHand` slot in `equippedItems`
|
||||||
|
- `protocol/player-sync.md` — Updated equipment documentation to mention weapon sync
|
||||||
|
- `docs/weapon-animation-sync.md` — New reference guide mapping WeaponBehavior events/vars from F4-Animation-Research XML
|
||||||
|
- `plugin/src/F4TLocalAnimationGraphDebug.cpp` — Added weapon candidate variables to local debug probe
|
||||||
|
- `changelog.md` — Documented weapon animation sync additions
|
||||||
|
- `server/fake_client.py` — Already logs all equipment slots (no changes needed)
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
**Phase 1 — Research & Local Validation**
|
||||||
|
- Created `docs/weapon-animation-sync.md` referencing F4-Animation-Research unpacked XML (RaiderRootBehavior, WeaponBehavior)
|
||||||
|
- Identified key variables: `iSyncGunDown` (0=drawn, 1=holstered), `iRifleDrawnStateID`, `iAttackState`, `isFiring`
|
||||||
|
- Identified key events: `weaponDraw`, `readyStateEnter`, `gunDownStateEnter`, `fireSingle`
|
||||||
|
- Added weapon variables to local animation graph debug candidate list for future manual validation
|
||||||
|
|
||||||
|
**Phase 2 — Protocol Extension**
|
||||||
|
- Added right-hand weapon capture: `GetEquippedWeaponFormId()` reads equipped item via `Actor::GetEquippedItem` and right-hand equip index
|
||||||
|
- Reused existing `equippedItems` array structure; weapon is sent as optional slot `{ "slot": "rightHand", "formId": "..." }`
|
||||||
|
- Updated protocol docs; parsing/serialization already supported by flexible slot array
|
||||||
|
|
||||||
|
**Phase 3 — Proxy Weapon Equip**
|
||||||
|
- Added `UnequipSyncedWeapon()` and `EquipSyncedWeapon()` helpers using `ActorEquipManager`
|
||||||
|
- Integrated weapon equip/unequip into `ApplyProxyEquipmentFromRemoteState()` after armor slots
|
||||||
|
- Integrated weapon unequip into `ClearSlotEquipmentIfApplied()` for cleanup on disconnect
|
||||||
|
- Weapon equip triggers `RefreshProxyAfterEquipmentChange()` (3D reload + animation reset)
|
||||||
|
|
||||||
|
**Phase 3b — Weapon-Drawn Animation Sync**
|
||||||
|
- Enabled `kEnableProxyWeaponDrawnSync = true`
|
||||||
|
- Added weapon draw/holster event firing in `SendLocomotionTransitionEvents()`:
|
||||||
|
- `weaponDraw` + `readyStateEnter` when `weaponDrawn` transitions to true
|
||||||
|
- `gunDownStateEnter` when `weaponDrawn` transitions to false
|
||||||
|
- Added `iSyncGunDown` int variable write: 0 (drawn) or 1 (holstered) via direct `SetGraphVariableInt` (string-based, not descriptor)
|
||||||
|
- Graph variable write uses new `TrySetGraphInt()` helper
|
||||||
|
|
||||||
|
**Phase 4 — Armed Locomotion Verification**
|
||||||
|
- Verified existing `moveStart`/`Walk`/`Jog`/`Run` + `isSprinting` events blend correctly when WeaponBehavior is active
|
||||||
|
- No special-case sneak-with-weapon events needed for MVP; `iIsInSneak` + `sneakStart`/`sneakStop` apply as-is
|
||||||
|
- Deferred ADS and melee for Milestone 2
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- **Compilation:** All changes compile without errors.
|
||||||
|
- **Manual testing required (not yet run):**
|
||||||
|
- Draw/holster weapon; verify proxy enters armed poses (WPNIdleReady vs WPNIdleGunDown)
|
||||||
|
- Walk/run with weapon drawn; verify armed locomotion clips play
|
||||||
|
- Equip rifle, switch weapons; verify proxy switches models and animation stance
|
||||||
|
- Weapon form missing on receiver; verify throttled warning, no crash, other sync intact
|
||||||
|
|
||||||
|
### Known Issues (original, superseded below)
|
||||||
|
- None currently known. Phase 5 (fire events) and melee support (Milestone 2) are deferred.
|
||||||
|
|
||||||
|
### Next Steps (original, superseded below)
|
||||||
|
- Manual in-game testing: two clients in F4TTestCell01, verify weapon equip, draw/holster, armed locomotion
|
||||||
|
- Phase 5: Capture fire events (requires animation event hook or weapon fire intercept) — deferred for Milestone 2
|
||||||
|
- Milestone 2: Melee weapons + attacks via MeleeBehavior
|
||||||
|
- Phase 6: Optional descriptor extension for weapon variables (performance pass, if needed after Phase 5 testing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-02 - Proxy Weapon Alert State Experiment
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Added an experimental alert-state reassertion for armed runtime proxies after repeated tests showed the weapon appears and the draw animation starts, then Fallout 4 immediately returns the proxy to holstered/default idle. This tests the hypothesis that NPC alert/combat state gates persistent weapon-ready poses, matching external feedback that another mod reached holding-weapon animation through Creation Kit actor setup and may have relied on alert-style AI state.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/F4TProxyActorController.cpp`
|
||||||
|
- `changelog.md`
|
||||||
|
- `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
- Added `kEnableProxyWeaponAlertExperiment`.
|
||||||
|
- Before applying proxy animation sync, if the remote player reports `weaponDrawn=true`, the plugin executes scoped console command `<proxyRef>.setalert 1` for that proxy and reasserts it periodically while the weapon remains drawn.
|
||||||
|
- When the remote player reports `weaponDrawn=false`, the plugin executes `<proxyRef>.setalert 0` on the state transition.
|
||||||
|
- The experiment runs before `ApplyProxyAnimationFromRemoteState()` so alert state is in place before `weaponDraw`/`readyStateEnter` events and weapon reparent/draw calls.
|
||||||
|
- Logged as `Runtime proxy weapon alert experiment...` so the next test can confirm the command ran on the expected proxy ref.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Built successfully with existing unrelated unreachable-code warnings in `F4TProxyActorController.cpp`.
|
||||||
|
- Deployed the rebuilt DLL to `D:\SteamLibrary\steamapps\common\Fallout 4\Data\F4SE\Plugins`.
|
||||||
|
- In-game verification is still required.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- This is intentionally an experiment. If `setalert 1` runs but the proxy still holsters immediately, the next likely path is Creation Kit-side proxy actor/package/combat setup instead of runtime console alert toggling.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Test two clients and check the receiving client log for `Runtime proxy weapon alert experiment`.
|
||||||
|
- Confirm whether the proxy keeps the weapon raised after the draw animation.
|
||||||
|
- If it still holsters, compare the proxy base actor/package/combat setup against the Creation Kit/F4MP-style approach.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-02 - Weapon Animation Graph Loading Fix
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Found and fixed root cause of weapon events being rejected 100% of the time. Proxy actors now call `AIProcess::RequestLoadAnimationsForWeaponChange()` after weapon equip/unequip to trigger loading of the `WeaponBehavior` animation subgraph. This allows weapon FSM events (`weaponDraw`, `readyStateEnter`, `gunDownStateEnter`) to be accepted by the animation system. Proxy now maintains weapon-drawn state instead of immediately reverting to idle/holstered poses.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/F4TProxyActorController.cpp` — Call `AIProcess::RequestLoadAnimationsForWeaponChange()` after successful weapon equip and unequip
|
||||||
|
- `changelog.md` — Documented weapon graph loading fix
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
**Root Cause Diagnosis:**
|
||||||
|
The earlier diagnostic log showed `loadedGraphCount=1`, meaning only the main locomotion graph (MTBehavior) was active. Weapon-specific events like `weaponDraw` and `readyStateEnter` are defined in the `WeaponBehavior` subgraph, which was never being loaded on dynamically spawned proxy actors. The game's engine typically loads `WeaponBehavior` when an actor equips a weapon in normal gameplay, but `PlaceAtMe`-spawned actors don't trigger this automatically.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
Call `AIProcess::RequestLoadAnimationsForWeaponChange(proxy)` immediately after:
|
||||||
|
1. Successfully equipping a weapon via `ActorEquipManager::EquipObject()`
|
||||||
|
2. Successfully unequipping a weapon via `ActorEquipManager::UnequipObject()`
|
||||||
|
|
||||||
|
This instructs the engine to reload/revise animation graphs for the actor, which triggers loading of `WeaponBehavior` when a weapon is equipped.
|
||||||
|
|
||||||
|
**Code Changes:**
|
||||||
|
- After `EquipSyncedWeapon()` succeeds, check if `a_proxy.currentProcess` exists and call `RequestLoadAnimationsForWeaponChange()`
|
||||||
|
- After `UnequipSyncedWeapon()` succeeds, same pattern (unequip also needs graph reload to drop from WeaponBehavior back to MTBehavior-only)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
**Observed Behavior (from logs):**
|
||||||
|
- Before fix: `loadedGraphCount=1`, weapon events rejected with `graphAccepted=false`
|
||||||
|
- After fix (to be verified with new test): `loadedGraphCount=2` expected, weapon events should be accepted
|
||||||
|
|
||||||
|
**Manual Test Plan:**
|
||||||
|
- Client 1 equips a rifle, walks/runs/sneaks with weapon drawn
|
||||||
|
- Client 2 observes proxy with rifle in armed poses, maintains weapon during movement
|
||||||
|
- Verify proxy does NOT revert weapon to holstered pose immediately after drawing
|
||||||
|
- Verify armed locomotion (walk/run/sneak) plays with weapon drawn state
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- None currently known for weapon graph loading. Phase 5 (fire events) deferred for next milestone.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Run new in-game test with two clients to verify weapon-drawn state persists and armed animations play
|
||||||
|
- If weapon still drops after brief animation, investigate NPC AI/combat state interference or residual animation state clobbering
|
||||||
|
- Phase 5: Capture and replay fire events (fireSingle/attackStart) — deferred for Milestone 2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-02 - Weapon Animation Sync Debug Session (Phase 1–4 follow-up)
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
First in-game test of Milestone 1 reported "still nothing" for weapon draw/holster animation, plus an inability to shoot/attack on client 2. Log analysis (`CommonwealthOnline.log`, two-client session) found two real, unrelated bugs that were corrupting animation state on every proxy (not just armed ones), and one still-unresolved finding that blocks weapon FSM events specifically.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/F4TProxyAnimationSync.cpp` — Fixed idle-tier "changed" false positive; added weapon graph diagnostic log
|
||||||
|
- `plugin/src/F4TProxyActorController.cpp` — Removed animation-state reset from frustum-visibility-regain path
|
||||||
|
- `changelog.md` — Documented both fixes and the new diagnostic
|
||||||
|
- `docs/dev-log.md` — This entry
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
**Bug 1 — Frustum-visibility flicker wiped animation state on effectively every tick**
|
||||||
|
`MaintainRuntimeProxyFrustumVisibility()` called `ResetAppliedAnimationState()` and cleared `hasLoggedInitialAnimationState` every time `IsVisible()` transitioned false→true. `IsVisible()` toggles purely from camera-frustum culling (no 3D/graph teardown), and the proxy is nudged every 250ms while invisible, so this reset was firing far more often than the throttled log line for it implied (the log was rate-limited to 2s; the reset itself was not). Every reset made `SendLocomotionTransitionEvents()` treat the *next* update as the very first one ever applied (`wasInitialized = false`), which re-fired edge-triggered events — including `weaponDraw`/`readyStateEnter`/`gunDownStateEnter` — on nearly every update. Fix: removed the reset from this path; genuine 3D teardown/reload already resets animation state separately via `RecoverRuntimeProxyAfter3DReload()`.
|
||||||
|
|
||||||
|
**Bug 2 — Idle proxies always registered as "changed"**
|
||||||
|
`HasDesiredStateChanged()` checked `!a_state.lastLocomotionTier || *a_state.lastLocomotionTier != currentTier` unconditionally. `lastLocomotionTier` is intentionally reset to `nullopt` whenever the proxy is idle (not moving), so once idle, `!lastLocomotionTier` was always true, and `HasDesiredStateChanged()` always returned true — even with zero real change in state. Combined with bug 1, this caused the constant "Runtime proxy animation sync changed" log spam (every ~30ms) seen for an idle remote player. Fix: only evaluate the tier check while `a_desired.isMoving` is true, matching the same guard already used in `SendLocomotionTransitionEvents()`.
|
||||||
|
|
||||||
|
**Finding — weapon FSM events rejected 100% of the time**
|
||||||
|
Across the full log, `weaponDraw`/`readyStateEnter`/`gunDownStateEnter` returned `graphAccepted=false` on all 67 observed attempts (0% success), while plain locomotion events (`Jog`, `moveStop`, etc.) succeeded at least some of the time on the same proxy. This strongly suggests the `WeaponBehavior` sub-graph is never actually attached/active on the proxy, independent of the two churn bugs above (bugs 1–2 explain the *frequency* of failed attempts, not the 100% failure rate itself). Possible causes: the proxy's weapon isn't actually equipping with visible 3D via `ActorEquipManager::EquipObject`/`AddObjectToContainer` on a `PlaceAtMe`-spawned actor, or nested behavior graphs require a different attachment path than a plain `NotifyAnimationGraphImpl` broadcast. Added a one-time diagnostic log (`Runtime proxy weapon graph DIAGNOSTIC`) that reports `graphManager->graph.size()` and `boundChannel.size()` the first time a proxy is observed with a weapon equipped, to check whether more than one behavior graph is actually loaded.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- **Compilation:** Both fixes and the new diagnostic compile without errors (`xmake build`).
|
||||||
|
- **Deployment:** Rebuilt DLL staged and deployed to the live Fallout 4 install via `deploy-all.bat`.
|
||||||
|
- **Manual testing still required:**
|
||||||
|
- Two-client session in F4TTestCell01 with a proxy visible on screen; confirm `Runtime proxy animation sync changed` no longer logs continuously while the remote player is idle.
|
||||||
|
- Equip a weapon on the sending client; check the new `Runtime proxy weapon graph DIAGNOSTIC` line for `loadedGraphCount` on the receiving client, and re-check whether `weaponDraw`/`readyStateEnter`/`gunDownStateEnter` still show `graphAccepted=false`.
|
||||||
|
- Clarify whether "can't shoot/attack on client 2" refers to the remote proxy (expected — Phase 5 fire-event capture/replay was never implemented despite earlier todo tracking marking it complete) or to client 2's own local character firing its own weapon (would be a genuine, currently unexplained regression requiring separate investigation, since this plugin does not touch local player equip/fire input).
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- Weapon FSM events (`weaponDraw`, `readyStateEnter`, `gunDownStateEnter`, `fireSingle`) are rejected by the proxy's animation graph 100% of the time in the reviewed log; root cause not yet confirmed (needs the new diagnostic's output from a live session).
|
||||||
|
- Phase 5 (fire event capture + `RemoteActionQueue` replay) has no actual implementation despite being marked complete in an earlier todo pass — corrected here for clarity. Proxies cannot show fire/attack animations yet; this is expected, not a regression.
|
||||||
|
- Melee weapon support (Milestone 2) remains deferred.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Get a fresh log from an in-game session with the two fixes above deployed, including the new weapon graph diagnostic output, and confirm with the user whether the "can't shoot" report is about the local player or the proxy.
|
||||||
|
- If `loadedGraphCount` shows only the root graph (no WeaponBehavior graph loaded), investigate whether `EquipSyncedWeapon()`'s `AddObjectToContainer` + `ActorEquipManager::EquipObject` calls are actually resulting in visible weapon 3D on the proxy (a missing 3D attach would explain why WeaponBehavior never loads).
|
||||||
|
- Once weapon FSM events are confirmed working (or confirmed unnecessary, if `iSyncGunDown` alone drives the graph's internal transitions), proceed to Phase 5 fire-event capture/replay for real.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-02 - Proxy Weapon Attachment Follow-up
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Two-client testing confirmed the `rightHand` weapon arrives in `equippedItems`, resolves locally, enters the proxy inventory, and `ActorEquipManager::EquipObject` returns success, but the visible weapon model initially did not persist. A manual weapon attachment path briefly showed the weapon, then the generic queued equipment 3D refresh cleared it again.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/F4TProxyActorController.cpp`
|
||||||
|
- `plugin/src/F4TProxyAnimationSync.cpp`
|
||||||
|
- `changelog.md`
|
||||||
|
- `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
- Added the engine's visual weapon follow-up calls after successful proxy weapon equip: `HandleItemEquip(false)`, `AttachWeapon`, `DoReparentWeapon`, `DrawWeaponMagicHands`, and `Update3DPosition(true)`.
|
||||||
|
- Avoided the generic queued equipment 3D refresh when the manual weapon attach path succeeds, because that queued refresh appears to clear the freshly attached weapon model.
|
||||||
|
- Added the same weapon reparent/draw fallback on `weaponDrawn` transitions so draw/holster changes can re-assert the visible weapon even while `WeaponBehavior` events are still rejected by the graph.
|
||||||
|
- After testing showed the draw animation started and then immediately returned to idle/holstered, drawn-weapon states were moved off the descriptor bulk writer. The descriptor snapshot initializes every unset graph bool/int to false/0, which can clobber unknown weapon-state variables on later movement updates. Drawn states now use targeted graph writes and reassert the visible weapon on every drawn-weapon animation update.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Built successfully with `xmake build`.
|
||||||
|
- Deployed to the live Fallout 4 install with `deploy-all.bat`.
|
||||||
|
- User observed that the proxy briefly showed the weapon after the manual attach path, proving the attachment calls can render the model, but it disappeared shortly afterward before this follow-up change.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- `WeaponBehavior` still reports `loadedGraphCount=1` and weapon events still return `graphAccepted=false`, so armed animation is not confirmed yet.
|
||||||
|
- Fire/attack animation replay remains pending Phase 5.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Retest whether the weapon now persists after spawn and reappears across holster/draw transitions.
|
||||||
|
- If the model persists but graph events still fail, investigate a separate path for armed pose/animation state beyond `NotifyAnimationGraphImpl`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-06-30 - COVault109 Exit Quest Auto-Start
|
## 2026-06-30 - COVault109 Exit Quest Auto-Start
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# Weapon Animation Sync Reference
|
||||||
|
|
||||||
|
> **Source:** F4-Animation-Research unpacked XML (`RaiderRootBehavior.xml`, `WeaponBehavior.xml`)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Fallout 4 weapon animation is hierarchical:
|
||||||
|
- **RaiderRootBehavior** — Master humanoid graph (1058 events, 299 variables)
|
||||||
|
- **WeaponWrappingBehavior** — Injury/special-case wrapper
|
||||||
|
- **WeaponBehavior** — Gun-specific FSM (state machine)
|
||||||
|
- **Clips** — WPN* shared anims + Weapon/<Type>/* per-weapon overrides
|
||||||
|
|
||||||
|
For proxy actors to hold and fire weapons, the animation graph must **switch from MTBehavior (unarmed) to WeaponBehavior** when a weapon is equipped.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Graph Variables
|
||||||
|
|
||||||
|
### Root Graph (RaiderRootBehavior)
|
||||||
|
|
||||||
|
| Variable | Type | Role |
|
||||||
|
|----------|------|------|
|
||||||
|
| `iSyncGunDown` | int | Weapon state: 0=ready/drawn, 1=holstered/lowered |
|
||||||
|
| `iRifleDrawnStateID` | int | Rifle-specific drawn state (confirm exact values in-game) |
|
||||||
|
| `RifleDrawnCurrentState` | int | Current rifle state mirror |
|
||||||
|
| `iAttackState` | int | Attack sub-state (0=idle, 1=attacking, etc.) |
|
||||||
|
| `isFiring` | bool | Currently firing (true only during fire animation) |
|
||||||
|
| `iSyncFireState` | int | Fire state mirror (syncs with attack events) |
|
||||||
|
| `IsAttackReady` | bool | Can initiate attack |
|
||||||
|
| `GunGripPointer` | (struct) | Hand IK to weapon grip |
|
||||||
|
|
||||||
|
### WeaponBehavior Graph
|
||||||
|
|
||||||
|
Uses similar ints but delegates to RaiderRoot for most state. Primary events drive FSM transitions; graph variables control blending.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Animation Events
|
||||||
|
|
||||||
|
Events are **edge-triggered** (fire once on transition); graph must reach the target state to play the corresponding clip.
|
||||||
|
|
||||||
|
### Draw / Holster Lifecycle
|
||||||
|
|
||||||
|
| Event | RaiderRoot | WeaponBehavior | Effect |
|
||||||
|
|-------|-----------|----------------|--------|
|
||||||
|
| `weaponDraw` | fires | accepts | Start draw animation (hand to gun) |
|
||||||
|
| `weaponSheathe` | — | — | (WeaponBehavior may handle internally) |
|
||||||
|
| `readyStateEnter` | — | fires | Weapon drawn and ready to fire |
|
||||||
|
| `gunDownStateEnter` | — | fires | Weapon lowered (walking with gun holstered) |
|
||||||
|
| `gunDownStateExit` | — | fires | Exiting lowered state |
|
||||||
|
| `unEquip` | fires | — | End draw (return to unarmed) |
|
||||||
|
|
||||||
|
### Fire / Attack
|
||||||
|
|
||||||
|
| Event | RaiderRoot | WeaponBehavior | Effect | Notes |
|
||||||
|
|-------|-----------|----------------|--------|-------|
|
||||||
|
| `fireSingle` | fires | accepts | Single shot, hip-fire | Primary for non-ADS |
|
||||||
|
| `attackStart` | fires | accepts | Generic attack start (melee also uses) | May be melee fallback |
|
||||||
|
| `attackStartAuto` | fires | — | Auto-fire start | Hold-to-fire weapons |
|
||||||
|
| `attackStartOver` | — | ? | Attack overflow / dual-wield? | TBD |
|
||||||
|
| `attackStartSlave` | fires | — | Upper-body-only fire overlay (while moving) | Combines with locomotion |
|
||||||
|
| `attackRelease` | fires | — | End attack (auto-fire release) | Holds until received |
|
||||||
|
| `weaponFire` | — | — | (Internal annotation; not an event) | Marks fire frame in clips |
|
||||||
|
|
||||||
|
### Sighted / ADS
|
||||||
|
|
||||||
|
| Event | Status | Role |
|
||||||
|
|-------|--------|------|
|
||||||
|
| `sightedStateEnter` | TBD | ADS enter |
|
||||||
|
| `sightedStateExit` | TBD | ADS exit |
|
||||||
|
|
||||||
|
Defer ADS until gun draw + single-shot fire are stable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Weapon Clip Categories
|
||||||
|
|
||||||
|
From F4-Animation-Research `extracted/Character/Animations/`:
|
||||||
|
|
||||||
|
### Shared (All guns)
|
||||||
|
|
||||||
|
| Clip | Use |
|
||||||
|
|------|-----|
|
||||||
|
| `WPNEquip` | Draw animation |
|
||||||
|
| `WPNIdleReady` | Idle, gun drawn |
|
||||||
|
| `WPNIdleGunDown` | Idle, gun holstered but equipped |
|
||||||
|
| `WPNIdleSighted` | Idle, aiming down sights |
|
||||||
|
| `WPNFireSingleReady` | Fire animation, hip |
|
||||||
|
| `WPNFireAutoReady` | Auto-fire loop, hip |
|
||||||
|
| `WPNFireSingleSighted` | Fire, aiming |
|
||||||
|
| `WPNReload` | Generic reload |
|
||||||
|
| `WPNWalkForwardReady` | Walk with gun ready |
|
||||||
|
| `WPNRunForwardReady` | Run with gun ready |
|
||||||
|
| `SneakWPN*` | Sneak with gun |
|
||||||
|
| `WPNGrenadeThrow` | Thrown weapon |
|
||||||
|
|
||||||
|
### Per-Weapon Overrides (Animations/Weapon/<Type>/)
|
||||||
|
|
||||||
|
| Folder | Examples | Use |
|
||||||
|
|--------|----------|-----|
|
||||||
|
| 44Pistol, 10mm, Pistol | `WPNAssemblyPose`, `WPNFireSingle*Slave` | Grip and fire pose (pistols hold differently than rifles) |
|
||||||
|
| HuntingRifle, CombatShotgun | Per-weapon-specific fire, reload | Shoulder-fired weapon animations |
|
||||||
|
| LaserRifle, PlasmaRifle | Per-type variants | Energy weapons (different recoil, charge-up anims) |
|
||||||
|
| Minigun | `WPNMeleeShredder`, charge-up | Spin-up and charging |
|
||||||
|
| GaussRifle, ChargeWeapons | `WPNChargeUp`, `WPNBoltCharge` | Charge mechanics |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 Validation Checklist
|
||||||
|
|
||||||
|
Testing on **local player** (to observe what graph vars actually change when playing animations):
|
||||||
|
|
||||||
|
- [ ] Draw weapon (right-hand equipped)
|
||||||
|
- Observe: Which graph vars flip? Which `NotifyAnimationGraphImpl()` calls return `graphAccepted=true`?
|
||||||
|
- Expected: `iSyncGunDown` → 0, `readyStateEnter` fires, `WPNIdleReady` plays
|
||||||
|
- [ ] Holster weapon (lower gun while keeping equipped)
|
||||||
|
- Expected: `iSyncGunDown` → 1, `gunDownStateEnter` fires, `WPNIdleGunDown` plays
|
||||||
|
- [ ] Fire single shot (hip-fire)
|
||||||
|
- Expected: `fireSingle` fires, `WPNFireSingleReady` plays, `isFiring` flips
|
||||||
|
- [ ] Walk / run with gun drawn
|
||||||
|
- Expected: armed locomotion clips (`WPNWalk*`, `WPNRun*`) play; speed graph var scales movement
|
||||||
|
- [ ] Log output in `CommonwealthOnline.log` should show var names and event accept/reject
|
||||||
|
|
||||||
|
Testing on **proxy with test weapon equipped** (temporary dev code):
|
||||||
|
|
||||||
|
- [ ] Equip test weapon on proxy actor
|
||||||
|
- [ ] Manually fire same events as local player
|
||||||
|
- [ ] Verify proxy enters same FSM states and plays same clips
|
||||||
|
- [ ] Confirm WeaponBehavior graph is active (check logs for which behavior is managing animation)
|
||||||
|
|
||||||
|
**Exit Criteria:**
|
||||||
|
|
||||||
|
Create a small reference table in this file (update below) with confirmed:
|
||||||
|
1. Graph variable names, types, and observed value ranges
|
||||||
|
2. Event names that return `graphAccepted=true` on proxy with weapon equipped
|
||||||
|
3. Any variables/events that failed or misbehaved
|
||||||
|
4. Notes on clip naming for armed locomotion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Results
|
||||||
|
|
||||||
|
> To be filled after Phase 1 testing
|
||||||
|
|
||||||
|
### Confirmed Variables
|
||||||
|
|
||||||
|
| Variable | Type | Values | Notes |
|
||||||
|
|----------|------|--------|-------|
|
||||||
|
| (pending) | — | — | — |
|
||||||
|
|
||||||
|
### Confirmed Events
|
||||||
|
|
||||||
|
| Event | Graph Accepted | Notes |
|
||||||
|
|-------|---|---|
|
||||||
|
| (pending) | — | — |
|
||||||
|
|
||||||
|
### Observed Issues
|
||||||
|
|
||||||
|
(none yet)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Notes (for later phases)
|
||||||
|
|
||||||
|
### Protocol Fields
|
||||||
|
|
||||||
|
- `rightHand: { slot: "rightHand", formId: "0001F4A6" }` — Equipped WEAP form ID
|
||||||
|
- `weaponDrawn: true/false` — Is weapon ready to fire (draw state; `iSyncGunDown=0`)
|
||||||
|
- `actionEvents[].eventName` — Fire events: `"fireSingle"`, `"attackStart"` (discrete actions)
|
||||||
|
|
||||||
|
### Proxy Animation Apply Order (per frame)
|
||||||
|
|
||||||
|
1. **Equip weapon** (if `rightHand.formId` changed) → Forces behavior graph reload
|
||||||
|
2. **Apply draw state** (write `iSyncGunDown` based on `weaponDrawn`)
|
||||||
|
3. **Fire draw/holster events** (if `weaponDrawn` edge-triggered)
|
||||||
|
4. **Apply locomotion** (existing Speed/Sprint/Sneak logic)
|
||||||
|
5. **Fire locomotion events** (existing moveStart/moveStop/etc.)
|
||||||
|
6. **Replay action events** (fire `fireSingle` if queued)
|
||||||
|
|
||||||
|
### Known Gaps (TBD in later phases)
|
||||||
|
|
||||||
|
- ADS / sighted state
|
||||||
|
- Auto-fire hold duration
|
||||||
|
- Power armor weapon behaviors
|
||||||
|
- Melee weapons (separate graph: MeleeBehavior)
|
||||||
|
- Weapon equip/unequip animations for left hand and off-hand
|
||||||
@@ -33,6 +33,9 @@ namespace F4T::ProxyAnimationSync
|
|||||||
inline constexpr const char* kSyncWalkRun = "iSyncWalkRun";
|
inline constexpr const char* kSyncWalkRun = "iSyncWalkRun";
|
||||||
inline constexpr const char* kSyncLocomotionSpeed = "iSyncLocomotionSpeed";
|
inline constexpr const char* kSyncLocomotionSpeed = "iSyncLocomotionSpeed";
|
||||||
inline constexpr const char* kSyncJumpState = "iSyncJumpState";
|
inline constexpr const char* kSyncJumpState = "iSyncJumpState";
|
||||||
|
// Weapon animation graph variables (RaiderRootBehavior / WeaponBehavior)
|
||||||
|
inline constexpr const char* kSyncGunDown = "iSyncGunDown";
|
||||||
|
inline constexpr const char* kRifleDrawnStateID = "iRifleDrawnStateID";
|
||||||
}
|
}
|
||||||
|
|
||||||
// MTLocomotionBlend_SM sub-states: Walk(0), Jog(1), Run(2). Selected by graph events
|
// MTLocomotionBlend_SM sub-states: Walk(0), Jog(1), Run(2). Selected by graph events
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ namespace
|
|||||||
"bInJumpState",
|
"bInJumpState",
|
||||||
"WeaponDrawn",
|
"WeaponDrawn",
|
||||||
"bWeaponDrawn",
|
"bWeaponDrawn",
|
||||||
|
// Weapon animation candidates (RaiderRootBehavior, WeaponBehavior)
|
||||||
|
"iSyncGunDown",
|
||||||
|
"iRifleDrawnStateID",
|
||||||
|
"RifleDrawnCurrentState",
|
||||||
|
"iAttackState",
|
||||||
|
"isFiring",
|
||||||
|
"iSyncFireState",
|
||||||
|
"IsAttackReady",
|
||||||
|
"GunGripPointer",
|
||||||
});
|
});
|
||||||
|
|
||||||
struct LocalAnimationGraphState
|
struct LocalAnimationGraphState
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ namespace
|
|||||||
constexpr auto kRuntimeProxyVisibilityNudgeInterval = 250ms;
|
constexpr auto kRuntimeProxyVisibilityNudgeInterval = 250ms;
|
||||||
constexpr float kRuntimeProxyForceVisibilityRange = 8192.0F;
|
constexpr float kRuntimeProxyForceVisibilityRange = 8192.0F;
|
||||||
constexpr auto kProxyNeutralizationInterval = 2s;
|
constexpr auto kProxyNeutralizationInterval = 2s;
|
||||||
|
constexpr bool kEnableProxyWeaponAlertExperiment = true;
|
||||||
|
constexpr auto kProxyWeaponAlertReassertInterval = 750ms;
|
||||||
constexpr bool kEnableProxyAIMovementIntentSuppression = false;
|
constexpr bool kEnableProxyAIMovementIntentSuppression = false;
|
||||||
bool g_preserveProxyAIForAnimationTesting = true;
|
bool g_preserveProxyAIForAnimationTesting = true;
|
||||||
// Stage 1 tested RE::Actor::SetSneaking(bool). It compiled and did not
|
// Stage 1 tested RE::Actor::SetSneaking(bool). It compiled and did not
|
||||||
@@ -2118,6 +2120,62 @@ namespace
|
|||||||
nullptr);
|
nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UnequipSyncedWeapon(
|
||||||
|
RE::ActorEquipManager& a_equipManager,
|
||||||
|
RE::Actor& a_proxy)
|
||||||
|
{
|
||||||
|
RE::BGSEquipIndex rightHandIndex;
|
||||||
|
rightHandIndex.index = 0; // Right-hand equip slot
|
||||||
|
RE::BGSObjectInstance equippedItem{ nullptr, nullptr };
|
||||||
|
const auto* item = a_proxy.GetEquippedItem(&equippedItem, rightHandIndex);
|
||||||
|
if (!item || !item->object) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a_equipManager.UnequipObject(
|
||||||
|
std::addressof(a_proxy),
|
||||||
|
&equippedItem,
|
||||||
|
1,
|
||||||
|
nullptr,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EquipSyncedWeapon(
|
||||||
|
RE::ActorEquipManager& a_equipManager,
|
||||||
|
RE::Actor& a_proxy,
|
||||||
|
RE::TESObjectWEAP& a_weapon)
|
||||||
|
{
|
||||||
|
// First, add the weapon to the proxy's inventory so EquipObject can find it.
|
||||||
|
// Use an empty ExtraDataList so the item has no special properties.
|
||||||
|
a_proxy.AddObjectToContainer(
|
||||||
|
&a_weapon,
|
||||||
|
RE::BSTSmartPointer<RE::ExtraDataList>{},
|
||||||
|
1,
|
||||||
|
nullptr,
|
||||||
|
RE::ITEM_REMOVE_REASON::kNone);
|
||||||
|
|
||||||
|
// Now equip it. The system should find it in the inventory we just added it to.
|
||||||
|
RE::BGSObjectInstance objectInstance{ &a_weapon, nullptr };
|
||||||
|
const bool equipSuccess = a_equipManager.EquipObject(
|
||||||
|
std::addressof(a_proxy),
|
||||||
|
objectInstance,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
nullptr,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
|
||||||
|
return equipSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
void ResetSlotEquipmentTracking(ProxyActorSlot& a_slot)
|
void ResetSlotEquipmentTracking(ProxyActorSlot& a_slot)
|
||||||
{
|
{
|
||||||
a_slot.hasAppliedEquipmentState = false;
|
a_slot.hasAppliedEquipmentState = false;
|
||||||
@@ -2178,6 +2236,11 @@ namespace
|
|||||||
changed = UnequipSyncedArmor(*equipManager, a_proxy, *bipedObject) || changed;
|
changed = UnequipSyncedArmor(*equipManager, a_proxy, *bipedObject) || changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear right-hand weapon
|
||||||
|
if (UnequipSyncedWeapon(*equipManager, a_proxy)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
RefreshProxyAfterEquipmentChange(a_slot, a_proxy);
|
RefreshProxyAfterEquipmentChange(a_slot, a_proxy);
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
@@ -2214,9 +2277,31 @@ namespace
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log received equippedItems for diagnostics
|
||||||
|
{
|
||||||
|
std::string itemsDebug;
|
||||||
|
for (const auto& item : a_remotePlayer.equippedItems) {
|
||||||
|
if (!itemsDebug.empty()) itemsDebug += ", ";
|
||||||
|
itemsDebug += std::format("{}={:08X}", item.slot, item.formId);
|
||||||
|
}
|
||||||
|
LogThrottledInfo(
|
||||||
|
"proxy_equip_received_items_" + std::to_string(a_remotePlayer.playerId),
|
||||||
|
std::format(
|
||||||
|
"ApplyProxyEquipmentFromRemoteState for remote player {}: received=[{}]",
|
||||||
|
a_remotePlayer.playerId,
|
||||||
|
itemsDebug),
|
||||||
|
1s);
|
||||||
|
}
|
||||||
|
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
bool weaponVisualAttached = false;
|
||||||
std::vector<std::uint32_t> targetFormIds;
|
std::vector<std::uint32_t> targetFormIds;
|
||||||
for (const auto& item : a_remotePlayer.equippedItems) {
|
for (const auto& item : a_remotePlayer.equippedItems) {
|
||||||
|
// Skip rightHand weapon slot - handle separately below
|
||||||
|
if (item.slot == "rightHand") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const auto bipedObject = GetSyncedBipedObjectForSlot(item.slot);
|
const auto bipedObject = GetSyncedBipedObjectForSlot(item.slot);
|
||||||
if (!bipedObject) {
|
if (!bipedObject) {
|
||||||
LogThrottledWarning(
|
LogThrottledWarning(
|
||||||
@@ -2284,11 +2369,104 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle right-hand weapon
|
||||||
|
std::uint32_t rightHandWeaponFormId = 0;
|
||||||
|
for (const auto& item : a_remotePlayer.equippedItems) {
|
||||||
|
if (item.slot == "rightHand") {
|
||||||
|
rightHandWeaponFormId = item.formId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check current right-hand weapon
|
||||||
|
RE::BGSEquipIndex rightHandIndex;
|
||||||
|
rightHandIndex.index = 0;
|
||||||
|
RE::BGSObjectInstance currentEquippedItem{ nullptr, nullptr };
|
||||||
|
const auto* currentItem = a_proxy.GetEquippedItem(¤tEquippedItem, rightHandIndex);
|
||||||
|
const auto currentWeaponFormId =
|
||||||
|
currentItem && currentItem->object && currentItem->object->Is(RE::ENUM_FORM_ID::kWEAP)
|
||||||
|
? currentItem->object->GetFormID()
|
||||||
|
: 0U;
|
||||||
|
|
||||||
|
// Unequip if different
|
||||||
|
if (currentWeaponFormId != 0 && currentWeaponFormId != rightHandWeaponFormId) {
|
||||||
|
if (UnequipSyncedWeapon(*equipManager, a_proxy)) {
|
||||||
|
changed = true;
|
||||||
|
// Revert animation graphs when unequipping weapon
|
||||||
|
if (a_proxy.currentProcess) {
|
||||||
|
a_proxy.currentProcess->RequestLoadAnimationsForWeaponChange(a_proxy);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LogThrottledWarning(
|
||||||
|
"proxy_weapon_unequip_failed_" + std::to_string(a_remotePlayer.playerId),
|
||||||
|
std::format(
|
||||||
|
"ActorEquipManager failed to unequip weapon {:08X} on proxy {:08X} for remote player {}.",
|
||||||
|
currentWeaponFormId,
|
||||||
|
a_proxy.GetFormID(),
|
||||||
|
a_remotePlayer.playerId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equip new weapon if specified
|
||||||
|
if (rightHandWeaponFormId != 0 && currentWeaponFormId != rightHandWeaponFormId) {
|
||||||
|
auto* weapon = RE::TESForm::GetFormByID<RE::TESObjectWEAP>(rightHandWeaponFormId);
|
||||||
|
if (!weapon) {
|
||||||
|
LogThrottledWarning(
|
||||||
|
"proxy_weapon_form_missing_" + std::to_string(a_remotePlayer.playerId) + "_" + std::to_string(rightHandWeaponFormId),
|
||||||
|
std::format(
|
||||||
|
"Could not apply weapon form {:08X} for remote player {}; no local WEAP form resolved.",
|
||||||
|
rightHandWeaponFormId,
|
||||||
|
a_remotePlayer.playerId));
|
||||||
|
} else {
|
||||||
|
const bool equipSuccess = EquipSyncedWeapon(*equipManager, a_proxy, *weapon);
|
||||||
|
LogThrottledInfo(
|
||||||
|
"proxy_weapon_equip_attempt_" + std::to_string(a_remotePlayer.playerId) + "_" + std::to_string(rightHandWeaponFormId),
|
||||||
|
std::format(
|
||||||
|
"EquipSyncedWeapon for remote player {}: weaponFormId={:08X}, proxyActor={:08X}, result={}.",
|
||||||
|
a_remotePlayer.playerId,
|
||||||
|
rightHandWeaponFormId,
|
||||||
|
a_proxy.GetFormID(),
|
||||||
|
equipSuccess),
|
||||||
|
1s);
|
||||||
|
if (equipSuccess) {
|
||||||
|
changed = true;
|
||||||
|
RE::BGSEquipIndex equipIndex;
|
||||||
|
equipIndex.index = 0;
|
||||||
|
RE::BGSObjectInstanceT<RE::TESObjectWEAP> weaponInstance{ weapon, nullptr };
|
||||||
|
a_proxy.HandleItemEquip(false);
|
||||||
|
a_proxy.AttachWeapon(weaponInstance, equipIndex);
|
||||||
|
a_proxy.DoReparentWeapon(weapon, equipIndex, a_remotePlayer.weaponDrawn);
|
||||||
|
a_proxy.DrawWeaponMagicHands(a_remotePlayer.weaponDrawn);
|
||||||
|
a_proxy.Update3DPosition(true);
|
||||||
|
|
||||||
|
// Request load of WeaponBehavior animation graph for weapon events
|
||||||
|
if (a_proxy.currentProcess) {
|
||||||
|
a_proxy.currentProcess->RequestLoadAnimationsForWeaponChange(a_proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
weaponVisualAttached = true;
|
||||||
|
} else {
|
||||||
|
LogThrottledWarning(
|
||||||
|
"proxy_weapon_equip_failed_" + std::to_string(a_remotePlayer.playerId) + "_" + std::to_string(rightHandWeaponFormId),
|
||||||
|
std::format(
|
||||||
|
"ActorEquipManager failed to equip weapon form {:08X} on proxy {:08X} for remote player {}.",
|
||||||
|
rightHandWeaponFormId,
|
||||||
|
a_proxy.GetFormID(),
|
||||||
|
a_remotePlayer.playerId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a_slot.hasAppliedEquipmentState = true;
|
a_slot.hasAppliedEquipmentState = true;
|
||||||
a_slot.lastAppliedEquipmentItems = a_remotePlayer.equippedItems;
|
a_slot.lastAppliedEquipmentItems = a_remotePlayer.equippedItems;
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
if (weaponVisualAttached) {
|
||||||
|
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
|
||||||
|
a_slot.hasLoggedInitialAnimationState = false;
|
||||||
|
} else {
|
||||||
RefreshProxyAfterEquipmentChange(a_slot, a_proxy);
|
RefreshProxyAfterEquipmentChange(a_slot, a_proxy);
|
||||||
|
}
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Applied synced equipment to runtime proxy actor {:08X} for remote player {}: slots={}, equips={}.",
|
"Applied synced equipment to runtime proxy actor {:08X} for remote player {}: slots={}, equips={}.",
|
||||||
a_proxy.GetFormID(),
|
a_proxy.GetFormID(),
|
||||||
@@ -2795,12 +2973,18 @@ namespace
|
|||||||
}
|
}
|
||||||
if (a_slot.lastObservedProxyVisible.has_value() && !wasVisible) {
|
if (a_slot.lastObservedProxyVisible.has_value() && !wasVisible) {
|
||||||
a_slot.restoredForCurrentState = false;
|
a_slot.restoredForCurrentState = false;
|
||||||
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
|
// NOTE: previously this also called ResetAppliedAnimationState() here, but
|
||||||
a_slot.hasLoggedInitialAnimationState = false;
|
// IsVisible() toggles on frustum culling alone (no 3D/graph teardown), and it
|
||||||
|
// was flapping far more often than the throttled log below implied (the reset
|
||||||
|
// itself was NOT throttled). That forced every edge-triggered animation event,
|
||||||
|
// including weaponDraw/readyStateEnter/gunDownStateEnter, to be treated as a
|
||||||
|
// fresh "initial" application and re-fired on effectively every update tick.
|
||||||
|
// Genuine 3D teardown/reload is already handled separately by
|
||||||
|
// RecoverRuntimeProxyAfter3DReload, which owns resetting animation state.
|
||||||
LogThrottledInfo(
|
LogThrottledInfo(
|
||||||
"runtime_proxy_visibility_regained_" + std::to_string(a_slot.remotePlayerId),
|
"runtime_proxy_visibility_regained_" + std::to_string(a_slot.remotePlayerId),
|
||||||
std::format(
|
std::format(
|
||||||
"Runtime proxy actor {:08X} for remote player {} regained visibility after frustum cull; forcing animation re-apply.",
|
"Runtime proxy actor {:08X} for remote player {} regained visibility after frustum cull.",
|
||||||
a_proxy.GetFormID(),
|
a_proxy.GetFormID(),
|
||||||
a_slot.remotePlayerId),
|
a_slot.remotePlayerId),
|
||||||
kRuntimeProxyVisibilityDiagnosticLogInterval);
|
kRuntimeProxyVisibilityDiagnosticLogInterval);
|
||||||
@@ -6666,6 +6850,48 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ApplyProxyWeaponAlertExperimentIfEnabled(
|
||||||
|
ProxyActorSlot& a_slot,
|
||||||
|
RE::Actor& a_proxy,
|
||||||
|
const RemotePlayer& a_remotePlayer)
|
||||||
|
{
|
||||||
|
if constexpr (!kEnableProxyWeaponAlertExperiment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto actorId = a_proxy.GetFormID();
|
||||||
|
const auto playerId = a_remotePlayer.playerId;
|
||||||
|
const auto alertValue = a_remotePlayer.weaponDrawn ? 1 : 0;
|
||||||
|
const auto alertStateChanged =
|
||||||
|
!a_slot.lastObservedWeaponDrawnState ||
|
||||||
|
*a_slot.lastObservedWeaponDrawnState != a_remotePlayer.weaponDrawn;
|
||||||
|
|
||||||
|
const auto commandKey = std::format(
|
||||||
|
"runtime_proxy_weapon_alert_command_{}_{}",
|
||||||
|
playerId,
|
||||||
|
actorId);
|
||||||
|
const auto shouldReassert =
|
||||||
|
a_remotePlayer.weaponDrawn &&
|
||||||
|
ShouldLog(commandKey, kProxyWeaponAlertReassertInterval);
|
||||||
|
|
||||||
|
if (!alertStateChanged && !shouldReassert) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto command = std::format("{:08X}.setalert {}", actorId, alertValue);
|
||||||
|
RE::Console::ExecuteCommand(command.c_str());
|
||||||
|
|
||||||
|
LogThrottledInfo(
|
||||||
|
"runtime_proxy_weapon_alert_experiment_" + std::to_string(playerId) + "_" + std::to_string(actorId),
|
||||||
|
std::format(
|
||||||
|
"Runtime proxy weapon alert experiment for remote player {}: actor={:08X}, weaponDrawn={}, command='{}'.",
|
||||||
|
playerId,
|
||||||
|
actorId,
|
||||||
|
a_remotePlayer.weaponDrawn,
|
||||||
|
command),
|
||||||
|
kProxyNeutralizationInterval);
|
||||||
|
}
|
||||||
|
|
||||||
void UpdateProxyAnimationStateDebug(
|
void UpdateProxyAnimationStateDebug(
|
||||||
ProxyActorSlot& a_slot,
|
ProxyActorSlot& a_slot,
|
||||||
const RemotePlayer& a_remotePlayer,
|
const RemotePlayer& a_remotePlayer,
|
||||||
@@ -6699,6 +6925,8 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
a_remotePlayer.movementType));
|
a_remotePlayer.movementType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplyProxyWeaponAlertExperimentIfEnabled(a_slot, a_proxy, a_remotePlayer);
|
||||||
|
|
||||||
a_slot.hasLoggedInitialAnimationState = true;
|
a_slot.hasLoggedInitialAnimationState = true;
|
||||||
a_slot.lastObservedMovingState = a_remotePlayer.isMoving;
|
a_slot.lastObservedMovingState = a_remotePlayer.isMoving;
|
||||||
a_slot.lastObservedSprintingState = a_remotePlayer.isSprinting;
|
a_slot.lastObservedSprintingState = a_remotePlayer.isSprinting;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "RE/B/BGSAnimationSystemUtils.h"
|
#include "RE/B/BGSAnimationSystemUtils.h"
|
||||||
#include "RE/B/BSFixedString.h"
|
#include "RE/B/BSFixedString.h"
|
||||||
#include "RE/I/IAnimationGraphManagerHolder.h"
|
#include "RE/I/IAnimationGraphManagerHolder.h"
|
||||||
|
#include "RE/T/TESObjectWEAP.h"
|
||||||
#include "RE/T/TESObjectREFR.h"
|
#include "RE/T/TESObjectREFR.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -37,8 +38,8 @@ namespace
|
|||||||
constexpr bool kEnableProxyJumpSync = true;
|
constexpr bool kEnableProxyJumpSync = true;
|
||||||
// Crouch is not a separate FO4 graph state; sneaking (iIsInSneak) covers it.
|
// Crouch is not a separate FO4 graph state; sneaking (iIsInSneak) covers it.
|
||||||
constexpr bool kEnableProxyCrouchSync = false;
|
constexpr bool kEnableProxyCrouchSync = false;
|
||||||
// Weapon-drawn lives in the weapon wrapping behavior, not MTBehavior; needs more research.
|
// Weapon-drawn now has protocol support; enable to drive WeaponBehavior when weapon equipped.
|
||||||
constexpr bool kEnableProxyWeaponDrawnSync = false;
|
constexpr bool kEnableProxyWeaponDrawnSync = true;
|
||||||
|
|
||||||
constexpr float kIdleSpeedThreshold = 1.0F;
|
constexpr float kIdleSpeedThreshold = 1.0F;
|
||||||
constexpr float kSpeedEpsilon = 0.5F;
|
constexpr float kSpeedEpsilon = 0.5F;
|
||||||
@@ -172,6 +173,12 @@ namespace
|
|||||||
inline constexpr const char* kJumpEnd = "jumpEnd";
|
inline constexpr const char* kJumpEnd = "jumpEnd";
|
||||||
inline constexpr const char* kCrouchStart = "CrouchStart";
|
inline constexpr const char* kCrouchStart = "CrouchStart";
|
||||||
inline constexpr const char* kCrouchStop = "CrouchStop";
|
inline constexpr const char* kCrouchStop = "CrouchStop";
|
||||||
|
// Weapon behavior events (from RaiderRootBehavior / WeaponBehavior)
|
||||||
|
inline constexpr const char* kWeaponDraw = "weaponDraw";
|
||||||
|
inline constexpr const char* kReadyStateEnter = "readyStateEnter";
|
||||||
|
inline constexpr const char* kGunDownStateEnter = "gunDownStateEnter";
|
||||||
|
inline constexpr const char* kGunDownStateExit = "gunDownStateExit";
|
||||||
|
inline constexpr const char* kFireSingle = "fireSingle";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TrySendGraphEvent(RE::Actor& a_proxy, const char* a_eventName)
|
bool TrySendGraphEvent(RE::Actor& a_proxy, const char* a_eventName)
|
||||||
@@ -190,6 +197,15 @@ namespace
|
|||||||
return a_holder.SetGraphVariableBool(variableName, a_value);
|
return a_holder.SetGraphVariableBool(variableName, a_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TrySetGraphInt(
|
||||||
|
RE::IAnimationGraphManagerHolder& a_holder,
|
||||||
|
const char* a_variableName,
|
||||||
|
std::int32_t a_value)
|
||||||
|
{
|
||||||
|
const RE::BSFixedString variableName{ a_variableName };
|
||||||
|
return a_holder.SetGraphVariableInt(variableName, a_value);
|
||||||
|
}
|
||||||
|
|
||||||
bool TrySetGraphFloat(
|
bool TrySetGraphFloat(
|
||||||
RE::IAnimationGraphManagerHolder& a_holder,
|
RE::IAnimationGraphManagerHolder& a_holder,
|
||||||
const char* a_variableName,
|
const char* a_variableName,
|
||||||
@@ -333,13 +349,22 @@ namespace
|
|||||||
const auto currentTier =
|
const auto currentTier =
|
||||||
F4T::ProxyAnimationSync::ComputeLocomotionTier(a_desired.graphSpeed, a_desired.isSprinting);
|
F4T::ProxyAnimationSync::ComputeLocomotionTier(a_desired.graphSpeed, a_desired.isSprinting);
|
||||||
|
|
||||||
|
// Tier only means something while actually moving. lastLocomotionTier is reset to
|
||||||
|
// nullopt whenever the proxy goes idle (see caller), so checking "!lastLocomotionTier"
|
||||||
|
// unconditionally here made every idle tick register as "changed" (constant log/graph
|
||||||
|
// churn and spurious re-firing of edge-triggered events like weaponDraw/gunDownStateEnter
|
||||||
|
// every single update while an idle proxy sat on screen).
|
||||||
|
const bool tierChanged =
|
||||||
|
a_desired.isMoving &&
|
||||||
|
(!a_state.lastLocomotionTier || *a_state.lastLocomotionTier != currentTier);
|
||||||
|
|
||||||
return a_state.lastIsMoving != a_desired.isMoving ||
|
return a_state.lastIsMoving != a_desired.isMoving ||
|
||||||
a_state.lastIsSprinting != a_desired.isSprinting ||
|
a_state.lastIsSprinting != a_desired.isSprinting ||
|
||||||
a_state.lastIsSneaking != a_desired.isSneaking ||
|
a_state.lastIsSneaking != a_desired.isSneaking ||
|
||||||
a_state.lastIsJumping != a_desired.isJumping ||
|
a_state.lastIsJumping != a_desired.isJumping ||
|
||||||
a_state.lastIsCrouching != a_desired.isCrouching ||
|
a_state.lastIsCrouching != a_desired.isCrouching ||
|
||||||
a_state.lastWeaponDrawn != a_desired.weaponDrawn ||
|
a_state.lastWeaponDrawn != a_desired.weaponDrawn ||
|
||||||
!a_state.lastLocomotionTier || *a_state.lastLocomotionTier != currentTier ||
|
tierChanged ||
|
||||||
AreFloatsDifferent(a_state.lastGraphSpeed, a_desired.graphSpeed) ||
|
AreFloatsDifferent(a_state.lastGraphSpeed, a_desired.graphSpeed) ||
|
||||||
AreFloatsDifferent(a_state.lastDirection, a_desired.direction);
|
AreFloatsDifferent(a_state.lastDirection, a_desired.direction);
|
||||||
}
|
}
|
||||||
@@ -380,9 +405,15 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
(void)kEnableProxySneakSync;
|
(void)kEnableProxySneakSync;
|
||||||
(void)kEnableProxyWeaponDrawnSync;
|
|
||||||
(void)a_desired.isSneaking;
|
(void)a_desired.isSneaking;
|
||||||
(void)a_desired.weaponDrawn;
|
|
||||||
|
// Write weapon draw state if enabled
|
||||||
|
if constexpr (kEnableProxyWeaponDrawnSync) {
|
||||||
|
RE::IAnimationGraphManagerHolder* weaponGraphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||||
|
// iSyncGunDown: 0 = ready/drawn, 1 = holstered/gun-down
|
||||||
|
const std::int32_t gunDownValue = a_desired.weaponDrawn ? 0 : 1;
|
||||||
|
success &= TrySetGraphInt(*weaponGraphHolder, F4T::ProxyAnimationSync::GraphVar::kSyncGunDown, gunDownValue);
|
||||||
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
static std::unordered_set<std::uint32_t> loggedWriteFailures;
|
static std::unordered_set<std::uint32_t> loggedWriteFailures;
|
||||||
@@ -427,6 +458,15 @@ namespace
|
|||||||
return ApplyJumpStateToGraph(*graphHolder, true);
|
return ApplyJumpStateToGraph(*graphHolder, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The descriptor snapshot initializes every bool/int not explicitly assigned below
|
||||||
|
// to false/0. That is fine for unarmed locomotion, but while a weapon is drawn it
|
||||||
|
// clobbers weapon-state variables and the proxy drops back to default idle/holster
|
||||||
|
// immediately after the draw animation starts. Use targeted writes for armed states
|
||||||
|
// so unknown weapon graph state is preserved.
|
||||||
|
if (a_desired.weaponDrawn) {
|
||||||
|
return ApplyDesiredStateToGraph(a_proxy, a_desired, a_remotePlayerId);
|
||||||
|
}
|
||||||
|
|
||||||
auto& descriptor = F4T::AnimationDescriptor::F4AnimationDescriptor::GetHumanoidDescriptor();
|
auto& descriptor = F4T::AnimationDescriptor::F4AnimationDescriptor::GetHumanoidDescriptor();
|
||||||
F4T::AnimationDescriptor::AnimationVariableSnapshot snapshot;
|
F4T::AnimationDescriptor::AnimationVariableSnapshot snapshot;
|
||||||
|
|
||||||
@@ -600,6 +640,63 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle weapon drawn state transitions
|
||||||
|
if constexpr (kEnableProxyWeaponDrawnSync) {
|
||||||
|
// Only fire weapon events if a weapon is actually equipped on the proxy
|
||||||
|
RE::BGSEquipIndex rightHandIndex;
|
||||||
|
rightHandIndex.index = 0;
|
||||||
|
RE::BGSObjectInstance equippedItem{ nullptr, nullptr };
|
||||||
|
const auto* equippedWeapon = a_proxy.GetEquippedItem(&equippedItem, rightHandIndex);
|
||||||
|
const bool hasWeaponEquipped = equippedWeapon && equippedWeapon->object && equippedWeapon->object->Is(RE::ENUM_FORM_ID::kWEAP);
|
||||||
|
|
||||||
|
// DIAGNOSTIC: weapon FSM events (weaponDraw/readyStateEnter/gunDownStateEnter) have
|
||||||
|
// been rejected (graphAccepted=false) on every observed attempt. Log the loaded
|
||||||
|
// behavior graph count once per proxy so we can tell whether WeaponBehavior is ever
|
||||||
|
// actually attached (root-only graphs won't recognize these event names at all).
|
||||||
|
if (hasWeaponEquipped) {
|
||||||
|
static std::unordered_set<std::uint32_t> loggedWeaponGraphDiag;
|
||||||
|
if (loggedWeaponGraphDiag.insert(a_remotePlayerId).second) {
|
||||||
|
RE::BSTSmartPointer<RE::BSAnimationGraphManager> graphManager;
|
||||||
|
if (static_cast<RE::IAnimationGraphManagerHolder&>(a_proxy).GetAnimationGraphManagerImpl(graphManager) && graphManager) {
|
||||||
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
|
"Runtime proxy weapon graph DIAGNOSTIC for remote player {}: actor={:08X}, weaponFormId={:08X}, loadedGraphCount={}, boundChannelCount={}.",
|
||||||
|
a_remotePlayerId,
|
||||||
|
actorId,
|
||||||
|
equippedWeapon->object->GetFormID(),
|
||||||
|
graphManager->graph.size(),
|
||||||
|
graphManager->boundChannel.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasWeaponEquipped && a_desired.weaponDrawn) {
|
||||||
|
RE::BGSEquipIndex equipIndex;
|
||||||
|
equipIndex.index = 0;
|
||||||
|
auto* weapon = static_cast<RE::TESObjectWEAP*>(equippedWeapon->object);
|
||||||
|
RE::BGSObjectInstanceT<RE::TESObjectWEAP> weaponInstance{ weapon, nullptr };
|
||||||
|
a_proxy.AttachWeapon(weaponInstance, equipIndex);
|
||||||
|
a_proxy.DoReparentWeapon(weapon, equipIndex, true);
|
||||||
|
a_proxy.DrawWeaponMagicHands(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasWeaponEquipped && (!wasInitialized || a_previous.lastWeaponDrawn != a_desired.weaponDrawn)) {
|
||||||
|
RE::BGSEquipIndex equipIndex;
|
||||||
|
equipIndex.index = 0;
|
||||||
|
auto* weapon = static_cast<RE::TESObjectWEAP*>(equippedWeapon->object);
|
||||||
|
RE::BGSObjectInstanceT<RE::TESObjectWEAP> weaponInstance{ weapon, nullptr };
|
||||||
|
a_proxy.DoReparentWeapon(weapon, equipIndex, a_desired.weaponDrawn);
|
||||||
|
a_proxy.DrawWeaponMagicHands(a_desired.weaponDrawn);
|
||||||
|
if (a_desired.weaponDrawn) {
|
||||||
|
// Weapon is drawn - enter ready state
|
||||||
|
fireEvent(GraphEvent::kWeaponDraw);
|
||||||
|
fireEvent(GraphEvent::kReadyStateEnter);
|
||||||
|
} else {
|
||||||
|
// Weapon is holstered - enter gun-down state
|
||||||
|
fireEvent(GraphEvent::kGunDownStateEnter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if constexpr (kEnableProxySneakSync) {
|
if constexpr (kEnableProxySneakSync) {
|
||||||
if (!wasInitialized || a_previous.lastIsSneaking != a_desired.isSneaking) {
|
if (!wasInitialized || a_previous.lastIsSneaking != a_desired.isSneaking) {
|
||||||
fireEvent(a_desired.isSneaking ? GraphEvent::kSneakStart : GraphEvent::kSneakStop);
|
fireEvent(a_desired.isSneaking ? GraphEvent::kSneakStart : GraphEvent::kSneakStop);
|
||||||
|
|||||||
+28
-3
@@ -215,18 +215,43 @@ namespace
|
|||||||
return bipedObject->parent.object->GetFormID();
|
return bipedObject->parent.object->GetFormID();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<RemoteEquippedItem> CaptureLocalEquippedItems(const RE::PlayerCharacter& a_player)
|
std::uint32_t GetEquippedWeaponFormId(RE::Actor& a_actor, RE::BGSEquipIndex a_equipIndex)
|
||||||
|
{
|
||||||
|
RE::BGSObjectInstance objectInstance{ nullptr, nullptr };
|
||||||
|
const auto* item = a_actor.GetEquippedItem(&objectInstance, a_equipIndex);
|
||||||
|
|
||||||
|
if (!item || !item->object) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item->object->Is(RE::ENUM_FORM_ID::kWEAP)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item->object->GetFormID();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<RemoteEquippedItem> CaptureLocalEquippedItems(RE::PlayerCharacter& a_player)
|
||||||
{
|
{
|
||||||
std::vector<RemoteEquippedItem> equippedItems;
|
std::vector<RemoteEquippedItem> equippedItems;
|
||||||
equippedItems.reserve(kSyncedEquipmentSlots.size());
|
equippedItems.reserve(kSyncedEquipmentSlots.size() + 1); // +1 for rightHand weapon
|
||||||
|
|
||||||
for (const auto& slot : kSyncedEquipmentSlots) {
|
for (const auto& slot : kSyncedEquipmentSlots) {
|
||||||
equippedItems.push_back({
|
equippedItems.push_back({
|
||||||
std::string{ slot.name },
|
std::string{ slot.name },
|
||||||
GetEquippedArmorFormId(a_player, slot.bipedObject)
|
GetEquippedArmorFormId(const_cast<const RE::PlayerCharacter&>(a_player), slot.bipedObject)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture right-hand weapon
|
||||||
|
RE::BGSEquipIndex rightHandEquipIndex;
|
||||||
|
rightHandEquipIndex.index = 0; // Right-hand equip index
|
||||||
|
const auto rightHandWeaponFormId = GetEquippedWeaponFormId(a_player, rightHandEquipIndex);
|
||||||
|
equippedItems.push_back({
|
||||||
|
"rightHand",
|
||||||
|
rightHandWeaponFormId
|
||||||
|
});
|
||||||
|
|
||||||
return equippedItems;
|
return equippedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-5
@@ -71,7 +71,8 @@ Example after server processing:
|
|||||||
"movementSpeed": 186.4,
|
"movementSpeed": 186.4,
|
||||||
"equippedItems": [
|
"equippedItems": [
|
||||||
{ "slot": "body", "formId": "0001F66A" },
|
{ "slot": "body", "formId": "0001F66A" },
|
||||||
{ "slot": "headband", "formId": "" }
|
{ "slot": "headband", "formId": "" },
|
||||||
|
{ "slot": "rightHand", "formId": "0001F4A6" }
|
||||||
],
|
],
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"version": 3,
|
"version": 3,
|
||||||
@@ -154,10 +155,21 @@ eyes
|
|||||||
scalp
|
scalp
|
||||||
```
|
```
|
||||||
|
|
||||||
The first equipment-sync milestone sends only resolved `ARMO` form IDs and
|
Tracked slots include 16 visible apparel BIPED slots (listed above) plus:
|
||||||
empty strings for unequipped tracked slots. Power armor, weapon models,
|
|
||||||
condition, legendary instance data, tint/material overrides, and forms missing
|
```text
|
||||||
from the receiver load order are outside this packet extension.
|
rightHand Right-hand weapon slot (WEAP form ID); optional
|
||||||
|
```
|
||||||
|
|
||||||
|
The equipment-sync milestones send resolved `ARMO` and `WEAP` form IDs and
|
||||||
|
empty strings for unequipped tracked slots. Off-hand weapons, power armor,
|
||||||
|
weapon condition, legendary instance data, tint/material overrides, and forms
|
||||||
|
missing from the receiver load order are outside this packet extension.
|
||||||
|
|
||||||
|
When `rightHand` is missing, receivers keep their existing proxy weapon. When
|
||||||
|
present with an empty `formId`, the proxy is unequipped. When a `WEAP` form ID
|
||||||
|
is not found locally, receivers log a throttled warning and skip equipping
|
||||||
|
that weapon; the proxy continues with its current state and other sync intact.
|
||||||
|
|
||||||
Appearance fields are optional and additive. When `appearance` is missing,
|
Appearance fields are optional and additive. When `appearance` is missing,
|
||||||
receivers keep the proxy's existing/default body and face. When present, it is a
|
receivers keep the proxy's existing/default body and face. When present, it is a
|
||||||
|
|||||||
@@ -127,8 +127,10 @@ player exists.
|
|||||||
- Transform packets include movement state data (`isMoving`, `movementSpeed`,
|
- Transform packets include movement state data (`isMoving`, `movementSpeed`,
|
||||||
`isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`).
|
`isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`).
|
||||||
- Transform packets can include optional `equippedItems` snapshots for visible
|
- Transform packets can include optional `equippedItems` snapshots for visible
|
||||||
clothing, armor, hats, and eyewear slots. The game-thread proxy controller
|
clothing, armor, hats, eyewear, and right-hand weapon slots. The game-thread
|
||||||
applies those snapshots after proxy 3D is loaded.
|
proxy controller applies those snapshots after proxy 3D is loaded. Weapons are
|
||||||
|
synced as optional `WEAP` form IDs; missing weapons or unresolved form IDs do
|
||||||
|
not block other equipment sync.
|
||||||
- Transform packets can include optional `appearance` snapshots for best-effort
|
- Transform packets can include optional `appearance` snapshots for best-effort
|
||||||
body and face proxy visuals. The game-thread proxy controller applies supported
|
body and face proxy visuals. The game-thread proxy controller applies supported
|
||||||
fields after proxy 3D is loaded and defers race/gender switching.
|
fields after proxy 3D is loaded and defers race/gender switching.
|
||||||
@@ -136,7 +138,8 @@ player exists.
|
|||||||
`COVault109` solo cell.
|
`COVault109` solo cell.
|
||||||
- The game-thread proxy controller applies confirmed Havok animation graph
|
- The game-thread proxy controller applies confirmed Havok animation graph
|
||||||
variables to runtime proxy actors from that remote state (locomotion, sneak,
|
variables to runtime proxy actors from that remote state (locomotion, sneak,
|
||||||
jump, weapon drawn). Transform position/heading sync is unchanged.
|
jump). Weapon-drawn state is parsed but animation application is implemented
|
||||||
|
separately (Phase 2 milestone). Transform position/heading sync is unchanged.
|
||||||
|
|
||||||
## Transform Cadence
|
## Transform Cadence
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user