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:
@@ -5332,6 +5332,17 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
}
|
||||
|
||||
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 =
|
||||
effectiveRemote.isMoving && effectiveRemote.movementSpeed >= 1.0F;
|
||||
|
||||
|
||||
@@ -282,9 +282,13 @@ namespace
|
||||
}
|
||||
// Apply minimal hold (50ms) for debounce, but don't coast; snap to idle immediately.
|
||||
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
|
||||
// kept bInJumpState true after ground landings.
|
||||
|
||||
// While airborne, suppress locomotion so per-frame Speed/Jog events do not override
|
||||
// the jump graph. Keep graphSpeed for landing-tier selection on jump end.
|
||||
if (desired.isJumping) {
|
||||
desired.isMoving = false;
|
||||
desired.isSprinting = false;
|
||||
}
|
||||
|
||||
if (!desired.isMoving) {
|
||||
desired.isSprinting = false;
|
||||
@@ -373,6 +377,21 @@ namespace
|
||||
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(
|
||||
RE::Actor& a_proxy,
|
||||
const DesiredProxyAnimationState& a_desired,
|
||||
@@ -381,6 +400,13 @@ namespace
|
||||
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||
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();
|
||||
F4T::AnimationDescriptor::AnimationVariableSnapshot snapshot;
|
||||
|
||||
@@ -391,25 +417,25 @@ namespace
|
||||
|
||||
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);
|
||||
if (isSprintingIdx != static_cast<std::size_t>(-1)) {
|
||||
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) {
|
||||
const auto inJumpIdx = descriptor.GetBoolVariableIndex(F4T::ProxyAnimationSync::GraphVar::kInJumpState);
|
||||
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);
|
||||
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 =
|
||||
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) {
|
||||
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||
if (!wasInitialized || a_previous.lastIsJumping != 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:
|
||||
const bool wasLocomoting =
|
||||
a_previous.lastIsMoving || a_previous.lastLocomotionTier.has_value();
|
||||
if (wasLocomoting) {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
fireEvent(GraphEvent::kJumpStart);
|
||||
break;
|
||||
}
|
||||
fireEvent(GraphEvent::kJumpUp);
|
||||
TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kInJumpState, true);
|
||||
ApplyJumpStateToGraph(*graphHolder, true);
|
||||
} else if (wasInitialized) {
|
||||
const auto landTier = a_previous.lastLocomotionTier.value_or(currentTier);
|
||||
fireEvent(GraphEvent::kJumpDown);
|
||||
fireEvent(GraphEvent::kJumpLand);
|
||||
if (a_desired.isMoving) {
|
||||
if (currentTier == F4T::ProxyAnimationSync::LocomotionTier::kRun) {
|
||||
const auto remoteMoving =
|
||||
a_desired.isMoving || a_desired.graphSpeed >= kIdleSpeedThreshold;
|
||||
if (remoteMoving) {
|
||||
if (landTier == F4T::ProxyAnimationSync::LocomotionTier::kRun) {
|
||||
fireEvent(GraphEvent::kJumpLandToRun);
|
||||
} else {
|
||||
fireEvent(GraphEvent::kJumpLandToWalk);
|
||||
}
|
||||
fireEvent(GraphEvent::kMoveStart);
|
||||
fireEvent(GetLocomotionTierEventName(currentTier));
|
||||
fireEvent(GetLocomotionTierEventName(landTier));
|
||||
} else {
|
||||
fireEvent(GraphEvent::kJumpEnd);
|
||||
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 (!wasInitialized || a_previous.lastIsCrouching != a_desired.isCrouching) {
|
||||
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
|
||||
// it each frame. Negative when idle so the engine's idle Speed (0) stands.
|
||||
F4T::ProxyPuppet::SetProxyLocomotionState(
|
||||
actorId,
|
||||
desired.isMoving ? desired.graphSpeed : -1.0F,
|
||||
a_remotePlayer.angleZ);
|
||||
const float puppetSpeed =
|
||||
(desired.isJumping || !desired.isMoving) ? -1.0F : desired.graphSpeed;
|
||||
F4T::ProxyPuppet::SetProxyLocomotionState(actorId, puppetSpeed, a_remotePlayer.angleZ);
|
||||
|
||||
// Throttled per-update diagnostic: shows what speed the proxy actually receives
|
||||
// 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.
|
||||
const auto shouldApply =
|
||||
!a_state.hasAppliedGraphState ||
|
||||
HasDesiredStateChanged(a_state, desired);
|
||||
HasDesiredStateChanged(a_state, desired) ||
|
||||
desired.isJumping;
|
||||
|
||||
if (!shouldApply) {
|
||||
return;
|
||||
@@ -721,7 +763,9 @@ namespace F4T::ProxyAnimationSync
|
||||
a_state.lastWeaponDrawn = desired.weaponDrawn;
|
||||
a_state.lastGraphSpeed = desired.graphSpeed;
|
||||
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 =
|
||||
ComputeLocomotionTier(desired.graphSpeed, desired.isSprinting);
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ namespace
|
||||
constexpr auto kMovementStopHoldDuration = 300ms;
|
||||
constexpr auto kJumpVerticalSpeedThreshold = 80.0F;
|
||||
// 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 kSneakDiagnosticLogInterval = 2s;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user