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;
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +507,62 @@ 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;
|
if constexpr (kEnableProxyJumpSync) {
|
||||||
|
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||||
|
if (!wasInitialized || a_previous.lastIsJumping != a_desired.isJumping) {
|
||||||
|
if (a_desired.isJumping) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
fireEvent(GraphEvent::kJumpUp);
|
||||||
|
ApplyJumpStateToGraph(*graphHolder, true);
|
||||||
|
} else if (wasInitialized) {
|
||||||
|
const auto landTier = a_previous.lastLocomotionTier.value_or(currentTier);
|
||||||
|
fireEvent(GraphEvent::kJumpDown);
|
||||||
|
fireEvent(GraphEvent::kJumpLand);
|
||||||
|
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(landTier));
|
||||||
|
} else {
|
||||||
|
fireEvent(GraphEvent::kJumpEnd);
|
||||||
|
fireEvent(GraphEvent::kMoveStop);
|
||||||
|
}
|
||||||
|
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 =
|
const bool tierChanged =
|
||||||
a_desired.isMoving &&
|
a_desired.isMoving &&
|
||||||
(!a_previous.lastLocomotionTier || *a_previous.lastLocomotionTier != currentTier);
|
(!a_previous.lastLocomotionTier || *a_previous.lastLocomotionTier != currentTier);
|
||||||
@@ -496,10 +576,10 @@ namespace
|
|||||||
fireEvent(GetLocomotionTierEventName(currentTier));
|
fireEvent(GetLocomotionTierEventName(currentTier));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sprint sub-state transition (only meaningful while moving).
|
|
||||||
if (wasInitialized && a_previous.lastIsSprinting != a_desired.isSprinting) {
|
if (wasInitialized && a_previous.lastIsSprinting != a_desired.isSprinting) {
|
||||||
fireEvent(a_desired.isSprinting ? GraphEvent::kSprintStart : GraphEvent::kSprintStop);
|
fireEvent(a_desired.isSprinting ? GraphEvent::kSprintStart : GraphEvent::kSprintStop);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if constexpr (kEnableProxySneakSync) {
|
if constexpr (kEnableProxySneakSync) {
|
||||||
if (!wasInitialized || a_previous.lastIsSneaking != a_desired.isSneaking) {
|
if (!wasInitialized || a_previous.lastIsSneaking != a_desired.isSneaking) {
|
||||||
@@ -507,44 +587,6 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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:
|
|
||||||
fireEvent(GraphEvent::kJumpStart);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
fireEvent(GraphEvent::kJumpUp);
|
|
||||||
TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kInJumpState, true);
|
|
||||||
} else if (wasInitialized) {
|
|
||||||
fireEvent(GraphEvent::kJumpDown);
|
|
||||||
fireEvent(GraphEvent::kJumpLand);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user