Improve proxy locomotion, tier & jump sync
Fix multiple proxy animation issues: stale graph Speed collapsing jog into walk, missing slow-walk/jog tiers, and jump takeoff/landing not playing. Key changes: - Prefer position-derived movementSpeed when animationGraphSpeed is missing/stale (ResolveLocomotionGraphSpeed) and fall back to graph Speed only when it agrees within a tolerance; sender now transmits movementSpeed when local graph read is 0 and TryReadLocalAnimationGraphSpeed falls back to SpeedSmoothed. - Introduce locomotion tiers (Walk/Jog/Run) with ComputeLocomotionTier and sync ints (iSyncWalkRun/iSyncLocomotionSpeed/iSyncJumpState) to the graph; fire corresponding tier events when starting or changing tier. - Fix proxy motion application: persist per-proxy velocity and worldDelta so the Update hook does not zero-out controller motion; use measured frame motion to drive animation when network flags are sparse. - Restore correct jump behavior: fire MT/weapon-compatible jump events for takeoff and landing (JumpUp/JumpStartFrom*/jumpLand/jumpLandTo*), clear bInJumpState on landing and remove the extra proxy-side jump hold so landings exit the jump loop. - Misc: add constants and thresholds for tier/run/walk/run-land decisions and update logs to include tier labels. Files updated: plugin/include/F4TProxyAnimationSync.h, plugin/src/F4TProxyAnimationSync.cpp, plugin/src/F4AnimationDescriptor.cpp, plugin/src/F4TProxyPuppet.cpp, plugin/src/F4TProxyActorController.cpp, plugin/src/main.cpp, and docs/dev-log.md.
This commit is contained in:
@@ -7,6 +7,94 @@ failed experiments, successful tests, and next steps.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-05 - Locomotion Tier + Jump Takeoff Fix (Round 2)
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Fixed three root causes from the 15:34 session log where proxies showed only medium-walk
|
||||||
|
cadence, default forward (jog) looked like walk, and jump takeoff never played (landing only).
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/include/F4TProxyAnimationSync.h`
|
||||||
|
- `plugin/src/F4TProxyAnimationSync.cpp`
|
||||||
|
- `plugin/src/F4AnimationDescriptor.cpp`
|
||||||
|
- `plugin/src/F4TProxyPuppet.cpp`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
**Stale animationGraphSpeed overriding movement tiers (log evidence):**
|
||||||
|
- `Proxy anim diag` showed `recvMovementSpeed=9.0, recvAnimGraphSpeed=148.8` and
|
||||||
|
`recvMovementSpeed=58.6, recvAnimGraphSpeed=182.2` — graph readings lagged a full send
|
||||||
|
interval behind position-derived speed, collapsing jog into walk-tier animation.
|
||||||
|
- Fix: `ResolveLocomotionGraphSpeed()` now uses `movementSpeed` as primary; only adopts
|
||||||
|
`animationGraphSpeed` when it agrees within 40% of movementSpeed.
|
||||||
|
|
||||||
|
**Within-walk cadence stuck at medium pace:**
|
||||||
|
- `F4TProxyPuppet` Update hook re-applied `SyncControllerMotionFields` with **zero velocity**
|
||||||
|
every frame, undoing the real velocity written by `ApplyProxyLocomotionFrame` after `Move()`.
|
||||||
|
- Fix: persist per-proxy `velocity` and `worldDelta` in locomotion state; hook re-applies them.
|
||||||
|
|
||||||
|
**Jump takeoff missing:**
|
||||||
|
- All RaiderRoot events (`JumpStandingStart`, `jumpLand`, etc.) logged `graphAccepted=false`.
|
||||||
|
MTBehavior/WeaponBehavior use `JumpUp`/`JumpFullBody`/`JumpLayerOn` for takeoff and
|
||||||
|
`JumpDown` for landing.
|
||||||
|
- Fix: fire MT/weapon jump events on takeoff/land; added `iSyncJumpState` + `iSyncWalkRun`
|
||||||
|
to the humanoid descriptor (0=walk, 1=jog, 2=run from graph Speed bands).
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- `xmake build` succeeds.
|
||||||
|
- In-game: slow-walk, jog (default W), run should differ; jump should show takeoff + land.
|
||||||
|
- Log: `computedGraphSpeed` should track `recvMovementSpeed` when graph was stale; jump events
|
||||||
|
should include `JumpUp`/`JumpFullBody`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-05 - Slow-Walk/Jog Speed + Ground Jump Landing Fix
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Fixed the remaining two animation issues reported after the graph-variable name correction:
|
||||||
|
proxies only showed walk/run (no slow-walk/jog), and ground jumps never exited the jump loop.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/include/F4TProxyAnimationSync.h`
|
||||||
|
- `plugin/src/F4TProxyAnimationSync.cpp`
|
||||||
|
- `plugin/src/main.cpp`
|
||||||
|
- `plugin/src/F4TProxyActorController.cpp`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
**Slow-walk / jog missing (log evidence):**
|
||||||
|
- `Local send diag` showed `movementSpeed=58.5` with `localGraphSpeedRead=0.0` on many frames.
|
||||||
|
- Receivers treated `animationGraphSpeed=0.0` as valid and drove graph Speed to 0, collapsing
|
||||||
|
all sub-run paces to idle/walk. Only when the live graph read returned ~176 or ~373 did
|
||||||
|
jog/run appear.
|
||||||
|
- Fix: `ResolveLocomotionGraphSpeed()` ignores animationGraphSpeed unless it is above the idle
|
||||||
|
threshold; falls back to position-derived `movementSpeed` (~1:1). Sender now transmits
|
||||||
|
`movementSpeed` when the local graph read is 0. `TryReadLocalAnimationGraphSpeed` also
|
||||||
|
tries `SpeedSmoothed` as a fallback.
|
||||||
|
|
||||||
|
**Ground jump never ends:**
|
||||||
|
- Takeoff used `JumpStart`; landing tried `moveStart`/`moveStop`, which does not exit the
|
||||||
|
ground-jump loop (table drops work via a different fall path).
|
||||||
|
- Proxy also held `isJumping` for 400ms after the network cleared, delaying landing events.
|
||||||
|
- Fix: use real FO4 events from behavior-graph research:
|
||||||
|
- Takeoff: `JumpStandingStart` / `JumpDirectionalStart`
|
||||||
|
- Landing: `jumpLand` + `jumpLandToWalk`/`jumpLandToRun` + `JumpStandingEnd`, clear
|
||||||
|
`bInJumpState`, then re-assert locomotion.
|
||||||
|
- Removed proxy-side jump hold; sender's 200ms hold is sufficient.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- `xmake build` succeeds.
|
||||||
|
- In-game: slow-walk (~50-75), jog (~150-220), run (~300+) should each blend via graph Speed;
|
||||||
|
ground jumps should land and return to locomotion. Check log for `jumpLand` events with
|
||||||
|
`graphAccepted=true`.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- Jump land run/walk threshold (150 u/s) may need tuning against in-game feel.
|
||||||
|
- Sprint animation tier still depends on `IsSprinting` + speed floor, not yet verified.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Verify all three locomotion tiers and ground-jump landing in a fresh log.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-06-05 - Fix False Jump State Masking Locomotion Animation
|
## 2026-06-05 - Fix False Jump State Masking Locomotion Animation
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|||||||
@@ -30,12 +30,35 @@ namespace F4T::ProxyAnimationSync
|
|||||||
inline constexpr const char* kIsSprinting = "IsSprinting";
|
inline constexpr const char* kIsSprinting = "IsSprinting";
|
||||||
inline constexpr const char* kInJumpState = "bInJumpState";
|
inline constexpr const char* kInJumpState = "bInJumpState";
|
||||||
inline constexpr const char* kIsInSneak = "iIsInSneak";
|
inline constexpr const char* kIsInSneak = "iIsInSneak";
|
||||||
|
inline constexpr const char* kSyncWalkRun = "iSyncWalkRun";
|
||||||
|
inline constexpr const char* kSyncLocomotionSpeed = "iSyncLocomotionSpeed";
|
||||||
|
inline constexpr const char* kSyncJumpState = "iSyncJumpState";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps network movementSpeed (units/sec) to graph Speed (~0-105). Used when the sender
|
// MTLocomotionBlend_SM sub-states: Walk(0), Jog(1), Run(2). Selected by graph events
|
||||||
// does not provide animationGraphSpeed.
|
// "Walk"/"Jog"/"Run" and mirrored in iSyncLocomotionSpeed.
|
||||||
|
enum class LocomotionTier : std::int32_t
|
||||||
|
{
|
||||||
|
kWalk = 0,
|
||||||
|
kJog = 1,
|
||||||
|
kRun = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
LocomotionTier ComputeLocomotionTier(float a_graphSpeed, bool a_isSprinting);
|
||||||
|
|
||||||
|
// Maps network movementSpeed (units/sec) to graph Speed. Used when animationGraphSpeed
|
||||||
|
// is missing or unreliable.
|
||||||
float MapMovementSpeedToGraphSpeed(float a_movementSpeed, bool a_isSprinting);
|
float MapMovementSpeedToGraphSpeed(float a_movementSpeed, bool a_isSprinting);
|
||||||
|
|
||||||
|
// Picks the graph Speed to drive locomotion blending. Prefers animationGraphSpeed only
|
||||||
|
// when it is above the idle threshold; a read of 0.0 while moving is common and must
|
||||||
|
// not override the position-derived movementSpeed (which carries slow-walk/jog tiers).
|
||||||
|
float ResolveLocomotionGraphSpeed(
|
||||||
|
float a_movementSpeed,
|
||||||
|
float a_animationGraphSpeed,
|
||||||
|
bool a_isMoving,
|
||||||
|
bool a_isSprinting);
|
||||||
|
|
||||||
struct ProxyAppliedAnimationState
|
struct ProxyAppliedAnimationState
|
||||||
{
|
{
|
||||||
bool hasAppliedGraphState{ false };
|
bool hasAppliedGraphState{ false };
|
||||||
@@ -47,6 +70,7 @@ namespace F4T::ProxyAnimationSync
|
|||||||
bool lastWeaponDrawn{ false };
|
bool lastWeaponDrawn{ false };
|
||||||
float lastGraphSpeed{ 0.0F };
|
float lastGraphSpeed{ 0.0F };
|
||||||
float lastDirection{ 0.0F };
|
float lastDirection{ 0.0F };
|
||||||
|
std::optional<LocomotionTier> lastLocomotionTier{};
|
||||||
std::chrono::steady_clock::time_point movingHoldUntil{};
|
std::chrono::steady_clock::time_point movingHoldUntil{};
|
||||||
std::chrono::steady_clock::time_point jumpHoldUntil{};
|
std::chrono::steady_clock::time_point jumpHoldUntil{};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ namespace F4T::AnimationDescriptor
|
|||||||
// ===== INT VARIABLES (VARIABLE_TYPE_INT32) =====
|
// ===== INT VARIABLES (VARIABLE_TYPE_INT32) =====
|
||||||
m_intVariables = {
|
m_intVariables = {
|
||||||
"iIsInSneak", // 0 - sneak/crouch state (0 = standing, 1 = sneaking)
|
"iIsInSneak", // 0 - sneak/crouch state (0 = standing, 1 = sneaking)
|
||||||
|
"iSyncWalkRun", // 1 - locomotion tier sync (0 = walk, 1 = jog, 2 = run)
|
||||||
|
"iSyncLocomotionSpeed", // 2 - MTLocomotionBlend_SM start state (0/1/2)
|
||||||
|
"iSyncJumpState", // 3 - jump sync state (1 while airborne)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build reverse lookup map for int variables
|
// Build reverse lookup map for int variables
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ namespace
|
|||||||
std::optional<std::string> lastObservedMovementType;
|
std::optional<std::string> lastObservedMovementType;
|
||||||
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
|
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
|
||||||
std::optional<bool> lastAppliedVisualSneakingState;
|
std::optional<bool> lastAppliedVisualSneakingState;
|
||||||
|
std::optional<RemotePlayer> lastRemotePlayerSnapshot;
|
||||||
F4T::ProxyAnimationSync::ProxyAppliedAnimationState appliedAnimationState{};
|
F4T::ProxyAnimationSync::ProxyAppliedAnimationState appliedAnimationState{};
|
||||||
|
|
||||||
// Phase 5.1: Action queue for discrete action replay
|
// Phase 5.1: Action queue for discrete action replay
|
||||||
@@ -4812,6 +4813,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
a_slot.restoredForCurrentState = true;
|
a_slot.restoredForCurrentState = true;
|
||||||
a_slot.firstSmoothedMovementLogged = false;
|
a_slot.firstSmoothedMovementLogged = false;
|
||||||
a_slot.remoteStateMovedLogged = false;
|
a_slot.remoteStateMovedLogged = false;
|
||||||
|
a_slot.lastRemotePlayerSnapshot = a_remotePlayer;
|
||||||
|
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Runtime proxy actor {:08X} restored/reused for remote player {} at visible X={:.2f}, Y={:.2f}, Z={:.2f} from remote X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
"Runtime proxy actor {:08X} restored/reused for remote player {} at visible X={:.2f}, Y={:.2f}, Z={:.2f} from remote X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
||||||
@@ -4969,16 +4971,14 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
a_slot.targetPosition = targetPosition;
|
a_slot.targetPosition = targetPosition;
|
||||||
a_slot.targetHeading = a_remotePlayer.angleZ;
|
a_slot.targetHeading = a_remotePlayer.angleZ;
|
||||||
a_slot.puppetDirection = a_remotePlayer.angleZ;
|
a_slot.puppetDirection = a_remotePlayer.angleZ;
|
||||||
|
a_slot.lastRemotePlayerSnapshot = a_remotePlayer;
|
||||||
|
|
||||||
if (a_remotePlayer.isMoving && a_remotePlayer.movementSpeed >= 1.0F) {
|
if (a_remotePlayer.isMoving && a_remotePlayer.movementSpeed >= 1.0F) {
|
||||||
if (a_remotePlayer.animationGraphSpeed >= 0.0F &&
|
a_slot.puppetGraphSpeed = F4T::ProxyAnimationSync::ResolveLocomotionGraphSpeed(
|
||||||
std::isfinite(a_remotePlayer.animationGraphSpeed)) {
|
|
||||||
a_slot.puppetGraphSpeed = a_remotePlayer.animationGraphSpeed;
|
|
||||||
} else {
|
|
||||||
a_slot.puppetGraphSpeed = F4T::ProxyAnimationSync::MapMovementSpeedToGraphSpeed(
|
|
||||||
a_remotePlayer.movementSpeed,
|
a_remotePlayer.movementSpeed,
|
||||||
|
a_remotePlayer.animationGraphSpeed,
|
||||||
|
a_remotePlayer.isMoving,
|
||||||
a_remotePlayer.isSprinting);
|
a_remotePlayer.isSprinting);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
a_slot.puppetGraphSpeed = -1.0F;
|
a_slot.puppetGraphSpeed = -1.0F;
|
||||||
}
|
}
|
||||||
@@ -5359,6 +5359,12 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
frameDeltaSeconds,
|
frameDeltaSeconds,
|
||||||
-1.0F,
|
-1.0F,
|
||||||
slot.puppetDirection);
|
slot.puppetDirection);
|
||||||
|
if (slot.lastRemotePlayerSnapshot) {
|
||||||
|
F4T::ProxyAnimationSync::ApplyProxyAnimationFromRemoteState(
|
||||||
|
slot.appliedAnimationState,
|
||||||
|
*proxy,
|
||||||
|
*slot.lastRemotePlayerSnapshot);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5388,6 +5394,33 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
frameDeltaSeconds,
|
frameDeltaSeconds,
|
||||||
slot.puppetGraphSpeed,
|
slot.puppetGraphSpeed,
|
||||||
slot.puppetDirection);
|
slot.puppetDirection);
|
||||||
|
|
||||||
|
// Drive animation from measured proxy motion every frame. Network isMoving is
|
||||||
|
// often false while the proxy is visibly jogging (log: recvMoving=false during
|
||||||
|
// active translation). MTBehavior also needs explicit Jog/Run/Walk events.
|
||||||
|
if (slot.lastRemotePlayerSnapshot) {
|
||||||
|
auto effectiveRemote = *slot.lastRemotePlayerSnapshot;
|
||||||
|
const float horizontalDelta =
|
||||||
|
std::sqrt((frameDelta.x * frameDelta.x) + (frameDelta.y * frameDelta.y));
|
||||||
|
const float measuredSpeed =
|
||||||
|
frameDeltaSeconds > 0.0F ? horizontalDelta / frameDeltaSeconds : 0.0F;
|
||||||
|
const float puppetSpeed =
|
||||||
|
slot.puppetGraphSpeed >= 0.0F ? slot.puppetGraphSpeed : measuredSpeed;
|
||||||
|
const float locomotionSpeed = std::max(measuredSpeed, puppetSpeed);
|
||||||
|
|
||||||
|
if (locomotionSpeed >= 8.0F) {
|
||||||
|
effectiveRemote.isMoving = true;
|
||||||
|
effectiveRemote.movementSpeed = locomotionSpeed;
|
||||||
|
if (effectiveRemote.animationGraphSpeed < 1.0F) {
|
||||||
|
effectiveRemote.animationGraphSpeed = locomotionSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
F4T::ProxyAnimationSync::ApplyProxyAnimationFromRemoteState(
|
||||||
|
slot.appliedAnimationState,
|
||||||
|
*proxy,
|
||||||
|
effectiveRemote);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,9 +45,14 @@ namespace
|
|||||||
// Hold duration for movement stop debounce. Must be short so animation stop events
|
// Hold duration for movement stop debounce. Must be short so animation stop events
|
||||||
// fire promptly; long holds cause characters to glide while playing stop animations.
|
// fire promptly; long holds cause characters to glide while playing stop animations.
|
||||||
constexpr auto kProxyMovingStopHoldDuration = std::chrono::milliseconds{ 50 };
|
constexpr auto kProxyMovingStopHoldDuration = std::chrono::milliseconds{ 50 };
|
||||||
// Minimum time to keep a remote jump "active" on the proxy so takeoff animation can play
|
// Graph Speed above ~150 selects run-tier landings (jumpLandToRun); below uses walk-tier.
|
||||||
// before we fire landing/locomotion exit events (network updates are ~100ms apart).
|
constexpr float kJumpLandRunSpeedThreshold = 150.0F;
|
||||||
constexpr auto kProxyJumpMinDuration = std::chrono::milliseconds{ 400 };
|
// Reject animationGraphSpeed when it diverges this much from movementSpeed (stale graph
|
||||||
|
// readings lag a full send interval and collapse jog into walk-tier animation).
|
||||||
|
constexpr float kGraphSpeedAgreementRatio = 0.4F;
|
||||||
|
// FO4 default forward (W / full stick) is jog. Slow walk is below kWalkTierMax.
|
||||||
|
constexpr float kWalkTierMax = 90.0F;
|
||||||
|
constexpr float kRunGraphSpeedThreshold = 280.0F;
|
||||||
|
|
||||||
// Graph Speed range confirmed from local-player read-by-name debug in
|
// Graph Speed range confirmed from local-player read-by-name debug in
|
||||||
// Fallout4Together.log: walk starts ~47, full run ~373. FO4's graph "Speed" is in the
|
// Fallout4Together.log: walk starts ~47, full run ~373. FO4's graph "Speed" is in the
|
||||||
@@ -125,16 +130,26 @@ namespace
|
|||||||
// which doubles as a diagnostic for which event names this FO4 graph accepts.
|
// which doubles as a diagnostic for which event names this FO4 graph accepts.
|
||||||
namespace GraphEvent
|
namespace GraphEvent
|
||||||
{
|
{
|
||||||
|
// MTBehavior event names (case-sensitive; from MTBehavior.hkx eventNames).
|
||||||
inline constexpr const char* kMoveStart = "moveStart";
|
inline constexpr const char* kMoveStart = "moveStart";
|
||||||
inline constexpr const char* kMoveStop = "moveStop";
|
inline constexpr const char* kMoveStop = "moveStop";
|
||||||
inline constexpr const char* kSprintStart = "SprintStart";
|
inline constexpr const char* kSprintStart = "sprintStart";
|
||||||
inline constexpr const char* kSprintStop = "SprintStop";
|
inline constexpr const char* kSprintStop = "sprintStop";
|
||||||
inline constexpr const char* kSneakStart = "SneakStart";
|
inline constexpr const char* kSneakStart = "sneakStart";
|
||||||
inline constexpr const char* kSneakStop = "SneakStop";
|
inline constexpr const char* kSneakStop = "sneakStop";
|
||||||
// JumpStart is accepted by the FO4 proxy graph (takeoff). JumpStop is not reliable
|
// MTLocomotionBlend_SM tier transitions (Walk state 0, Jog state 1, Run state 2).
|
||||||
// for landing; use moveStart/moveStop to exit the jump state instead.
|
inline constexpr const char* kWalk = "Walk";
|
||||||
inline constexpr const char* kJumpStart = "JumpStart";
|
inline constexpr const char* kJog = "Jog";
|
||||||
inline constexpr const char* kJumpStop = "JumpStop";
|
inline constexpr const char* kRun = "Run";
|
||||||
|
inline constexpr const char* kJumpStart = "jumpStart";
|
||||||
|
inline constexpr const char* kJumpStartFromWalk = "jumpStartFromWalk";
|
||||||
|
inline constexpr const char* kJumpStartFromRun = "jumpStartFromRun";
|
||||||
|
inline constexpr const char* kJumpUp = "JumpUp";
|
||||||
|
inline constexpr const char* kJumpDown = "JumpDown";
|
||||||
|
inline constexpr const char* kJumpLand = "jumpLand";
|
||||||
|
inline constexpr const char* kJumpLandToWalk = "jumpLandToWalk";
|
||||||
|
inline constexpr const char* kJumpLandToRun = "jumpLandToRun";
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
@@ -173,6 +188,20 @@ namespace
|
|||||||
return std::clamp(a_speed, kGraphSpeedMin, kGraphSpeedMax);
|
return std::clamp(a_speed, kGraphSpeedMin, kGraphSpeedMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* GetLocomotionTierEventName(F4T::ProxyAnimationSync::LocomotionTier a_tier)
|
||||||
|
{
|
||||||
|
switch (a_tier) {
|
||||||
|
case F4T::ProxyAnimationSync::LocomotionTier::kWalk:
|
||||||
|
return GraphEvent::kWalk;
|
||||||
|
case F4T::ProxyAnimationSync::LocomotionTier::kJog:
|
||||||
|
return GraphEvent::kJog;
|
||||||
|
case F4T::ProxyAnimationSync::LocomotionTier::kRun:
|
||||||
|
return GraphEvent::kRun;
|
||||||
|
default:
|
||||||
|
return GraphEvent::kJog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PopulateLocomotionSpeedSnapshot(
|
void PopulateLocomotionSpeedSnapshot(
|
||||||
F4T::AnimationDescriptor::AnimationVariableSnapshot& a_snapshot,
|
F4T::AnimationDescriptor::AnimationVariableSnapshot& a_snapshot,
|
||||||
const F4T::AnimationDescriptor::F4AnimationDescriptor& a_descriptor,
|
const F4T::AnimationDescriptor::F4AnimationDescriptor& a_descriptor,
|
||||||
@@ -193,17 +222,10 @@ namespace
|
|||||||
|
|
||||||
float ComputeGraphSpeed(const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
float ComputeGraphSpeed(const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
||||||
{
|
{
|
||||||
if (!a_remotePlayer.isMoving || a_remotePlayer.movementSpeed < kIdleSpeedThreshold) {
|
return F4T::ProxyAnimationSync::ResolveLocomotionGraphSpeed(
|
||||||
return kGraphSpeedIdle;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer the sender's live graph Speed (TiltedEvolution sends full variable snapshots).
|
|
||||||
if (a_remotePlayer.animationGraphSpeed >= 0.0F && std::isfinite(a_remotePlayer.animationGraphSpeed)) {
|
|
||||||
return ClampGraphSpeed(a_remotePlayer.animationGraphSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return F4T::ProxyAnimationSync::MapMovementSpeedToGraphSpeed(
|
|
||||||
a_remotePlayer.movementSpeed,
|
a_remotePlayer.movementSpeed,
|
||||||
|
a_remotePlayer.animationGraphSpeed,
|
||||||
|
a_remotePlayer.isMoving,
|
||||||
a_remotePlayer.isSprinting);
|
a_remotePlayer.isSprinting);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +244,9 @@ namespace
|
|||||||
DesiredProxyAnimationState BuildDesiredState(const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
DesiredProxyAnimationState BuildDesiredState(const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
||||||
{
|
{
|
||||||
DesiredProxyAnimationState desired{};
|
DesiredProxyAnimationState desired{};
|
||||||
desired.isMoving = a_remotePlayer.isMoving && a_remotePlayer.movementSpeed >= kIdleSpeedThreshold;
|
// movementSpeed can be valid while isMoving is false (sparse network flags).
|
||||||
|
const bool hasLocomotionSpeed = a_remotePlayer.movementSpeed >= kIdleSpeedThreshold;
|
||||||
|
desired.isMoving = (a_remotePlayer.isMoving || hasLocomotionSpeed) && hasLocomotionSpeed;
|
||||||
desired.isSprinting = desired.isMoving && a_remotePlayer.isSprinting;
|
desired.isSprinting = desired.isMoving && a_remotePlayer.isSprinting;
|
||||||
desired.isSneaking = a_remotePlayer.isSneaking;
|
desired.isSneaking = a_remotePlayer.isSneaking;
|
||||||
desired.isJumping = a_remotePlayer.isJumping;
|
desired.isJumping = a_remotePlayer.isJumping;
|
||||||
@@ -246,21 +270,6 @@ namespace
|
|||||||
return a_state.movingHoldUntil.time_since_epoch().count() != 0 && now < a_state.movingHoldUntil;
|
return a_state.movingHoldUntil.time_since_epoch().count() != 0 && now < a_state.movingHoldUntil;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ApplyJumpHoldState(
|
|
||||||
F4T::ProxyAnimationSync::ProxyAppliedAnimationState& a_state,
|
|
||||||
bool a_rawIsJumping)
|
|
||||||
{
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
|
||||||
if (a_rawIsJumping) {
|
|
||||||
if (a_state.jumpHoldUntil.time_since_epoch().count() == 0 || now >= a_state.jumpHoldUntil) {
|
|
||||||
a_state.jumpHoldUntil = now + kProxyJumpMinDuration;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a_state.jumpHoldUntil.time_since_epoch().count() != 0 && now < a_state.jumpHoldUntil;
|
|
||||||
}
|
|
||||||
|
|
||||||
DesiredProxyAnimationState BuildDebouncedDesiredState(
|
DesiredProxyAnimationState BuildDebouncedDesiredState(
|
||||||
F4T::ProxyAnimationSync::ProxyAppliedAnimationState& a_state,
|
F4T::ProxyAnimationSync::ProxyAppliedAnimationState& a_state,
|
||||||
const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
||||||
@@ -269,9 +278,9 @@ namespace
|
|||||||
auto desired = rawDesired;
|
auto desired = rawDesired;
|
||||||
// Apply minimal hold (50ms) for debounce, but don't coast; snap to idle immediately.
|
// Apply minimal hold (50ms) for debounce, but don't coast; snap to idle immediately.
|
||||||
desired.isMoving = ApplyMovingHoldState(a_state, rawDesired.isMoving);
|
desired.isMoving = ApplyMovingHoldState(a_state, rawDesired.isMoving);
|
||||||
if constexpr (kEnableProxyJumpSync) {
|
// Jump state follows the network value directly. The sender already holds isJumping
|
||||||
desired.isJumping = ApplyJumpHoldState(a_state, rawDesired.isJumping);
|
// briefly for packet coalescing; an extra proxy-side hold delayed landing events and
|
||||||
}
|
// kept bInJumpState true after ground landings.
|
||||||
|
|
||||||
if (!desired.isMoving) {
|
if (!desired.isMoving) {
|
||||||
desired.isSprinting = false;
|
desired.isSprinting = false;
|
||||||
@@ -293,12 +302,16 @@ namespace
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto currentTier =
|
||||||
|
F4T::ProxyAnimationSync::ComputeLocomotionTier(a_desired.graphSpeed, a_desired.isSprinting);
|
||||||
|
|
||||||
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 ||
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -389,6 +402,11 @@ namespace
|
|||||||
if (inJumpIdx != static_cast<std::size_t>(-1)) {
|
if (inJumpIdx != static_cast<std::size_t>(-1)) {
|
||||||
snapshot.bools[inJumpIdx] = a_desired.isJumping;
|
snapshot.bools[inJumpIdx] = a_desired.isJumping;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto syncJumpIdx = descriptor.GetIntVariableIndex(F4T::ProxyAnimationSync::GraphVar::kSyncJumpState);
|
||||||
|
if (syncJumpIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.ints[syncJumpIdx] = a_desired.isJumping ? 1U : 0U;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if constexpr (kEnableProxySneakSync) {
|
if constexpr (kEnableProxySneakSync) {
|
||||||
@@ -398,6 +416,23 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if constexpr (kEnableProxyLocomotionSync) {
|
||||||
|
const auto tier = static_cast<std::int32_t>(
|
||||||
|
F4T::ProxyAnimationSync::ComputeLocomotionTier(a_desired.graphSpeed, a_desired.isSprinting));
|
||||||
|
const auto tierValue = static_cast<std::uint32_t>(tier);
|
||||||
|
|
||||||
|
const auto syncWalkRunIdx = descriptor.GetIntVariableIndex(F4T::ProxyAnimationSync::GraphVar::kSyncWalkRun);
|
||||||
|
if (syncWalkRunIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.ints[syncWalkRunIdx] = tierValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto syncLocomotionIdx =
|
||||||
|
descriptor.GetIntVariableIndex(F4T::ProxyAnimationSync::GraphVar::kSyncLocomotionSpeed);
|
||||||
|
if (syncLocomotionIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.ints[syncLocomotionIdx] = tierValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Single bulk write operation (major performance win)
|
// Single bulk write operation (major performance win)
|
||||||
const auto success = descriptor.LoadAnimationVariablesToCache(*graphHolder, snapshot);
|
const auto success = descriptor.LoadAnimationVariablesToCache(*graphHolder, snapshot);
|
||||||
|
|
||||||
@@ -440,9 +475,21 @@ namespace
|
|||||||
accepted));
|
accepted));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Movement start/stop is the critical transition that takes the graph out of idle.
|
const auto currentTier =
|
||||||
if (!wasInitialized || a_previous.lastIsMoving != a_desired.isMoving) {
|
F4T::ProxyAnimationSync::ComputeLocomotionTier(a_desired.graphSpeed, a_desired.isSprinting);
|
||||||
fireEvent(a_desired.isMoving ? GraphEvent::kMoveStart : GraphEvent::kMoveStop);
|
const bool startedMoving = a_desired.isMoving && (!wasInitialized || !a_previous.lastIsMoving);
|
||||||
|
const bool stoppedMoving = wasInitialized && a_previous.lastIsMoving && !a_desired.isMoving;
|
||||||
|
const bool tierChanged =
|
||||||
|
a_desired.isMoving &&
|
||||||
|
(!a_previous.lastLocomotionTier || *a_previous.lastLocomotionTier != currentTier);
|
||||||
|
|
||||||
|
if (startedMoving) {
|
||||||
|
fireEvent(GraphEvent::kMoveStart);
|
||||||
|
fireEvent(GetLocomotionTierEventName(currentTier));
|
||||||
|
} else if (stoppedMoving) {
|
||||||
|
fireEvent(GraphEvent::kMoveStop);
|
||||||
|
} else if (tierChanged) {
|
||||||
|
fireEvent(GetLocomotionTierEventName(currentTier));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sprint sub-state transition (only meaningful while moving).
|
// Sprint sub-state transition (only meaningful while moving).
|
||||||
@@ -457,13 +504,39 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
if constexpr (kEnableProxyJumpSync) {
|
if constexpr (kEnableProxyJumpSync) {
|
||||||
|
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||||
if (!wasInitialized || a_previous.lastIsJumping != a_desired.isJumping) {
|
if (!wasInitialized || a_previous.lastIsJumping != a_desired.isJumping) {
|
||||||
if (a_desired.isJumping) {
|
if (a_desired.isJumping) {
|
||||||
|
const auto jumpTier = a_previous.lastLocomotionTier.value_or(currentTier);
|
||||||
|
switch (jumpTier) {
|
||||||
|
case F4T::ProxyAnimationSync::LocomotionTier::kRun:
|
||||||
|
fireEvent(GraphEvent::kJumpStartFromRun);
|
||||||
|
break;
|
||||||
|
case F4T::ProxyAnimationSync::LocomotionTier::kWalk:
|
||||||
|
fireEvent(GraphEvent::kJumpStartFromWalk);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
fireEvent(GraphEvent::kJumpStart);
|
fireEvent(GraphEvent::kJumpStart);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fireEvent(GraphEvent::kJumpUp);
|
||||||
|
TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kInJumpState, true);
|
||||||
} else if (wasInitialized) {
|
} else if (wasInitialized) {
|
||||||
// JumpStop is not consumed by the graph; re-assert ground locomotion to
|
fireEvent(GraphEvent::kJumpDown);
|
||||||
// exit the jump loop (same mechanism as "jump off a table" resetting it).
|
fireEvent(GraphEvent::kJumpLand);
|
||||||
fireEvent(a_desired.isMoving ? GraphEvent::kMoveStart : GraphEvent::kMoveStop);
|
if (a_desired.isMoving) {
|
||||||
|
if (currentTier == F4T::ProxyAnimationSync::LocomotionTier::kRun) {
|
||||||
|
fireEvent(GraphEvent::kJumpLandToRun);
|
||||||
|
} else {
|
||||||
|
fireEvent(GraphEvent::kJumpLandToWalk);
|
||||||
|
}
|
||||||
|
fireEvent(GraphEvent::kMoveStart);
|
||||||
|
fireEvent(GetLocomotionTierEventName(currentTier));
|
||||||
|
} else {
|
||||||
|
fireEvent(GraphEvent::kJumpEnd);
|
||||||
|
fireEvent(GraphEvent::kMoveStop);
|
||||||
|
}
|
||||||
|
TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kInJumpState, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,12 +554,23 @@ namespace
|
|||||||
const DesiredProxyAnimationState& a_desired,
|
const DesiredProxyAnimationState& a_desired,
|
||||||
bool a_initial)
|
bool a_initial)
|
||||||
{
|
{
|
||||||
|
const auto tier = F4T::ProxyAnimationSync::ComputeLocomotionTier(
|
||||||
|
a_desired.graphSpeed,
|
||||||
|
a_desired.isSprinting);
|
||||||
|
const char* tierLabel = "jog";
|
||||||
|
if (tier == F4T::ProxyAnimationSync::LocomotionTier::kWalk) {
|
||||||
|
tierLabel = "walk";
|
||||||
|
} else if (tier == F4T::ProxyAnimationSync::LocomotionTier::kRun) {
|
||||||
|
tierLabel = "run";
|
||||||
|
}
|
||||||
|
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, graphSpeed={:.1f}, direction={:.3f}.",
|
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, tier={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, graphSpeed={:.1f}, direction={:.3f}.",
|
||||||
a_initial ? "initial" : "changed",
|
a_initial ? "initial" : "changed",
|
||||||
a_remotePlayerId,
|
a_remotePlayerId,
|
||||||
a_actorId,
|
a_actorId,
|
||||||
a_desired.isMoving,
|
a_desired.isMoving,
|
||||||
|
tierLabel,
|
||||||
a_desired.isSprinting,
|
a_desired.isSprinting,
|
||||||
a_desired.isSneaking,
|
a_desired.isSneaking,
|
||||||
a_desired.isJumping,
|
a_desired.isJumping,
|
||||||
@@ -517,6 +601,32 @@ namespace F4T::ProxyAnimationSync
|
|||||||
return ClampGraphSpeed(graphSpeed);
|
return ClampGraphSpeed(graphSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float ResolveLocomotionGraphSpeed(
|
||||||
|
float a_movementSpeed,
|
||||||
|
float a_animationGraphSpeed,
|
||||||
|
bool a_isMoving,
|
||||||
|
bool a_isSprinting)
|
||||||
|
{
|
||||||
|
if (!a_isMoving || a_movementSpeed < kIdleSpeedThreshold) {
|
||||||
|
return kGraphSpeedIdle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position-derived movementSpeed tracks the sender's real pace tiers. Graph Speed
|
||||||
|
// can lag a full network interval (log: movement=9, graph=148), which collapses jog
|
||||||
|
// into walk-tier animation when we preferred the stale graph reading.
|
||||||
|
auto graphSpeed = MapMovementSpeedToGraphSpeed(a_movementSpeed, a_isSprinting);
|
||||||
|
|
||||||
|
if (a_animationGraphSpeed > kIdleSpeedThreshold && std::isfinite(a_animationGraphSpeed)) {
|
||||||
|
const float baseline = std::max(a_movementSpeed, 1.0F);
|
||||||
|
const float divergence = std::fabs(a_animationGraphSpeed - a_movementSpeed) / baseline;
|
||||||
|
if (divergence <= kGraphSpeedAgreementRatio) {
|
||||||
|
graphSpeed = ClampGraphSpeed(a_animationGraphSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return graphSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
void ResetAppliedAnimationState(ProxyAppliedAnimationState& a_state)
|
void ResetAppliedAnimationState(ProxyAppliedAnimationState& a_state)
|
||||||
{
|
{
|
||||||
a_state = {};
|
a_state = {};
|
||||||
@@ -607,8 +717,25 @@ namespace F4T::ProxyAnimationSync
|
|||||||
a_state.lastWeaponDrawn = desired.weaponDrawn;
|
a_state.lastWeaponDrawn = desired.weaponDrawn;
|
||||||
a_state.lastGraphSpeed = desired.graphSpeed;
|
a_state.lastGraphSpeed = desired.graphSpeed;
|
||||||
a_state.lastDirection = desired.direction;
|
a_state.lastDirection = desired.direction;
|
||||||
|
if (desired.isMoving) {
|
||||||
|
a_state.lastLocomotionTier =
|
||||||
|
ComputeLocomotionTier(desired.graphSpeed, desired.isSprinting);
|
||||||
|
} else {
|
||||||
|
a_state.lastLocomotionTier.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocomotionTier ComputeLocomotionTier(float a_graphSpeed, bool a_isSprinting)
|
||||||
|
{
|
||||||
|
if (a_isSprinting || a_graphSpeed >= kRunGraphSpeedThreshold) {
|
||||||
|
return LocomotionTier::kRun;
|
||||||
|
}
|
||||||
|
if (a_graphSpeed > kWalkTierMax) {
|
||||||
|
return LocomotionTier::kJog;
|
||||||
|
}
|
||||||
|
return LocomotionTier::kWalk;
|
||||||
|
}
|
||||||
|
|
||||||
void ClearProxyAnimationState(
|
void ClearProxyAnimationState(
|
||||||
ProxyAppliedAnimationState& a_state,
|
ProxyAppliedAnimationState& a_state,
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ namespace F4T::ProxyPuppet
|
|||||||
{
|
{
|
||||||
float speed{ -1.0F };
|
float speed{ -1.0F };
|
||||||
float direction{ 0.0F };
|
float direction{ 0.0F };
|
||||||
|
RE::NiPoint3 velocity{};
|
||||||
|
RE::NiPoint3 worldDelta{};
|
||||||
};
|
};
|
||||||
|
|
||||||
std::shared_mutex g_proxyFormIdMutex;
|
std::shared_mutex g_proxyFormIdMutex;
|
||||||
@@ -160,11 +162,10 @@ namespace F4T::ProxyPuppet
|
|||||||
a_this->UpdateNoAI(a_delta);
|
a_this->UpdateNoAI(a_delta);
|
||||||
|
|
||||||
if (locomotion.speed >= 0.0F) {
|
if (locomotion.speed >= 0.0F) {
|
||||||
const RE::NiPoint3 zeroVelocity{};
|
SyncControllerMotionFields(*a_this, locomotion.velocity, locomotion.speed);
|
||||||
SyncControllerMotionFields(*a_this, zeroVelocity, locomotion.speed);
|
|
||||||
TryPushMotionFeedbackToGraph(
|
TryPushMotionFeedbackToGraph(
|
||||||
*a_this,
|
*a_this,
|
||||||
RE::NiPoint3{},
|
locomotion.worldDelta,
|
||||||
a_delta,
|
a_delta,
|
||||||
locomotion.speed,
|
locomotion.speed,
|
||||||
locomotion.direction);
|
locomotion.direction);
|
||||||
@@ -228,7 +229,9 @@ namespace F4T::ProxyPuppet
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::unique_lock lock{ g_proxyFormIdMutex };
|
const std::unique_lock lock{ g_proxyFormIdMutex };
|
||||||
g_proxyLocomotion[a_formID] = ProxyLocomotionState{ a_speed, a_direction };
|
auto& state = g_proxyLocomotion[a_formID];
|
||||||
|
state.speed = a_speed;
|
||||||
|
state.direction = a_direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetProxyDesiredSpeed(std::uint32_t a_formID, float a_speed)
|
void SetProxyDesiredSpeed(std::uint32_t a_formID, float a_speed)
|
||||||
@@ -261,12 +264,17 @@ namespace F4T::ProxyPuppet
|
|||||||
(a_graphSpeed >= 0.0F && std::isfinite(a_graphSpeed)) ? a_graphSpeed : horizontalSpeed;
|
(a_graphSpeed >= 0.0F && std::isfinite(a_graphSpeed)) ? a_graphSpeed : horizontalSpeed;
|
||||||
|
|
||||||
if (feedbackSpeed < 8.0F) {
|
if (feedbackSpeed < 8.0F) {
|
||||||
SetProxyLocomotionState(a_actor.GetFormID(), -1.0F, a_direction);
|
const std::unique_lock lock{ g_proxyFormIdMutex };
|
||||||
|
g_proxyLocomotion[a_actor.GetFormID()] = ProxyLocomotionState{ -1.0F, a_direction, {}, {} };
|
||||||
SyncControllerMotionFields(a_actor, {}, 0.0F);
|
SyncControllerMotionFields(a_actor, {}, 0.0F);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetProxyLocomotionState(a_actor.GetFormID(), feedbackSpeed, a_direction);
|
{
|
||||||
|
const std::unique_lock lock{ g_proxyFormIdMutex };
|
||||||
|
g_proxyLocomotion[a_actor.GetFormID()] =
|
||||||
|
ProxyLocomotionState{ feedbackSpeed, a_direction, velocity, a_worldDelta };
|
||||||
|
}
|
||||||
SyncControllerMotionFields(a_actor, velocity, feedbackSpeed);
|
SyncControllerMotionFields(a_actor, velocity, feedbackSpeed);
|
||||||
TryPushMotionFeedbackToGraph(
|
TryPushMotionFeedbackToGraph(
|
||||||
a_actor,
|
a_actor,
|
||||||
|
|||||||
+17
-5
@@ -90,16 +90,23 @@ namespace
|
|||||||
float TryReadLocalAnimationGraphSpeed(RE::PlayerCharacter& a_player)
|
float TryReadLocalAnimationGraphSpeed(RE::PlayerCharacter& a_player)
|
||||||
{
|
{
|
||||||
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_player);
|
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_player);
|
||||||
|
|
||||||
|
auto tryRead = [&](const char* a_name) -> float {
|
||||||
float speed = -1.0F;
|
float speed = -1.0F;
|
||||||
if (graphHolder->GetGraphVariableImplFloat(
|
if (graphHolder->GetGraphVariableImplFloat(RE::BSFixedString{ a_name }, speed) &&
|
||||||
RE::BSFixedString{ F4T::ProxyAnimationSync::GraphVar::kSpeed },
|
|
||||||
speed) &&
|
|
||||||
std::isfinite(speed) &&
|
std::isfinite(speed) &&
|
||||||
speed >= 0.0F) {
|
speed > kMovingSpeedThreshold) {
|
||||||
return speed;
|
return speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1.0F;
|
return -1.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (const auto speed = tryRead(F4T::ProxyAnimationSync::GraphVar::kSpeed); speed >= 0.0F) {
|
||||||
|
return speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tryRead(F4T::ProxyAnimationSync::GraphVar::kSpeedSmoothed);
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerTransform GetPlayerTransform(const RE::PlayerCharacter& a_player)
|
PlayerTransform GetPlayerTransform(const RE::PlayerCharacter& a_player)
|
||||||
@@ -526,8 +533,13 @@ namespace
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto animationGraphSpeed =
|
auto animationGraphSpeed =
|
||||||
networkMovementState.isMoving ? TryReadLocalAnimationGraphSpeed(*player) : -1.0F;
|
networkMovementState.isMoving ? TryReadLocalAnimationGraphSpeed(*player) : -1.0F;
|
||||||
|
if (networkMovementState.isMoving && animationGraphSpeed < kMovingSpeedThreshold) {
|
||||||
|
// Graph Speed is frequently 0.0 on movement frames; send movementSpeed so receivers
|
||||||
|
// can blend slow-walk/jog/run instead of snapping to idle speed.
|
||||||
|
animationGraphSpeed = networkMovementState.movementSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
// Sender-side diagnostic: reveals, from whichever instance owns the log,
|
// Sender-side diagnostic: reveals, from whichever instance owns the log,
|
||||||
// what this local player computes and transmits while moving. Throttled.
|
// what this local player computes and transmits while moving. Throttled.
|
||||||
|
|||||||
Reference in New Issue
Block a user