Add data-only proxy animation debug logging

Track and log per-slot animation/movement diagnostics for runtime proxy actors without applying any visual animation. Adds a ProxyAnimationSpeedBucket enum and GetProxyAnimationSpeedBucket/GetProxyAnimationSpeedBucketLabel helpers, a new UpdateProxyAnimationStateDebug() routine, and fields on ProxyActorSlot to remember last-observed moving, sprinting, sneaking, jumping, weapon-drawn, movement type, and coarse speed-bucket state. Resets observation state when slots are created/reused and emits initial/transition-only logs including playerId and proxy FormID. Documentation and dev-log entries updated; visual animation, animation graph changes, and AI/movement behavior remain intentionally unmodified.
This commit is contained in:
2026-06-03 11:36:38 +12:00
parent 3d1abc66ad
commit 44ea1b98da
3 changed files with 189 additions and 19 deletions
+115 -16
View File
@@ -141,6 +141,15 @@ namespace
kHolding
};
enum class ProxyAnimationSpeedBucket
{
kIdle,
kSlow,
kWalk,
kRun,
kSprint
};
struct RemotePlayerSelection
{
std::optional<RemotePlayer> selectedPlayer;
@@ -218,7 +227,14 @@ namespace
bool remoteStateMovedLogged{ false };
bool reusableLogged{ false };
bool directRemoteSpawnLogged{ false };
std::optional<bool> lastObservedSneakState;
bool hasLoggedInitialAnimationState{ false };
std::optional<bool> lastObservedMovingState;
std::optional<bool> lastObservedSprintingState;
std::optional<bool> lastObservedSneakingState;
std::optional<bool> lastObservedJumpingState;
std::optional<bool> lastObservedWeaponDrawnState;
std::optional<std::string> lastObservedMovementType;
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
};
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
@@ -361,6 +377,10 @@ namespace
}
void ResetRemoteMovementStateTracking();
void UpdateProxyAnimationStateDebug(
ProxyActorSlot& a_slot,
const RemotePlayer& a_remotePlayer,
const RE::Actor& a_proxy);
const char* GetProxyNeutralizationReasonLabel(ProxyNeutralizationReason a_reason)
{
@@ -378,6 +398,47 @@ namespace
}
}
const char* GetProxyAnimationSpeedBucketLabel(ProxyAnimationSpeedBucket a_bucket)
{
switch (a_bucket) {
case ProxyAnimationSpeedBucket::kIdle:
return "idle";
case ProxyAnimationSpeedBucket::kSlow:
return "slow";
case ProxyAnimationSpeedBucket::kWalk:
return "walk";
case ProxyAnimationSpeedBucket::kRun:
return "run";
case ProxyAnimationSpeedBucket::kSprint:
return "sprint";
default:
return "unknown";
}
}
ProxyAnimationSpeedBucket GetProxyAnimationSpeedBucket(const RemotePlayer& a_remotePlayer)
{
if (!a_remotePlayer.isMoving || a_remotePlayer.movementSpeed < 1.0F) {
return ProxyAnimationSpeedBucket::kIdle;
}
if (a_remotePlayer.isSprinting) {
return ProxyAnimationSpeedBucket::kSprint;
}
// TODO: Tune these diagnostic thresholds against real Fallout 4 movement units
// before using them for visual locomotion animation decisions.
if (a_remotePlayer.movementSpeed < 75.0F) {
return ProxyAnimationSpeedBucket::kSlow;
}
if (a_remotePlayer.movementSpeed < 150.0F) {
return ProxyAnimationSpeedBucket::kWalk;
}
if (a_remotePlayer.movementSpeed < 300.0F) {
return ProxyAnimationSpeedBucket::kRun;
}
return ProxyAnimationSpeedBucket::kSprint;
}
std::string FormatPosition(const RE::NiPoint3& a_position)
{
return std::format("({:.2f}, {:.2f}, {:.2f})", a_position.x, a_position.y, a_position.z);
@@ -4001,7 +4062,14 @@ namespace
void ResetSlotObservationState(ProxyActorSlot& a_slot)
{
a_slot.lastObservedSneakState.reset();
a_slot.hasLoggedInitialAnimationState = false;
a_slot.lastObservedMovingState.reset();
a_slot.lastObservedSprintingState.reset();
a_slot.lastObservedSneakingState.reset();
a_slot.lastObservedJumpingState.reset();
a_slot.lastObservedWeaponDrawnState.reset();
a_slot.lastObservedMovementType.reset();
a_slot.lastObservedMovementSpeedBucket.reset();
}
bool HasSlotHeldSinceTime(const ProxyActorSlot& a_slot)
@@ -4161,7 +4229,6 @@ namespace
a_slot.restoredForCurrentState = true;
a_slot.firstSmoothedMovementLogged = false;
a_slot.remoteStateMovedLogged = false;
a_slot.lastObservedSneakState = a_remotePlayer.isSneaking;
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor {:08X} restored/reused for remote player {} at X={:.2f}, Y={:.2f}, Z={:.2f}.",
@@ -4170,6 +4237,7 @@ namespace
a_remotePlayer.x,
a_remotePlayer.y,
a_remotePlayer.z));
UpdateProxyAnimationStateDebug(a_slot, a_remotePlayer, a_proxy);
LogProxyMovementDiagnostic(
a_proxy,
a_remotePlayer,
@@ -4179,22 +4247,52 @@ namespace
ProxyMovementDiagnosticMode::kRestore);
}
void ApplyRemoteMovementStateToSlot(ProxyActorSlot& a_slot, const RemotePlayer& a_remotePlayer)
void UpdateProxyAnimationStateDebug(
ProxyActorSlot& a_slot,
const RemotePlayer& a_remotePlayer,
const RE::Actor& a_proxy)
{
if (!a_slot.lastObservedSneakState) {
a_slot.lastObservedSneakState = a_remotePlayer.isSneaking;
return;
const auto speedBucket = GetProxyAnimationSpeedBucket(a_remotePlayer);
const auto shouldLogInitial = !a_slot.hasLoggedInitialAnimationState;
const auto changed =
!a_slot.lastObservedMovingState || *a_slot.lastObservedMovingState != a_remotePlayer.isMoving ||
!a_slot.lastObservedSprintingState || *a_slot.lastObservedSprintingState != a_remotePlayer.isSprinting ||
!a_slot.lastObservedSneakingState || *a_slot.lastObservedSneakingState != a_remotePlayer.isSneaking ||
!a_slot.lastObservedJumpingState || *a_slot.lastObservedJumpingState != a_remotePlayer.isJumping ||
!a_slot.lastObservedWeaponDrawnState || *a_slot.lastObservedWeaponDrawnState != a_remotePlayer.weaponDrawn ||
!a_slot.lastObservedMovementType || *a_slot.lastObservedMovementType != a_remotePlayer.movementType ||
!a_slot.lastObservedMovementSpeedBucket || *a_slot.lastObservedMovementSpeedBucket != speedBucket;
if (shouldLogInitial || changed) {
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy animation state {} for remote player {}: actor={:08X}, holdingIndex={}, moving={}, speed={:.1f}, speedBucket={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, movementType={}.",
shouldLogInitial ? "initial" : "changed",
a_remotePlayer.playerId,
a_proxy.GetFormID(),
a_slot.holdingPositionIndex,
a_remotePlayer.isMoving,
a_remotePlayer.movementSpeed,
GetProxyAnimationSpeedBucketLabel(speedBucket),
a_remotePlayer.isSprinting,
a_remotePlayer.isSneaking,
a_remotePlayer.isJumping,
a_remotePlayer.weaponDrawn,
a_remotePlayer.movementType));
}
if (*a_slot.lastObservedSneakState == a_remotePlayer.isSneaking) {
return;
}
a_slot.hasLoggedInitialAnimationState = true;
a_slot.lastObservedMovingState = a_remotePlayer.isMoving;
a_slot.lastObservedSprintingState = a_remotePlayer.isSprinting;
a_slot.lastObservedSneakingState = a_remotePlayer.isSneaking;
a_slot.lastObservedJumpingState = a_remotePlayer.isJumping;
a_slot.lastObservedWeaponDrawnState = a_remotePlayer.weaponDrawn;
a_slot.lastObservedMovementType = a_remotePlayer.movementType;
a_slot.lastObservedMovementSpeedBucket = speedBucket;
a_slot.lastObservedSneakState = a_remotePlayer.isSneaking;
LogInfoWithLocalPlayerPrefix(std::format(
"Remote player {} sneak state changed: isSneaking={}",
a_remotePlayer.playerId,
a_remotePlayer.isSneaking));
// 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.
}
void MoveSlotProxyToRemotePlayer(
@@ -4218,6 +4316,7 @@ namespace
a_slot.holdReason.reset();
a_slot.heldSinceTime = {};
a_slot.reusableLogged = false;
UpdateProxyAnimationStateDebug(a_slot, a_remotePlayer, a_proxy);
if (shouldSnap) {
LogThrottledInfo(
@@ -4362,6 +4461,7 @@ namespace
CaptureSlotInitialRemoteSpawnTransform(slot->second, a_remotePlayer);
if (inserted) {
slot->second.holdingPositionIndex = g_nextProxyHoldingPositionIndex++;
ResetSlotObservationState(slot->second);
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy slot created for remote player {} with holding index {}.",
a_remotePlayer.playerId,
@@ -4416,7 +4516,6 @@ namespace
return true;
}
ApplyRemoteMovementStateToSlot(a_slot, a_remotePlayer);
MoveSlotProxyToRemotePlayer(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
return true;
}