Improve proxy jump sync and event handling

Handle remote jump state more robustly and avoid locomotion graph clobbering. Changes: detect jumping early in ActorController and short-circuit to apply jump animation/state; suppress locomotion (Speed/Sprint) while airborne and keep graphSpeed for landing-tier selection; add ApplyJumpStateToGraph helper to set jump vars; only write bulk descriptor fields when not airborne to avoid stomping jump graph; change snapshot writes so in-jump vars are not overwritten by descriptor bulk writes; reorder and refine jump/movement event logic so jump start/up/down/land are handled before locomotion tier events and preserve the pre-jump locomotion tier for landing transitions; ensure graph updates are applied while jumping; set puppet locomotion speed to -1 when jumping or idle so the engine's idle speed remains; and increase the network jump hold duration from 200ms to 400ms. These changes prevent missed/incorrect jump animations and improve landing clip selection.
This commit is contained in:
2026-06-05 15:59:35 +12:00
parent f4b37b6d12
commit 9412fcf8ea
3 changed files with 112 additions and 57 deletions
+11
View File
@@ -5332,6 +5332,17 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
} }
auto effectiveRemote = *a_slot.lastRemotePlayerSnapshot; auto effectiveRemote = *a_slot.lastRemotePlayerSnapshot;
if (effectiveRemote.isJumping) {
a_slot.puppetGraphSpeed = -1.0F;
F4T::ProxyPuppet::SetProxyLocomotionState(a_proxy.GetFormID(), -1.0F, a_slot.puppetDirection);
F4T::ProxyAnimationSync::ApplyProxyAnimationFromRemoteState(
a_slot.appliedAnimationState,
a_proxy,
effectiveRemote);
return;
}
const bool remoteLocomoting = const bool remoteLocomoting =
effectiveRemote.isMoving && effectiveRemote.movementSpeed >= 1.0F; effectiveRemote.isMoving && effectiveRemote.movementSpeed >= 1.0F;
+100 -56
View File
@@ -282,9 +282,13 @@ namespace
} }
// 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);
// Jump state follows the network value directly. The sender already holds isJumping
// briefly for packet coalescing; an extra proxy-side hold delayed landing events and // While airborne, suppress locomotion so per-frame Speed/Jog events do not override
// kept bInJumpState true after ground landings. // the jump graph. Keep graphSpeed for landing-tier selection on jump end.
if (desired.isJumping) {
desired.isMoving = false;
desired.isSprinting = false;
}
if (!desired.isMoving) { if (!desired.isMoving) {
desired.isSprinting = false; desired.isSprinting = false;
@@ -373,6 +377,21 @@ namespace
return success; return success;
} }
bool ApplyJumpStateToGraph(
RE::IAnimationGraphManagerHolder& a_holder,
bool a_isJumping)
{
auto success = true;
success &= TrySetGraphBool(a_holder, F4T::ProxyAnimationSync::GraphVar::kInJumpState, a_isJumping);
const RE::BSFixedString syncJumpName{ F4T::ProxyAnimationSync::GraphVar::kSyncJumpState };
success &= a_holder.SetGraphVariableInt(
syncJumpName,
a_isJumping ? 1 : 0);
return success;
}
bool ApplyDesiredStateToGraphDescriptorBased( bool ApplyDesiredStateToGraphDescriptorBased(
RE::Actor& a_proxy, RE::Actor& a_proxy,
const DesiredProxyAnimationState& a_desired, const DesiredProxyAnimationState& a_desired,
@@ -381,6 +400,13 @@ namespace
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy); auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
const auto actorId = a_proxy.GetFormID(); const auto actorId = a_proxy.GetFormID();
// Bulk descriptor writes initialize unset fields to 0/false. During jumps that
// clobbered Speed and locomotion tier every frame, forcing the graph back into the
// walk/jog loop and killing takeoff. Only touch jump variables while airborne.
if (a_desired.isJumping) {
return ApplyJumpStateToGraph(*graphHolder, true);
}
auto& descriptor = F4T::AnimationDescriptor::F4AnimationDescriptor::GetHumanoidDescriptor(); auto& descriptor = F4T::AnimationDescriptor::F4AnimationDescriptor::GetHumanoidDescriptor();
F4T::AnimationDescriptor::AnimationVariableSnapshot snapshot; F4T::AnimationDescriptor::AnimationVariableSnapshot snapshot;
@@ -391,25 +417,25 @@ namespace
PopulateLocomotionSpeedSnapshot(snapshot, descriptor, a_desired.graphSpeed); PopulateLocomotionSpeedSnapshot(snapshot, descriptor, a_desired.graphSpeed);
// NOTE: the FO4 graph "Direction" variable is the movement direction relative to
// facing (radians), not the world heading we carry in angleZ. Writing angleZ here
// 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); const auto isSprintingIdx = descriptor.GetBoolVariableIndex(F4T::ProxyAnimationSync::GraphVar::kIsSprinting);
if (isSprintingIdx != static_cast<std::size_t>(-1)) { if (isSprintingIdx != static_cast<std::size_t>(-1)) {
snapshot.bools[isSprintingIdx] = a_desired.isSprinting; snapshot.bools[isSprintingIdx] = a_desired.isSprinting;
} }
// NOTE: the FO4 graph "Direction" variable is the movement direction relative to
// facing (radians), not the world heading we carry in angleZ. Writing angleZ here
// 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.
if constexpr (kEnableProxyJumpSync) { if constexpr (kEnableProxyJumpSync) {
const auto inJumpIdx = descriptor.GetBoolVariableIndex(F4T::ProxyAnimationSync::GraphVar::kInJumpState); const auto inJumpIdx = descriptor.GetBoolVariableIndex(F4T::ProxyAnimationSync::GraphVar::kInJumpState);
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] = false;
} }
const auto syncJumpIdx = descriptor.GetIntVariableIndex(F4T::ProxyAnimationSync::GraphVar::kSyncJumpState); const auto syncJumpIdx = descriptor.GetIntVariableIndex(F4T::ProxyAnimationSync::GraphVar::kSyncJumpState);
if (syncJumpIdx != static_cast<std::size_t>(-1)) { if (syncJumpIdx != static_cast<std::size_t>(-1)) {
snapshot.ints[syncJumpIdx] = a_desired.isJumping ? 1U : 0U; snapshot.ints[syncJumpIdx] = 0U;
} }
} }
@@ -481,70 +507,86 @@ 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);
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).
if (wasInitialized && a_previous.lastIsSprinting != a_desired.isSprinting) {
fireEvent(a_desired.isSprinting ? GraphEvent::kSprintStart : GraphEvent::kSprintStop);
}
if constexpr (kEnableProxySneakSync) {
if (!wasInitialized || a_previous.lastIsSneaking != a_desired.isSneaking) {
fireEvent(a_desired.isSneaking ? GraphEvent::kSneakStart : GraphEvent::kSneakStop);
}
}
if constexpr (kEnableProxyJumpSync) { if constexpr (kEnableProxyJumpSync) {
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy); 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); const bool wasLocomoting =
switch (jumpTier) { a_previous.lastIsMoving || a_previous.lastLocomotionTier.has_value();
case F4T::ProxyAnimationSync::LocomotionTier::kRun: if (wasLocomoting) {
fireEvent(GraphEvent::kJumpStartFromRun); const auto jumpTier = a_previous.lastLocomotionTier.value_or(currentTier);
break; switch (jumpTier) {
case F4T::ProxyAnimationSync::LocomotionTier::kWalk: case F4T::ProxyAnimationSync::LocomotionTier::kRun:
fireEvent(GraphEvent::kJumpStartFromWalk); fireEvent(GraphEvent::kJumpStartFromRun);
break; break;
default: case F4T::ProxyAnimationSync::LocomotionTier::kWalk:
fireEvent(GraphEvent::kJumpStartFromWalk);
break;
default:
fireEvent(GraphEvent::kJumpStart);
break;
}
} else {
fireEvent(GraphEvent::kJumpStart); fireEvent(GraphEvent::kJumpStart);
break;
} }
fireEvent(GraphEvent::kJumpUp); fireEvent(GraphEvent::kJumpUp);
TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kInJumpState, true); ApplyJumpStateToGraph(*graphHolder, true);
} else if (wasInitialized) { } else if (wasInitialized) {
const auto landTier = a_previous.lastLocomotionTier.value_or(currentTier);
fireEvent(GraphEvent::kJumpDown); fireEvent(GraphEvent::kJumpDown);
fireEvent(GraphEvent::kJumpLand); fireEvent(GraphEvent::kJumpLand);
if (a_desired.isMoving) { const auto remoteMoving =
if (currentTier == F4T::ProxyAnimationSync::LocomotionTier::kRun) { a_desired.isMoving || a_desired.graphSpeed >= kIdleSpeedThreshold;
if (remoteMoving) {
if (landTier == F4T::ProxyAnimationSync::LocomotionTier::kRun) {
fireEvent(GraphEvent::kJumpLandToRun); fireEvent(GraphEvent::kJumpLandToRun);
} else { } else {
fireEvent(GraphEvent::kJumpLandToWalk); fireEvent(GraphEvent::kJumpLandToWalk);
} }
fireEvent(GraphEvent::kMoveStart); fireEvent(GraphEvent::kMoveStart);
fireEvent(GetLocomotionTierEventName(currentTier)); fireEvent(GetLocomotionTierEventName(landTier));
} else { } else {
fireEvent(GraphEvent::kJumpEnd); fireEvent(GraphEvent::kJumpEnd);
fireEvent(GraphEvent::kMoveStop); fireEvent(GraphEvent::kMoveStop);
} }
TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kInJumpState, false); ApplyJumpStateToGraph(*graphHolder, false);
} }
} }
} }
// Locomotion tier events after jump handling. Skip entirely while airborne.
const bool justLanded = wasInitialized && a_previous.lastIsJumping && !a_desired.isJumping;
if (!a_desired.isJumping) {
const bool startedMoving =
!justLanded &&
a_desired.isMoving && (!wasInitialized || !a_previous.lastIsMoving);
const bool stoppedMoving =
wasInitialized && a_previous.lastIsMoving && !a_desired.isMoving && !a_previous.lastIsJumping;
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));
}
if (wasInitialized && a_previous.lastIsSprinting != a_desired.isSprinting) {
fireEvent(a_desired.isSprinting ? GraphEvent::kSprintStart : GraphEvent::kSprintStop);
}
}
if constexpr (kEnableProxySneakSync) {
if (!wasInitialized || a_previous.lastIsSneaking != a_desired.isSneaking) {
fireEvent(a_desired.isSneaking ? GraphEvent::kSneakStart : GraphEvent::kSneakStop);
}
}
if constexpr (kEnableProxyCrouchSync) { if constexpr (kEnableProxyCrouchSync) {
if (!wasInitialized || a_previous.lastIsCrouching != a_desired.isCrouching) { if (!wasInitialized || a_previous.lastIsCrouching != a_desired.isCrouching) {
fireEvent(a_desired.isCrouching ? GraphEvent::kCrouchStart : GraphEvent::kCrouchStop); fireEvent(a_desired.isCrouching ? GraphEvent::kCrouchStart : GraphEvent::kCrouchStop);
@@ -654,10 +696,9 @@ namespace F4T::ProxyAnimationSync
// Hand the puppet Update hook the speed to re-assert after the engine resets // Hand the puppet Update hook the speed to re-assert after the engine resets
// it each frame. Negative when idle so the engine's idle Speed (0) stands. // it each frame. Negative when idle so the engine's idle Speed (0) stands.
F4T::ProxyPuppet::SetProxyLocomotionState( const float puppetSpeed =
actorId, (desired.isJumping || !desired.isMoving) ? -1.0F : desired.graphSpeed;
desired.isMoving ? desired.graphSpeed : -1.0F, F4T::ProxyPuppet::SetProxyLocomotionState(actorId, puppetSpeed, a_remotePlayer.angleZ);
a_remotePlayer.angleZ);
// Throttled per-update diagnostic: shows what speed the proxy actually receives // Throttled per-update diagnostic: shows what speed the proxy actually receives
// vs. computes, and reads back the live graph variables to detect engine clobber. // vs. computes, and reads back the live graph variables to detect engine clobber.
@@ -694,7 +735,8 @@ namespace F4T::ProxyAnimationSync
// Havok needs continuous graph updates to match foot placement to velocity. // Havok needs continuous graph updates to match foot placement to velocity.
const auto shouldApply = const auto shouldApply =
!a_state.hasAppliedGraphState || !a_state.hasAppliedGraphState ||
HasDesiredStateChanged(a_state, desired); HasDesiredStateChanged(a_state, desired) ||
desired.isJumping;
if (!shouldApply) { if (!shouldApply) {
return; return;
@@ -721,7 +763,9 @@ 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) { if (desired.isJumping) {
// Preserve pre-jump tier for takeoff/landing clip selection.
} else if (desired.isMoving) {
a_state.lastLocomotionTier = a_state.lastLocomotionTier =
ComputeLocomotionTier(desired.graphSpeed, desired.isSprinting); ComputeLocomotionTier(desired.graphSpeed, desired.isSprinting);
} else { } else {
+1 -1
View File
@@ -61,7 +61,7 @@ namespace
constexpr auto kMovementStopHoldDuration = 300ms; constexpr auto kMovementStopHoldDuration = 300ms;
constexpr auto kJumpVerticalSpeedThreshold = 80.0F; constexpr auto kJumpVerticalSpeedThreshold = 80.0F;
// Tail hold after IsJumping() clears so transform packets still carry isJumping=true. // Tail hold after IsJumping() clears so transform packets still carry isJumping=true.
constexpr auto kJumpStateHoldDuration = 200ms; constexpr auto kJumpStateHoldDuration = 400ms;
constexpr auto kJumpDiagnosticLogInterval = 1s; constexpr auto kJumpDiagnosticLogInterval = 1s;
constexpr auto kSneakDiagnosticLogInterval = 2s; constexpr auto kSneakDiagnosticLogInterval = 2s;