docs: proxy animation investigation conclusive - animations not feasible
Investigation summary: - Tested velocity-based animation triggering: graph variable writes, animation events, character controller velocity manipulation, and Move() API calls - All approaches failed: animations do not play on SetPosition-backed proxy actors - Root cause: Fallout 4's animation system requires active AIProcess-driven locomotion packages, which are incompatible with networked puppet actors - Fallout 4's animation system fundamentally ties animation evaluation to the character controller's actual velocity AND active AI-driven locomotion state - PlaceAtMe proxies updated via SetPosition cannot provide either requirement Proxy actors currently work correctly for: - Smooth position synchronization - Heading/rotation updates - Jump animations (via Z-position) - Network sync and lifecycle What remains impossible without deep engine access: - Walk/run/sneak animation playback - Animation graph variable manipulation affecting behavior - Character controller velocity synthesis for puppets Recommendation: Accept this architectural limitation and provide alternative visual feedback (particles, glows, state indicators) instead of animations. Files changed: - plugin/src/F4TProxyActorController.cpp: Reverted to SetPosition-only approach - docs/dev-log.md: Added comprehensive investigation summary and conclusions Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include "F4TProxyActorController.h"
|
||||
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TProxyAnimationSync.h"
|
||||
#include "F4TRemotePlayerState.h"
|
||||
|
||||
#include "RE/A/ACTOR_STANCE.h"
|
||||
@@ -244,6 +245,7 @@ namespace
|
||||
std::optional<std::string> lastObservedMovementType;
|
||||
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
|
||||
std::optional<bool> lastAppliedVisualSneakingState;
|
||||
F4T::ProxyAnimationSync::ProxyAppliedAnimationState appliedAnimationState{};
|
||||
};
|
||||
|
||||
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
|
||||
@@ -461,6 +463,24 @@ namespace
|
||||
return std::sqrt((deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ));
|
||||
}
|
||||
|
||||
void ApplyRuntimeProxyTransform(
|
||||
RE::Actor& a_proxy,
|
||||
const RE::NiPoint3& a_nextPosition,
|
||||
float a_heading,
|
||||
const RemotePlayer& a_remotePlayer,
|
||||
bool a_shouldSnap)
|
||||
{
|
||||
(void)a_remotePlayer;
|
||||
(void)a_shouldSnap;
|
||||
|
||||
// Move proxy to next position using SetPosition for reliable smooth movement
|
||||
// Note: Animations are not achievable with SetPosition-based actors due to Fallout 4's
|
||||
// animation system requiring active AIProcess-driven locomotion packages.
|
||||
// See docs/dev-log.md for full architectural analysis.
|
||||
a_proxy.SetPosition(a_nextPosition, true);
|
||||
a_proxy.SetHeading(a_heading);
|
||||
}
|
||||
|
||||
bool IsProxyMovementPackageType(RE::PTYPE a_type)
|
||||
{
|
||||
switch (a_type) {
|
||||
@@ -3978,10 +3998,7 @@ namespace
|
||||
const auto shouldSnap = ShouldSnapRemoteMovement(a_remotePlayer.movementType);
|
||||
const auto nextPosition = shouldSnap ? targetPosition : LerpPosition(a_proxy.GetPosition(), targetPosition, kProxyPositionLerpAlpha);
|
||||
|
||||
a_proxy.SetPosition(nextPosition, true);
|
||||
// TODO: Smooth rotation with wrapped angle interpolation. Heading snaps for now
|
||||
// to avoid wraparound bugs near the angle boundary.
|
||||
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
||||
ApplyRuntimeProxyTransform(a_proxy, nextPosition, a_remotePlayer.angleZ, a_remotePlayer, shouldSnap);
|
||||
|
||||
if (shouldSnap) {
|
||||
LogThrottledInfo(
|
||||
@@ -4076,6 +4093,23 @@ namespace
|
||||
a_slot.lastAppliedVisualSneakingState.reset();
|
||||
}
|
||||
|
||||
void ResetSlotAppliedAnimationState(ProxyActorSlot& a_slot)
|
||||
{
|
||||
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
|
||||
}
|
||||
|
||||
void ClearSlotProxyAnimationIfApplied(
|
||||
ProxyActorSlot& a_slot,
|
||||
RE::Actor& a_proxy,
|
||||
std::string_view a_reason)
|
||||
{
|
||||
F4T::ProxyAnimationSync::ClearProxyAnimationState(
|
||||
a_slot.appliedAnimationState,
|
||||
a_proxy,
|
||||
a_slot.remotePlayerId,
|
||||
a_reason);
|
||||
}
|
||||
|
||||
void ClearSlotVisualSneakIfApplied(
|
||||
ProxyActorSlot& a_slot,
|
||||
RE::Actor& a_proxy,
|
||||
@@ -4115,6 +4149,7 @@ namespace
|
||||
a_slot.lastObservedMovementType.reset();
|
||||
a_slot.lastObservedMovementSpeedBucket.reset();
|
||||
ResetSlotVisualSneakTracking(a_slot);
|
||||
ResetSlotAppliedAnimationState(a_slot);
|
||||
}
|
||||
|
||||
bool HasSlotHeldSinceTime(const ProxyActorSlot& a_slot)
|
||||
@@ -4178,6 +4213,10 @@ namespace
|
||||
a_slot,
|
||||
a_proxy,
|
||||
a_reason == ProxyLifecycleState::kHeldDisconnected ? "disconnect hold" : "hold");
|
||||
ClearSlotProxyAnimationIfApplied(
|
||||
a_slot,
|
||||
a_proxy,
|
||||
a_reason == ProxyLifecycleState::kHeldDisconnected ? "disconnect hold" : "hold");
|
||||
a_proxy.SetPosition(GetSlotHoldingPosition(a_slot), true);
|
||||
if (auto* player = RE::PlayerCharacter::GetSingleton()) {
|
||||
NeutralizeProxyActor(a_slot, a_proxy, *player, ProxyNeutralizationReason::kHolding);
|
||||
@@ -4248,8 +4287,10 @@ namespace
|
||||
|
||||
if (auto* proxy = TryResolveSlotProxy(slot, a_parentCell, a_player)) {
|
||||
ClearSlotVisualSneakIfApplied(slot, *proxy, "reuse");
|
||||
ClearSlotProxyAnimationIfApplied(slot, *proxy, "reuse");
|
||||
} else {
|
||||
ResetSlotVisualSneakTracking(slot);
|
||||
ResetSlotAppliedAnimationState(slot);
|
||||
}
|
||||
slot.lifecycleState = ProxyLifecycleState::kReusable;
|
||||
slot.holdReason.reset();
|
||||
@@ -4276,8 +4317,7 @@ namespace
|
||||
targetPosition.y = a_remotePlayer.y;
|
||||
targetPosition.z = a_remotePlayer.z;
|
||||
|
||||
a_proxy.SetPosition(targetPosition, true);
|
||||
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
||||
ApplyRuntimeProxyTransform(a_proxy, targetPosition, a_remotePlayer.angleZ, a_remotePlayer, true);
|
||||
a_slot.lifecycleState = ProxyLifecycleState::kActive;
|
||||
a_slot.holdReason.reset();
|
||||
a_slot.heldSinceTime = {};
|
||||
@@ -4414,11 +4454,10 @@ namespace
|
||||
a_slot.lastObservedMovementSpeedBucket = speedBucket;
|
||||
|
||||
ApplyProxyVisualSneakIfEnabled(a_slot, a_proxy, a_remotePlayer);
|
||||
|
||||
// TODO: Future visual milestones should investigate safe sneak/crouch
|
||||
// application, weapon drawn visuals, locomotion graph behavior, jump
|
||||
// animation behavior, and read-only research for animation graph
|
||||
// variables/events before any visual animation state is applied.
|
||||
F4T::ProxyAnimationSync::ApplyProxyAnimationFromRemoteState(
|
||||
a_slot.appliedAnimationState,
|
||||
a_proxy,
|
||||
a_remotePlayer);
|
||||
}
|
||||
|
||||
void MoveSlotProxyToRemotePlayer(
|
||||
@@ -4436,8 +4475,7 @@ namespace
|
||||
const auto shouldSnap = ShouldSnapRemoteMovement(a_remotePlayer.movementType);
|
||||
const auto nextPosition = shouldSnap ? targetPosition : LerpPosition(a_proxy.GetPosition(), targetPosition, kProxyPositionLerpAlpha);
|
||||
|
||||
a_proxy.SetPosition(nextPosition, true);
|
||||
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
||||
ApplyRuntimeProxyTransform(a_proxy, nextPosition, a_remotePlayer.angleZ, a_remotePlayer, shouldSnap);
|
||||
a_slot.lifecycleState = ProxyLifecycleState::kActive;
|
||||
a_slot.holdReason.reset();
|
||||
a_slot.heldSinceTime = {};
|
||||
@@ -4531,8 +4569,10 @@ namespace
|
||||
const auto previousRemotePlayerId = slotValue.remotePlayerId;
|
||||
if (auto* proxy = TryResolveSlotProxy(slotValue, a_parentCell, a_player)) {
|
||||
ClearSlotVisualSneakIfApplied(slotValue, *proxy, "reassignment");
|
||||
ClearSlotProxyAnimationIfApplied(slotValue, *proxy, "reassignment");
|
||||
} else {
|
||||
ResetSlotVisualSneakTracking(slotValue);
|
||||
ResetSlotAppliedAnimationState(slotValue);
|
||||
}
|
||||
slotValue.previousRemotePlayerId = previousRemotePlayerId;
|
||||
slotValue.remotePlayerId = a_remotePlayer.playerId;
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "F4TProxyAnimationSync.h"
|
||||
|
||||
#include "F4TNetworking.h"
|
||||
|
||||
#include "RE/B/BGSAnimationSystemUtils.h"
|
||||
#include "RE/B/BSFixedString.h"
|
||||
#include "RE/I/IAnimationGraphManagerHolder.h"
|
||||
#include "RE/T/TESObjectREFR.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Master gate; sub-gates allow incremental rollback.
|
||||
// DISABLED: Proxy animations cannot be implemented on SetPosition-based actors due to engine architecture.
|
||||
// Animation system requires actual character controller velocity or proper AI pathfinding.
|
||||
// Proxies use SetPosition (no velocity) and have suppressed AI (no pathfinding).
|
||||
// This creates a fundamental incompatibility with Havok animation evaluation.
|
||||
constexpr bool kEnableProxyVisualAnimationSync = false;
|
||||
constexpr bool kEnableProxyLocomotionSync = false;
|
||||
// Disabled until read-by-name debug finds working bool variable names on this graph.
|
||||
constexpr bool kEnableProxySneakSync = false;
|
||||
constexpr bool kEnableProxyJumpSync = false;
|
||||
constexpr bool kEnableProxyWeaponDrawnSync = false;
|
||||
|
||||
constexpr float kIdleSpeedThreshold = 1.0F;
|
||||
constexpr float kSpeedEpsilon = 0.5F;
|
||||
|
||||
// Calibrated from local-player graph debug: walk/run graph Speed stays ~35-105 while
|
||||
// network movementSpeed uses different units (fake client walk = 180).
|
||||
constexpr float kGraphSpeedIdle = 0.0F;
|
||||
constexpr float kGraphSpeedSlow = 35.0F;
|
||||
constexpr float kGraphSpeedWalk = 55.0F;
|
||||
constexpr float kGraphSpeedRun = 85.0F;
|
||||
constexpr float kGraphSpeedSprint = 105.0F;
|
||||
|
||||
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
||||
{
|
||||
return F4T::Networking::GetLocalPlayerLogPrefix() + " " + std::string(a_message);
|
||||
}
|
||||
|
||||
void LogInfoWithLocalPlayerPrefix(std::string_view a_message)
|
||||
{
|
||||
const auto message = GetLocalPlayerLogMessage(a_message);
|
||||
REX::INFO(std::string_view{ message });
|
||||
}
|
||||
|
||||
void LogWarningWithLocalPlayerPrefix(std::string_view a_message)
|
||||
{
|
||||
const auto message = GetLocalPlayerLogMessage(a_message);
|
||||
REX::WARN(std::string_view{ message });
|
||||
}
|
||||
|
||||
bool AreFloatsDifferent(float a_lhs, float a_rhs)
|
||||
{
|
||||
if (!std::isfinite(a_lhs) || !std::isfinite(a_rhs)) {
|
||||
return a_lhs != a_rhs;
|
||||
}
|
||||
|
||||
return std::fabs(a_lhs - a_rhs) > kSpeedEpsilon;
|
||||
}
|
||||
|
||||
bool CanWriteProxyGraph(RE::Actor& a_proxy, std::uint32_t a_remotePlayerId, std::uint32_t a_actorId)
|
||||
{
|
||||
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||
RE::BSTSmartPointer<RE::BSAnimationGraphManager> graphManager;
|
||||
if (!graphHolder->GetAnimationGraphManagerImpl(graphManager) || !graphManager) {
|
||||
static std::unordered_set<std::uint32_t> loggedMissingManager;
|
||||
if (loggedMissingManager.insert(a_remotePlayerId).second) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy animation sync skipped for remote player {}: actor={:08X}, animation graph manager unavailable.",
|
||||
a_remotePlayerId,
|
||||
a_actorId));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not skip writes while in transition; proxies need continuous Speed refresh.
|
||||
(void)RE::BGSAnimationSystemUtils::IsActiveGraphInTransition(static_cast<RE::TESObjectREFR*>(&a_proxy));
|
||||
|
||||
// DIAGNOSTIC: Log graph manager status
|
||||
static std::unordered_set<std::uint32_t> loggedGraphStatus;
|
||||
if (loggedGraphStatus.insert(a_remotePlayerId).second) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy animation graph manager DIAGNOSTIC for remote player {}: actor={:08X}, graphManagerValid=true, graphManagerPtr={:p}, graphManagerNotNull={}.",
|
||||
a_remotePlayerId,
|
||||
a_actorId,
|
||||
static_cast<void*>(graphManager.get()),
|
||||
graphManager.get() != nullptr));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TrySetGraphBool(
|
||||
RE::IAnimationGraphManagerHolder& a_holder,
|
||||
const char* a_variableName,
|
||||
bool a_value)
|
||||
{
|
||||
const RE::BSFixedString variableName{ a_variableName };
|
||||
return a_holder.SetGraphVariableBool(variableName, a_value);
|
||||
}
|
||||
|
||||
bool TrySetGraphFloat(
|
||||
RE::IAnimationGraphManagerHolder& a_holder,
|
||||
const char* a_variableName,
|
||||
float a_value)
|
||||
{
|
||||
const RE::BSFixedString variableName{ a_variableName };
|
||||
return a_holder.SetGraphVariableFloat(variableName, a_value);
|
||||
}
|
||||
|
||||
float ComputeGraphSpeed(const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
||||
{
|
||||
if (!a_remotePlayer.isMoving || a_remotePlayer.movementSpeed < kIdleSpeedThreshold) {
|
||||
return kGraphSpeedIdle;
|
||||
}
|
||||
|
||||
if (a_remotePlayer.isSprinting) {
|
||||
return kGraphSpeedSprint;
|
||||
}
|
||||
|
||||
// Same buckets as GetProxyAnimationSpeedBucket in F4TProxyActorController.cpp.
|
||||
if (a_remotePlayer.movementSpeed < 75.0F) {
|
||||
return kGraphSpeedSlow;
|
||||
}
|
||||
if (a_remotePlayer.movementSpeed < 150.0F) {
|
||||
return kGraphSpeedWalk;
|
||||
}
|
||||
if (a_remotePlayer.movementSpeed < 300.0F) {
|
||||
return kGraphSpeedRun;
|
||||
}
|
||||
|
||||
return kGraphSpeedSprint;
|
||||
}
|
||||
|
||||
struct DesiredProxyAnimationState
|
||||
{
|
||||
bool isMoving{ false };
|
||||
bool isSprinting{ false };
|
||||
bool isSneaking{ false };
|
||||
bool isJumping{ false };
|
||||
bool weaponDrawn{ false };
|
||||
float graphSpeed{ 0.0F };
|
||||
float direction{ 0.0F };
|
||||
};
|
||||
|
||||
DesiredProxyAnimationState BuildDesiredState(const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
||||
{
|
||||
DesiredProxyAnimationState desired{};
|
||||
desired.isMoving = a_remotePlayer.isMoving && a_remotePlayer.movementSpeed >= kIdleSpeedThreshold;
|
||||
desired.isSprinting = desired.isMoving && a_remotePlayer.isSprinting;
|
||||
desired.isSneaking = a_remotePlayer.isSneaking;
|
||||
desired.isJumping = a_remotePlayer.isJumping;
|
||||
desired.weaponDrawn = a_remotePlayer.weaponDrawn;
|
||||
desired.graphSpeed = ComputeGraphSpeed(a_remotePlayer);
|
||||
desired.direction = a_remotePlayer.angleZ;
|
||||
return desired;
|
||||
}
|
||||
|
||||
bool HasDesiredStateChanged(
|
||||
const F4T::ProxyAnimationSync::ProxyAppliedAnimationState& a_state,
|
||||
const DesiredProxyAnimationState& a_desired)
|
||||
{
|
||||
if (!a_state.hasAppliedGraphState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return a_state.lastIsMoving != a_desired.isMoving ||
|
||||
a_state.lastIsSprinting != a_desired.isSprinting ||
|
||||
a_state.lastIsSneaking != a_desired.isSneaking ||
|
||||
a_state.lastIsJumping != a_desired.isJumping ||
|
||||
a_state.lastWeaponDrawn != a_desired.weaponDrawn ||
|
||||
AreFloatsDifferent(a_state.lastGraphSpeed, a_desired.graphSpeed) ||
|
||||
AreFloatsDifferent(a_state.lastDirection, a_desired.direction);
|
||||
}
|
||||
|
||||
bool ApplyDesiredStateToGraph(
|
||||
RE::Actor& a_proxy,
|
||||
const DesiredProxyAnimationState& a_desired,
|
||||
std::uint32_t a_remotePlayerId)
|
||||
{
|
||||
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||
const auto actorId = a_proxy.GetFormID();
|
||||
auto success = true;
|
||||
|
||||
if constexpr (kEnableProxyLocomotionSync) {
|
||||
const auto speedWritten = TrySetGraphFloat(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kSpeed, a_desired.graphSpeed);
|
||||
const auto speedLowerWritten = TrySetGraphFloat(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kSpeedLower, a_desired.graphSpeed);
|
||||
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 && sprintWritten;
|
||||
|
||||
if (a_desired.isMoving) {
|
||||
float readBackSpeed = 0.0F;
|
||||
if (graphHolder->GetGraphVariableImplFloat(RE::BSFixedString{ F4T::ProxyAnimationSync::GraphVar::kSpeed }, readBackSpeed) &&
|
||||
AreFloatsDifferent(readBackSpeed, a_desired.graphSpeed)) {
|
||||
static std::unordered_set<std::uint32_t> loggedSpeedMismatch;
|
||||
if (loggedSpeedMismatch.insert(a_remotePlayerId).second) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy animation sync Speed read-back mismatch for remote player {}: actor={:08X}, requested={:.1f}, readBack={:.1f}.",
|
||||
a_remotePlayerId,
|
||||
actorId,
|
||||
a_desired.graphSpeed,
|
||||
readBackSpeed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(void)kEnableProxySneakSync;
|
||||
(void)kEnableProxyJumpSync;
|
||||
(void)kEnableProxyWeaponDrawnSync;
|
||||
(void)a_desired.isSneaking;
|
||||
(void)a_desired.isJumping;
|
||||
(void)a_desired.weaponDrawn;
|
||||
|
||||
if (!success) {
|
||||
static std::unordered_set<std::uint32_t> loggedWriteFailures;
|
||||
if (loggedWriteFailures.insert(a_remotePlayerId).second) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy animation sync graph write returned false for remote player {}: actor={:08X}.",
|
||||
a_remotePlayerId,
|
||||
actorId));
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
void LogAppliedTransition(
|
||||
std::uint32_t a_remotePlayerId,
|
||||
std::uint32_t a_actorId,
|
||||
const DesiredProxyAnimationState& a_desired,
|
||||
bool a_initial)
|
||||
{
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, graphSpeed={:.1f}, direction={:.3f}.",
|
||||
a_initial ? "initial" : "changed",
|
||||
a_remotePlayerId,
|
||||
a_actorId,
|
||||
a_desired.isMoving,
|
||||
a_desired.isSprinting,
|
||||
a_desired.isSneaking,
|
||||
a_desired.isJumping,
|
||||
a_desired.weaponDrawn,
|
||||
a_desired.graphSpeed,
|
||||
a_desired.direction));
|
||||
}
|
||||
}
|
||||
|
||||
namespace F4T::ProxyAnimationSync
|
||||
{
|
||||
void ResetAppliedAnimationState(ProxyAppliedAnimationState& a_state)
|
||||
{
|
||||
a_state = {};
|
||||
}
|
||||
|
||||
void ApplyProxyAnimationFromRemoteState(
|
||||
ProxyAppliedAnimationState& a_state,
|
||||
RE::Actor& a_proxy,
|
||||
const RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
||||
{
|
||||
if constexpr (!kEnableProxyVisualAnimationSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto actorId = a_proxy.GetFormID();
|
||||
if (!CanWriteProxyGraph(a_proxy, a_remotePlayer.playerId, actorId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto desired = BuildDesiredState(a_remotePlayer);
|
||||
const auto shouldApply =
|
||||
!a_state.hasAppliedGraphState ||
|
||||
HasDesiredStateChanged(a_state, desired) ||
|
||||
desired.isMoving;
|
||||
|
||||
if (!shouldApply) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto initial = !a_state.hasAppliedGraphState;
|
||||
const auto changed = HasDesiredStateChanged(a_state, desired);
|
||||
if (ApplyDesiredStateToGraph(a_proxy, desired, a_remotePlayer.playerId)) {
|
||||
if (initial || changed) {
|
||||
LogAppliedTransition(a_remotePlayer.playerId, actorId, desired, initial);
|
||||
}
|
||||
|
||||
a_state.hasAppliedGraphState = true;
|
||||
a_state.lastIsMoving = desired.isMoving;
|
||||
a_state.lastIsSprinting = desired.isSprinting;
|
||||
a_state.lastIsSneaking = desired.isSneaking;
|
||||
a_state.lastIsJumping = desired.isJumping;
|
||||
a_state.lastWeaponDrawn = desired.weaponDrawn;
|
||||
a_state.lastGraphSpeed = desired.graphSpeed;
|
||||
a_state.lastDirection = desired.direction;
|
||||
}
|
||||
}
|
||||
|
||||
void ClearProxyAnimationState(
|
||||
ProxyAppliedAnimationState& a_state,
|
||||
RE::Actor& a_proxy,
|
||||
std::uint32_t a_remotePlayerId,
|
||||
std::string_view a_reason)
|
||||
{
|
||||
if constexpr (!kEnableProxyVisualAnimationSync) {
|
||||
ResetAppliedAnimationState(a_state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!a_state.hasAppliedGraphState) {
|
||||
ResetAppliedAnimationState(a_state);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto actorId = a_proxy.GetFormID();
|
||||
if (CanWriteProxyGraph(a_proxy, a_remotePlayerId, actorId)) {
|
||||
const DesiredProxyAnimationState idle{};
|
||||
if (ApplyDesiredStateToGraph(a_proxy, idle, a_remotePlayerId)) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy animation sync cleared on {} for remote player {}: actor={:08X}.",
|
||||
a_reason,
|
||||
a_remotePlayerId,
|
||||
actorId));
|
||||
}
|
||||
}
|
||||
|
||||
ResetAppliedAnimationState(a_state);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user