Use authoritative FO4 animation variable names
Replace previously guessed graph variable names/types with the real FO4 humanoid movement behavior names (from MTBehavior.hkx) and update syncing logic accordingly. F4AnimationDescriptor now lists only the variables F4T drives (Speed, SpeedSmoothed, IsSprinting, bInJumpState, iIsInSneak) to avoid clobbering graph state; Direction is intentionally not written. Enable proxy sneak sync using the INT32 iIsInSneak, change SpeedSampled -> SpeedSmoothed, and update all Set/Get calls and diagnostic logging to the new names. Fix jump detection by relying on the actor IsJumping() API (removing the noisy vertical-speed heuristic that produced false positives and caused proxies to remain in jump state). Changes touch descriptor, proxy sync, puppet, main logic, and related docs to explain the rationale and testing notes.
This commit is contained in:
@@ -1,5 +1,18 @@
|
|||||||
# Animation Sync Analysis: Fallout 4 Together vs TiltedEvolution
|
# Animation Sync Analysis: Fallout 4 Together vs TiltedEvolution
|
||||||
|
|
||||||
|
> **Resolution update (2026-06-05):** The "variable names may be wrong" hypothesis below is
|
||||||
|
> now confirmed and fixed. The real FO4 humanoid graph variable names + types were extracted
|
||||||
|
> from `MTBehavior.hkx` (unpacked in the `F4-Animation-Research` repo) and applied to the code.
|
||||||
|
> Authoritative set actually driven by F4T:
|
||||||
|
> - `Speed`, `SpeedSmoothed` (float / `VARIABLE_TYPE_REAL`)
|
||||||
|
> - `IsSprinting`, `bInJumpState` (bool / `VARIABLE_TYPE_BOOL`)
|
||||||
|
> - `iIsInSneak` (int / `VARIABLE_TYPE_INT32`, 0 = standing, 1 = sneaking)
|
||||||
|
>
|
||||||
|
> Earlier guesses (`direction`, `isMoving`, `isSneaking`, `isCrouching`, `speedSampled`,
|
||||||
|
> `speedDamped`, `SpeedSampled`) do **not** exist on this graph and silently failed. `Direction`
|
||||||
|
> exists but is movement-relative-to-facing (not world heading), so it is intentionally left
|
||||||
|
> unwritten to avoid strafing artifacts. See `docs/dev-log.md` (2026-06-05) for the change set.
|
||||||
|
|
||||||
## Problem Statement
|
## Problem Statement
|
||||||
|
|
||||||
Your Fallout 4 Together animations aren't working on proxy actors. You have animation sync code in `F4TProxyAnimationSync.cpp` attempting to set graph variables by name (`Speed`, `IsSprinting`, etc.), but the variables aren't being applied or synced correctly.
|
Your Fallout 4 Together animations aren't working on proxy actors. You have animation sync code in `F4TProxyAnimationSync.cpp` attempting to set graph variables by name (`Speed`, `IsSprinting`, etc.), but the variables aren't being applied or synced correctly.
|
||||||
|
|||||||
+102
@@ -7,6 +7,108 @@ failed experiments, successful tests, and next steps.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-05 - Fix False Jump State Masking Locomotion Animation
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
After grounding the graph variable names (see entry below), an in-game log
|
||||||
|
(`Fallout4Together.log`) showed locomotion variables were now correct
|
||||||
|
(`graphSpeedReadBack=true(373.0)` while running), but proxies still never showed walk/run
|
||||||
|
animation. Root cause: the local player was reporting `isJumping=true` almost continuously
|
||||||
|
while merely running, so proxies were perpetually forced into the jump state, which masks
|
||||||
|
ground locomotion.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/main.cpp`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
- The log proved the graph layer is healthy: when moving, `recvMovementSpeed` ~545,
|
||||||
|
`computedGraphSpeed=373`, and `Speed`/`SpeedSmoothed` read back as 373 (engine not
|
||||||
|
clobbering them). `moveStart` is accepted on idle->move transitions.
|
||||||
|
- But `jumping=true` stuck on for 2.5s+ stretches during plain running. Source diagnostic:
|
||||||
|
`Local jump detection: apiJumping=false, verticalSpeed=2724.7, heldJumping=true`.
|
||||||
|
- `GetVerticalSpeed()` is `(z - prevZ) / dt`, which spikes to thousands of units/sec on flat
|
||||||
|
ground (tiny dt + z micro-jitter) - far above the 80 u/s `kJumpVerticalSpeedThreshold`,
|
||||||
|
so `derivedJumping` fired constantly. The real `IsJumping()` API was accurate (true only
|
||||||
|
on actual jumps).
|
||||||
|
- Fix: jump detection now relies solely on `a_player.IsJumping()`; removed the
|
||||||
|
vertical-speed-derived jump trigger. `verticalSpeed` is still computed and logged for
|
||||||
|
diagnostics. (`kJumpVerticalSpeedThreshold` is now unused.)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- `xmake build` succeeds with no new warnings.
|
||||||
|
- In-game verification pending: with false jumps gone, proxies should hold the locomotion
|
||||||
|
state and play walk/run while `Speed` drives the blend; `jumping=true` should now appear
|
||||||
|
only during real jumps.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- `kJumpVerticalSpeedThreshold` constant is now unused (left in place; harmless).
|
||||||
|
- Locomotion transition event names still unverified against behavior-graph `eventNames`.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Confirm walk/run now animates in-game and capture a fresh log.
|
||||||
|
- If a real jump is occasionally missed by `IsJumping()`, consider a *sane* vertical-speed
|
||||||
|
fallback (much higher threshold + clamp on dt) rather than the old raw heuristic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-05 - Animation Graph Variables Grounded in Real Behavior Data
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Replaced the guessed FO4 animation-graph variable names with the authoritative names and
|
||||||
|
types extracted from the real humanoid movement behavior graph
|
||||||
|
(`Meshes\Actors\Character\Behaviors\MTBehavior.hkx`), unpacked to XML in the companion
|
||||||
|
`F4-Animation-Research` repo. Almost every previous graph-variable name was wrong, so
|
||||||
|
`SetGraphVariable*` calls were silently failing and proxy animation state was never applied.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/include/F4TProxyAnimationSync.h`
|
||||||
|
- `plugin/src/F4TProxyAnimationSync.cpp`
|
||||||
|
- `plugin/src/F4AnimationDescriptor.cpp`
|
||||||
|
- `plugin/src/F4TProxyPuppet.cpp`
|
||||||
|
- `docs/animation-sync-analysis.md`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
- Ground truth comes from `MTBehavior.hkx` -> `hkbBehaviorGraphStringData::variableNames`
|
||||||
|
(67 entries) paired with the parallel `hkbBehaviorGraphData::variableInfos` types.
|
||||||
|
- Corrected names / types now used:
|
||||||
|
- `Speed`, `SpeedSmoothed` -> `VARIABLE_TYPE_REAL` (float). Previous code wrote
|
||||||
|
`speed`/`speedSampled`/`speedDamped`, none of which exist on this graph.
|
||||||
|
- `IsSprinting`, `bInJumpState` -> `VARIABLE_TYPE_BOOL` (bool).
|
||||||
|
- `iIsInSneak` -> `VARIABLE_TYPE_INT32` (int, 0 = standing, 1 = sneaking). Sneak was
|
||||||
|
previously modelled (incorrectly) as a bool `isSneaking`, so it never worked.
|
||||||
|
- `F4AnimationDescriptor` now lists ONLY the variables we actually drive. This matters
|
||||||
|
because `LoadAnimationVariablesToCache()` bulk-writes every descriptor variable from the
|
||||||
|
snapshot; listing extra variables would clobber graph state we do not intend to touch
|
||||||
|
(e.g. `IsPlayer`, `bAllowRotation`, `LookAt*`).
|
||||||
|
- Re-enabled `kEnableProxySneakSync` now that the correct int variable name is known.
|
||||||
|
- `Direction` is deliberately NOT written: the graph `Direction` is movement direction
|
||||||
|
relative to facing (radians), not the absolute world heading (`angleZ`) we carry. Writing
|
||||||
|
`angleZ` would make proxies strafe/moonwalk. Left at the graph default (forward) until a
|
||||||
|
relative movement direction is added to the protocol.
|
||||||
|
- Did NOT touch the network/JSON field names (`isMoving`, `isSprinting`, ...) — those are
|
||||||
|
protocol fields and stay as-is.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- `xmake build` succeeds (only pre-existing C4702 unreachable-code warnings in
|
||||||
|
`F4TProxyActorController.cpp`, unrelated to this change).
|
||||||
|
- In-game verification still required: confirm walk/run/sprint blend, sneak pose on proxies,
|
||||||
|
and watch the `Proxy anim diag` log line for `graphSpeedReadBack`/`speedSmoothedReadBack`
|
||||||
|
now reporting `true` (variables found) instead of failing silently.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- Locomotion transition EVENT names (`moveStart`, `SprintStart`, jump events, sneak events)
|
||||||
|
are unchanged and still empirically chosen; they are a separate axis from variables and
|
||||||
|
should be cross-checked against the behavior graph `eventNames` next.
|
||||||
|
- Weapon-drawn sync remains disabled; that state lives in the weapon wrapping behavior, not
|
||||||
|
`MTBehavior`, and needs its own research pass.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Verify in-game and capture a fresh `Fallout4Together.log` diagnostic.
|
||||||
|
- Cross-check the transition event names against the behavior graph `eventNames` arrays.
|
||||||
|
- If sneak works, investigate the weapon wrapping behavior for the weapon-drawn variable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-06-05 - Jump Animation Fix (Correct Event Names)
|
## 2026-06-05 - Jump Animation Fix (Correct Event Names)
|
||||||
|
|
||||||
### Issues Resolved
|
### Issues Resolved
|
||||||
|
|||||||
@@ -14,18 +14,22 @@ namespace RE
|
|||||||
|
|
||||||
namespace F4T::ProxyAnimationSync
|
namespace F4T::ProxyAnimationSync
|
||||||
{
|
{
|
||||||
// Confirmed via local read-by-name graph debug (Fallout4Together.log):
|
// Authoritative FO4 humanoid movement-behavior graph variable names, taken from
|
||||||
// - float "Speed" reads/writes while in-game (walk ~47-105)
|
// MTBehavior.hkx (hkbBehaviorGraphStringData::variableNames, with types from the
|
||||||
// - bool "IsSprinting" reads while in-game
|
// parallel variableInfos array) via the F4-Animation-Research unpack:
|
||||||
// - bIsMoving, bIsSneaking, bSprint, bInJumpState, bWeaponDrawn, Direction: unavailable by name
|
// Speed, SpeedSmoothed, Direction -> VARIABLE_TYPE_REAL (float)
|
||||||
|
// IsSprinting, bInJumpState -> VARIABLE_TYPE_BOOL (bool)
|
||||||
|
// iIsInSneak -> VARIABLE_TYPE_INT32 (int)
|
||||||
|
// Earlier code guessed names like "speed"/"speedSampled"/"direction" that do not exist
|
||||||
|
// on this graph, so every SetGraphVariable* call silently failed.
|
||||||
namespace GraphVar
|
namespace GraphVar
|
||||||
{
|
{
|
||||||
inline constexpr const char* kSpeed = "Speed";
|
inline constexpr const char* kSpeed = "Speed";
|
||||||
inline constexpr const char* kSpeedLower = "speed";
|
inline constexpr const char* kSpeedSmoothed = "SpeedSmoothed";
|
||||||
inline constexpr const char* kSpeedSampled = "speedSampled";
|
inline constexpr const char* kDirection = "Direction";
|
||||||
inline constexpr const char* kSpeedDamped = "speedDamped";
|
|
||||||
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";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps network movementSpeed (units/sec) to graph Speed (~0-105). Used when the sender
|
// Maps network movementSpeed (units/sec) to graph Speed (~0-105). Used when the sender
|
||||||
|
|||||||
@@ -43,35 +43,31 @@ namespace F4T::AnimationDescriptor
|
|||||||
// Humanoid Master Behavior Graph Variables (FO4)
|
// Humanoid Master Behavior Graph Variables (FO4)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
//
|
//
|
||||||
// These indices are based on comparison with TiltedEvolution's Skyrim SE model
|
// These names and types are taken from the authoritative FO4 humanoid movement
|
||||||
// and FO4's shared Creation Engine foundation. Variable names and indices should
|
// behavior graph (Meshes\Actors\Character\Behaviors\MTBehavior.hkx), unpacked to
|
||||||
// match the humanoid master behavior graph structure in FO4.
|
// XML in the F4-Animation-Research repo. They are the real
|
||||||
|
// hkbBehaviorGraphStringData::variableNames, paired with the parallel
|
||||||
|
// variableInfos types - NOT guesses.
|
||||||
//
|
//
|
||||||
// Note: Exact indices need verification via debug logging (Phase 1.1).
|
// Speed, SpeedSmoothed -> VARIABLE_TYPE_REAL (float)
|
||||||
// For now, using names that match common Creation Engine patterns.
|
// IsSprinting, bInJumpState -> VARIABLE_TYPE_BOOL (bool)
|
||||||
|
// iIsInSneak -> VARIABLE_TYPE_INT32 (int, 0 = standing, 1 = sneaking)
|
||||||
|
//
|
||||||
|
// IMPORTANT: only list variables F4T actually drives here. LoadAnimationVariablesToCache()
|
||||||
|
// bulk-writes every variable in the descriptor from the snapshot, so any variable listed
|
||||||
|
// but not explicitly set would be written as 0/false each update and clobber graph state
|
||||||
|
// we do not intend to touch (e.g. IsPlayer, bAllowRotation, LookAt* settings).
|
||||||
|
//
|
||||||
|
// The previous table guessed names ("direction", "isMoving", "isSneaking", "speedSampled",
|
||||||
|
// "speedDamped", ...) that do not exist on this graph, so every SetGraphVariable* call
|
||||||
|
// silently failed and no animation state was ever applied.
|
||||||
//
|
//
|
||||||
void F4AnimationDescriptor::InitializeHumanoidVariables()
|
void F4AnimationDescriptor::InitializeHumanoidVariables()
|
||||||
{
|
{
|
||||||
// ===== FLOAT VARIABLES (Locomotion & Direction) =====
|
// ===== FLOAT VARIABLES (VARIABLE_TYPE_REAL) =====
|
||||||
// Index these in order; the index becomes the cache access offset
|
|
||||||
|
|
||||||
m_floatVariables = {
|
m_floatVariables = {
|
||||||
// Locomotion core
|
"Speed", // 0 - primary locomotion speed (world units/sec)
|
||||||
"Speed", // 0 - Primary locomotion speed (0-105)
|
"SpeedSmoothed", // 1 - engine-damped speed; mirror Speed so it tracks the target
|
||||||
"direction", // 1 - Facing direction relative to movement
|
|
||||||
"speedDamped", // 2 - Damped speed for smoothing
|
|
||||||
"speedSampled", // 3 - Sampled speed
|
|
||||||
|
|
||||||
// Aiming & weapon
|
|
||||||
"pitchGunAim", // 4 - Pitch angle when aiming
|
|
||||||
"weaponAdjust", // 5 - Weapon positioning offset
|
|
||||||
|
|
||||||
// Physics
|
|
||||||
"velocityZ", // 6 - Vertical velocity (Havok-driven)
|
|
||||||
|
|
||||||
// Thresholds & parameters
|
|
||||||
"speedWalk", // 7 - Walk speed threshold
|
|
||||||
"speedRun", // 8 - Run speed threshold
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build reverse lookup map for float variables
|
// Build reverse lookup map for float variables
|
||||||
@@ -79,34 +75,20 @@ namespace F4T::AnimationDescriptor
|
|||||||
m_floatVariableIndexMap[m_floatVariables[i]] = i;
|
m_floatVariableIndexMap[m_floatVariables[i]] = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== BOOL VARIABLES (State Flags) =====
|
// ===== BOOL VARIABLES (VARIABLE_TYPE_BOOL) =====
|
||||||
m_boolVariables = {
|
m_boolVariables = {
|
||||||
// Movement state
|
"IsSprinting", // 0 - sprint sub-state
|
||||||
"isSprinting", // 0 - Sprint mode active
|
"bInJumpState", // 1 - jump / takeoff state
|
||||||
"isSneaking", // 1 - Sneak/crouch mode
|
};
|
||||||
"isMoving", // 2 - Any movement active
|
|
||||||
"isJumping", // 3 - Jump in progress
|
|
||||||
"isCrouching", // 4 - Crouch mode active
|
|
||||||
|
|
||||||
// Engine state
|
|
||||||
"bMotionDriven", // 5 - Motion-driven locomotion
|
|
||||||
"bInMoveState", // 6 - In locomotion state machine
|
|
||||||
"bSprintOK", // 7 - Sprint available (game state dependent)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build reverse lookup map for bool variables
|
// Build reverse lookup map for bool variables
|
||||||
for (std::size_t i = 0; i < m_boolVariables.size(); ++i) {
|
for (std::size_t i = 0; i < m_boolVariables.size(); ++i) {
|
||||||
m_boolVariableIndexMap[m_boolVariables[i]] = i;
|
m_boolVariableIndexMap[m_boolVariables[i]] = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== INT VARIABLES (Equipment & State) =====
|
// ===== INT VARIABLES (VARIABLE_TYPE_INT32) =====
|
||||||
m_intVariables = {
|
m_intVariables = {
|
||||||
// Equipment
|
"iIsInSneak", // 0 - sneak/crouch state (0 = standing, 1 = sneaking)
|
||||||
"iLeftHandType", // 0 - Left hand weapon/item type
|
|
||||||
"iRightHandEquipped", // 1 - Right hand equipped (0/1)
|
|
||||||
|
|
||||||
// State
|
|
||||||
"iIsInSneak", // 2 - Sneak state (0/1/2)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build reverse lookup map for int variables
|
// Build reverse lookup map for int variables
|
||||||
|
|||||||
@@ -30,10 +30,14 @@ namespace
|
|||||||
// The old reason for disabling (suppressed AI) no longer applies.
|
// The old reason for disabling (suppressed AI) no longer applies.
|
||||||
constexpr bool kEnableProxyVisualAnimationSync = true;
|
constexpr bool kEnableProxyVisualAnimationSync = true;
|
||||||
constexpr bool kEnableProxyLocomotionSync = true;
|
constexpr bool kEnableProxyLocomotionSync = true;
|
||||||
// Disabled until read-by-name debug finds working bool variable names on this graph.
|
// Enabled now that the authoritative FO4 graph name is known: sneak is the INT32
|
||||||
constexpr bool kEnableProxySneakSync = false;
|
// variable "iIsInSneak" (0 = standing, 1 = sneaking), not a bool. See MTBehavior.hkx
|
||||||
|
// in the F4-Animation-Research unpack.
|
||||||
|
constexpr bool kEnableProxySneakSync = true;
|
||||||
constexpr bool kEnableProxyJumpSync = true;
|
constexpr bool kEnableProxyJumpSync = true;
|
||||||
|
// 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.
|
||||||
constexpr bool kEnableProxyWeaponDrawnSync = false;
|
constexpr bool kEnableProxyWeaponDrawnSync = false;
|
||||||
|
|
||||||
constexpr float kIdleSpeedThreshold = 1.0F;
|
constexpr float kIdleSpeedThreshold = 1.0F;
|
||||||
@@ -181,14 +185,9 @@ namespace
|
|||||||
a_snapshot.floats[speedIdx] = graphSpeed;
|
a_snapshot.floats[speedIdx] = graphSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto speedSampledIdx = a_descriptor.GetFloatVariableIndex(F4T::ProxyAnimationSync::GraphVar::kSpeedSampled);
|
const auto speedSmoothedIdx = a_descriptor.GetFloatVariableIndex(F4T::ProxyAnimationSync::GraphVar::kSpeedSmoothed);
|
||||||
if (speedSampledIdx != static_cast<std::size_t>(-1) && speedSampledIdx < a_snapshot.floats.size()) {
|
if (speedSmoothedIdx != static_cast<std::size_t>(-1) && speedSmoothedIdx < a_snapshot.floats.size()) {
|
||||||
a_snapshot.floats[speedSampledIdx] = graphSpeed;
|
a_snapshot.floats[speedSmoothedIdx] = graphSpeed;
|
||||||
}
|
|
||||||
|
|
||||||
const auto speedDampedIdx = a_descriptor.GetFloatVariableIndex(F4T::ProxyAnimationSync::GraphVar::kSpeedDamped);
|
|
||||||
if (speedDampedIdx != static_cast<std::size_t>(-1) && speedDampedIdx < a_snapshot.floats.size()) {
|
|
||||||
a_snapshot.floats[speedDampedIdx] = graphSpeed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,18 +315,11 @@ namespace
|
|||||||
if constexpr (kEnableProxyLocomotionSync) {
|
if constexpr (kEnableProxyLocomotionSync) {
|
||||||
const auto graphSpeed = ClampGraphSpeed(a_desired.graphSpeed);
|
const auto graphSpeed = ClampGraphSpeed(a_desired.graphSpeed);
|
||||||
const auto speedWritten = TrySetGraphFloat(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kSpeed, graphSpeed);
|
const auto speedWritten = TrySetGraphFloat(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kSpeed, graphSpeed);
|
||||||
const auto speedLowerWritten = TrySetGraphFloat(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kSpeedLower, graphSpeed);
|
const auto speedSmoothedWritten =
|
||||||
const auto speedSampledWritten =
|
TrySetGraphFloat(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kSpeedSmoothed, graphSpeed);
|
||||||
TrySetGraphFloat(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kSpeedSampled, graphSpeed);
|
const auto sprintWritten =
|
||||||
const auto speedDampedWritten =
|
TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kIsSprinting, a_desired.isSprinting);
|
||||||
TrySetGraphFloat(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kSpeedDamped, graphSpeed);
|
success &= speedWritten && speedSmoothedWritten && sprintWritten;
|
||||||
bool sprintWritten = false;
|
|
||||||
if (a_desired.isSprinting) {
|
|
||||||
sprintWritten = TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kIsSprinting, true);
|
|
||||||
} else {
|
|
||||||
sprintWritten = TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kIsSprinting, false);
|
|
||||||
}
|
|
||||||
success &= speedWritten && speedLowerWritten && speedSampledWritten && speedDampedWritten && sprintWritten;
|
|
||||||
|
|
||||||
if (a_desired.isMoving) {
|
if (a_desired.isMoving) {
|
||||||
float readBackSpeed = 0.0F;
|
float readBackSpeed = 0.0F;
|
||||||
@@ -382,48 +374,33 @@ namespace
|
|||||||
|
|
||||||
PopulateLocomotionSpeedSnapshot(snapshot, descriptor, a_desired.graphSpeed);
|
PopulateLocomotionSpeedSnapshot(snapshot, descriptor, a_desired.graphSpeed);
|
||||||
|
|
||||||
const auto directionIdx = descriptor.GetFloatVariableIndex("direction");
|
// NOTE: the FO4 graph "Direction" variable is the movement direction relative to
|
||||||
if (directionIdx != static_cast<std::size_t>(-1)) {
|
// facing (radians), not the world heading we carry in angleZ. Writing angleZ here
|
||||||
snapshot.floats[directionIdx] = a_desired.direction;
|
// makes the proxy strafe/moonwalk, so Direction is intentionally left at its graph
|
||||||
|
// default (forward) until a relative movement direction is added to the protocol.
|
||||||
|
|
||||||
|
const auto isSprintingIdx = descriptor.GetBoolVariableIndex(F4T::ProxyAnimationSync::GraphVar::kIsSprinting);
|
||||||
|
if (isSprintingIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[isSprintingIdx] = a_desired.isSprinting;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate bool variables
|
if constexpr (kEnableProxyJumpSync) {
|
||||||
const auto isSprintingIdx = descriptor.GetBoolVariableIndex("isSprinting");
|
const auto inJumpIdx = descriptor.GetBoolVariableIndex(F4T::ProxyAnimationSync::GraphVar::kInJumpState);
|
||||||
if (isSprintingIdx != static_cast<std::size_t>(-1)) {
|
if (inJumpIdx != static_cast<std::size_t>(-1)) {
|
||||||
snapshot.bools[isSprintingIdx] = a_desired.isSprinting;
|
snapshot.bools[inJumpIdx] = a_desired.isJumping;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto isSneakingIdx = descriptor.GetBoolVariableIndex("isSneaking");
|
|
||||||
if (isSneakingIdx != static_cast<std::size_t>(-1)) {
|
|
||||||
snapshot.bools[isSneakingIdx] = a_desired.isSneaking;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto isMovingIdx = descriptor.GetBoolVariableIndex("isMoving");
|
|
||||||
if (isMovingIdx != static_cast<std::size_t>(-1)) {
|
|
||||||
snapshot.bools[isMovingIdx] = a_desired.isMoving;
|
|
||||||
}
|
|
||||||
|
|
||||||
if constexpr (kEnableProxyJumpSync) {
|
|
||||||
const auto isJumpingIdx = descriptor.GetBoolVariableIndex("isJumping");
|
|
||||||
if (isJumpingIdx != static_cast<std::size_t>(-1)) {
|
|
||||||
snapshot.bools[isJumpingIdx] = a_desired.isJumping;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if constexpr (kEnableProxyCrouchSync) {
|
if constexpr (kEnableProxySneakSync) {
|
||||||
const auto isCrouchingIdx = descriptor.GetBoolVariableIndex("isCrouching");
|
const auto sneakIdx = descriptor.GetIntVariableIndex(F4T::ProxyAnimationSync::GraphVar::kIsInSneak);
|
||||||
if (isCrouchingIdx != static_cast<std::size_t>(-1)) {
|
if (sneakIdx != static_cast<std::size_t>(-1)) {
|
||||||
snapshot.bools[isCrouchingIdx] = a_desired.isCrouching;
|
snapshot.ints[sneakIdx] = a_desired.isSneaking ? 1U : 0U;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
if constexpr (kEnableProxyJumpSync) {
|
|
||||||
TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kInJumpState, a_desired.isJumping);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
static std::unordered_set<std::uint32_t> loggedWriteFailures;
|
static std::unordered_set<std::uint32_t> loggedWriteFailures;
|
||||||
if (loggedWriteFailures.insert(a_remotePlayerId).second) {
|
if (loggedWriteFailures.insert(a_remotePlayerId).second) {
|
||||||
@@ -578,13 +555,13 @@ namespace F4T::ProxyAnimationSync
|
|||||||
last = now;
|
last = now;
|
||||||
auto* diagHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
auto* diagHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||||
float readSpeed = -1.0F;
|
float readSpeed = -1.0F;
|
||||||
float readSpeedSampled = -1.0F;
|
float readSpeedSmoothed = -1.0F;
|
||||||
const auto haveSpeed =
|
const auto haveSpeed =
|
||||||
diagHolder->GetGraphVariableImplFloat(RE::BSFixedString{ "Speed" }, readSpeed);
|
diagHolder->GetGraphVariableImplFloat(RE::BSFixedString{ F4T::ProxyAnimationSync::GraphVar::kSpeed }, readSpeed);
|
||||||
const auto haveSpeedSampled =
|
const auto haveSpeedSmoothed =
|
||||||
diagHolder->GetGraphVariableImplFloat(RE::BSFixedString{ "speedSampled" }, readSpeedSampled);
|
diagHolder->GetGraphVariableImplFloat(RE::BSFixedString{ F4T::ProxyAnimationSync::GraphVar::kSpeedSmoothed }, readSpeedSmoothed);
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Proxy anim diag p{} actor={:08X}: recvMoving={}, recvSprint={}, recvMovementSpeed={:.1f}, recvAnimGraphSpeed={:.1f}, computedGraphSpeed={:.1f}, graphSpeedReadBack={}({:.1f}), speedSampledReadBack={}({:.1f}).",
|
"Proxy anim diag p{} actor={:08X}: recvMoving={}, recvSprint={}, recvMovementSpeed={:.1f}, recvAnimGraphSpeed={:.1f}, computedGraphSpeed={:.1f}, graphSpeedReadBack={}({:.1f}), speedSmoothedReadBack={}({:.1f}).",
|
||||||
a_remotePlayer.playerId,
|
a_remotePlayer.playerId,
|
||||||
actorId,
|
actorId,
|
||||||
a_remotePlayer.isMoving,
|
a_remotePlayer.isMoving,
|
||||||
@@ -594,8 +571,8 @@ namespace F4T::ProxyAnimationSync
|
|||||||
desired.graphSpeed,
|
desired.graphSpeed,
|
||||||
haveSpeed,
|
haveSpeed,
|
||||||
readSpeed,
|
readSpeed,
|
||||||
haveSpeedSampled,
|
haveSpeedSmoothed,
|
||||||
readSpeedSampled));
|
readSpeedSmoothed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,37 +666,29 @@ namespace F4T::ProxyAnimationSync
|
|||||||
const auto desired = BuildDesiredState(a_remotePlayer);
|
const auto desired = BuildDesiredState(a_remotePlayer);
|
||||||
PopulateLocomotionSpeedSnapshot(snapshot, descriptor, desired.graphSpeed);
|
PopulateLocomotionSpeedSnapshot(snapshot, descriptor, desired.graphSpeed);
|
||||||
|
|
||||||
// Direction: from remote player's heading
|
// Direction intentionally not written; see ApplyDesiredStateToGraphDescriptorBased.
|
||||||
if (descriptor.GetFloatVariableIndex("direction") != static_cast<std::size_t>(-1)) {
|
|
||||||
snapshot.floats[descriptor.GetFloatVariableIndex("direction")] = desired.direction;
|
// Prepare bool variables (IsSprinting, bInJumpState)
|
||||||
|
snapshot.bools.resize(descriptor.GetBoolVariableCount(), false);
|
||||||
|
|
||||||
|
const auto isSprintingIdx = descriptor.GetBoolVariableIndex(GraphVar::kIsSprinting);
|
||||||
|
if (isSprintingIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[isSprintingIdx] = desired.isSprinting;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare bool variables (isSprinting, isSneaking, isMoving, isJumping, isCrouching)
|
const auto inJumpIdx = descriptor.GetBoolVariableIndex(GraphVar::kInJumpState);
|
||||||
snapshot.bools.resize(descriptor.GetBoolVariableCount(), false);
|
if (inJumpIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[inJumpIdx] = desired.isJumping;
|
||||||
|
}
|
||||||
|
|
||||||
if (descriptor.GetBoolVariableIndex("isSprinting") != static_cast<std::size_t>(-1)) {
|
// Prepare int variables (iIsInSneak)
|
||||||
snapshot.bools[descriptor.GetBoolVariableIndex("isSprinting")] = desired.isSprinting;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptor.GetBoolVariableIndex("isSneaking") != static_cast<std::size_t>(-1)) {
|
|
||||||
snapshot.bools[descriptor.GetBoolVariableIndex("isSneaking")] = desired.isSneaking;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptor.GetBoolVariableIndex("isMoving") != static_cast<std::size_t>(-1)) {
|
|
||||||
snapshot.bools[descriptor.GetBoolVariableIndex("isMoving")] = desired.isMoving;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptor.GetBoolVariableIndex("isJumping") != static_cast<std::size_t>(-1)) {
|
|
||||||
snapshot.bools[descriptor.GetBoolVariableIndex("isJumping")] = desired.isJumping;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptor.GetBoolVariableIndex("isCrouching") != static_cast<std::size_t>(-1)) {
|
|
||||||
snapshot.bools[descriptor.GetBoolVariableIndex("isCrouching")] = desired.isCrouching;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare int variables (equipment, etc.)
|
|
||||||
snapshot.ints.resize(descriptor.GetIntVariableCount(), 0);
|
snapshot.ints.resize(descriptor.GetIntVariableCount(), 0);
|
||||||
|
|
||||||
|
const auto sneakIdx = descriptor.GetIntVariableIndex(GraphVar::kIsInSneak);
|
||||||
|
if (sneakIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.ints[sneakIdx] = desired.isSneaking ? 1U : 0U;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply all variables at once via descriptor bulk write
|
// Apply all variables at once via descriptor bulk write
|
||||||
if (!descriptor.LoadAnimationVariablesToCache(
|
if (!descriptor.LoadAnimationVariablesToCache(
|
||||||
*static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy),
|
*static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy),
|
||||||
|
|||||||
@@ -66,8 +66,10 @@ namespace F4T::ProxyPuppet
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(a_this);
|
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(a_this);
|
||||||
|
// "Speed" and "SpeedSmoothed" are the real FO4 humanoid graph float variables
|
||||||
|
// (MTBehavior.hkx). The previous "SpeedSampled" name does not exist on this graph.
|
||||||
graphHolder->SetGraphVariableFloat(RE::BSFixedString{ "Speed" }, a_speed);
|
graphHolder->SetGraphVariableFloat(RE::BSFixedString{ "Speed" }, a_speed);
|
||||||
graphHolder->SetGraphVariableFloat(RE::BSFixedString{ "SpeedSampled" }, a_speed);
|
graphHolder->SetGraphVariableFloat(RE::BSFixedString{ "SpeedSmoothed" }, a_speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SyncControllerMotionFields(RE::Actor& a_actor, const RE::NiPoint3& a_velocity, float a_speed)
|
void SyncControllerMotionFields(RE::Actor& a_actor, const RE::NiPoint3& a_velocity, float a_speed)
|
||||||
|
|||||||
+6
-2
@@ -296,9 +296,13 @@ namespace
|
|||||||
const auto derivedMoving = a_movementSpeed >= kMovingSpeedThreshold;
|
const auto derivedMoving = a_movementSpeed >= kMovingSpeedThreshold;
|
||||||
const auto isMoving = ResolveMovementHoldState(derivedMoving, a_currentTime, a_movingHoldUntil);
|
const auto isMoving = ResolveMovementHoldState(derivedMoving, a_currentTime, a_movingHoldUntil);
|
||||||
const auto sneakDetection = GetSneakDetectionState(a_player);
|
const auto sneakDetection = GetSneakDetectionState(a_player);
|
||||||
|
// Jump detection relies solely on the IsJumping() API. The previous
|
||||||
|
// vertical-speed heuristic (a_verticalSpeed >= kJumpVerticalSpeedThreshold) produced
|
||||||
|
// constant false positives: (z - prevZ)/dt spikes to thousands of units/sec on flat
|
||||||
|
// ground due to tiny dt and z micro-jitter, so proxies were stuck in a perpetual jump
|
||||||
|
// state that masked the run/walk locomotion animation (see Fallout4Together.log).
|
||||||
const auto apiJumping = a_player.IsJumping();
|
const auto apiJumping = a_player.IsJumping();
|
||||||
const auto derivedJumping = a_verticalSpeed >= kJumpVerticalSpeedThreshold;
|
if (apiJumping) {
|
||||||
if (apiJumping || derivedJumping) {
|
|
||||||
jumpHoldUntil = a_currentTime + kJumpStateHoldDuration;
|
jumpHoldUntil = a_currentTime + kJumpStateHoldDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user