Add local animation graph read-only debug

Introduce a game-thread-only, read-only diagnostic helper to probe local player animation graph variables and (optionally) numeric cache IDs. Adds F4TLocalAnimationGraphDebug.h/cpp with UpdateLocalPlayerAnimationGraphDebug(...) and wires a call into plugin/src/main.cpp immediately after GetPlayerMovementState. The helper probes curated variable names via CommonLibF4 read-only APIs, logs availability/changes, and includes a disabled-by-default numeric cache ID scan (kEnableLocalPlayerAnimationGraphCacheIdReadOnlyDebug = false) due to a reported crash during testing. Update also adds extensive dev-log documentation (docs/dev-log.md) describing behavior, testing notes, and rollback instructions. Build succeeded; manual F4SE in-game verification remains pending.
This commit is contained in:
2026-06-03 12:40:26 +12:00
parent 1aecccefb7
commit 3d301a436b
4 changed files with 1056 additions and 0 deletions
+804
View File
@@ -0,0 +1,804 @@
#include "pch.h"
#include "F4TLocalAnimationGraphDebug.h"
#include "F4TNetworking.h"
#include "RE/A/ACTOR_STANCE.h"
#include "RE/B/BGSAnimationSystemUtils.h"
#include "RE/B/BSFixedString.h"
#include "RE/I/IAnimationGraphManagerHolder.h"
#include "RE/P/PlayerCharacter.h"
#include <array>
#include <cmath>
#include <cstdint>
#include <format>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace
{
constexpr bool kEnableLocalPlayerAnimationGraphReadOnlyDebug = true;
// Numeric graph cache ID probing caused a Fallout 4 crash during testing. Keep disabled by default.
// Future investigation must isolate single IDs/types manually instead of scanning the full cache.
constexpr bool kEnableLocalPlayerAnimationGraphCacheIdReadOnlyDebug = false;
constexpr bool kEnableProxyAnimationGraphReadOnlyDebug = false;
constexpr auto kCandidateGraphVariables = std::to_array<std::string_view>({
"IsSneaking",
"bIsSneaking",
"IsCrouching",
"bIsCrouching",
"Sneaking",
"bSneaking",
"IsMoving",
"bIsMoving",
"Speed",
"speed",
"Direction",
"fSpeed",
"bSprint",
"IsSprinting",
"IsJumping",
"bInJumpState",
"WeaponDrawn",
"bWeaponDrawn",
});
struct LocalAnimationGraphState
{
bool isMoving{ false };
bool isSprinting{ false };
bool isSneaking{ false };
bool isJumping{ false };
bool weaponDrawn{ false };
bool playerCharacterIsSneaking{ false };
bool actorIsSneaking{ false };
bool apiJumping{ false };
bool apiSprinting{ false };
bool apiWeaponDrawn{ false };
bool actorStateForceSneak{ false };
std::uint32_t actorStateStance{ 0 };
std::int32_t weaponState{ 0 };
std::int32_t gunState{ 0 };
float movementSpeed{ 0.0F };
bool graphManagerAvailable{ false };
bool graphInTransition{ false };
std::uint32_t graphVariableCacheSize{ 0 };
};
struct CandidateReadState
{
bool boolUnavailableLogged{ false };
bool intUnavailableLogged{ false };
bool floatUnavailableLogged{ false };
bool hasBoolValue{ false };
bool lastBoolValue{ false };
bool hasIntValue{ false };
std::int32_t lastIntValue{ 0 };
bool hasFloatValue{ false };
float lastFloatValue{ 0.0F };
};
struct CacheIdReadState
{
bool boolUnavailableObserved{ false };
bool hasBoolValue{ false };
bool lastBoolValue{ false };
bool intUnavailableObserved{ false };
bool hasIntValue{ false };
std::int32_t lastIntValue{ 0 };
bool floatUnavailableObserved{ false };
bool hasFloatValue{ false };
float lastFloatValue{ 0.0F };
};
struct CacheIdDebugState
{
bool hasCacheSize{ false };
std::uint32_t cacheSize{ 0 };
std::vector<CacheIdReadState> idStates{};
std::vector<std::uint32_t> candidateSneakBoolIds{};
std::vector<std::uint32_t> candidateSneakIntIds{};
std::vector<std::uint32_t> candidateMovementFloatIds{};
std::vector<std::uint32_t> candidateWeaponBoolIds{};
std::vector<std::uint32_t> candidateWeaponIntIds{};
std::vector<std::uint32_t> candidateJumpBoolIds{};
std::vector<std::uint32_t> candidateJumpIntIds{};
std::vector<std::uint32_t> candidateSprintBoolIds{};
std::vector<std::uint32_t> candidateSprintIntIds{};
};
struct CacheIdProbeUpdate
{
std::vector<std::uint32_t> changedBoolIds{};
std::vector<std::uint32_t> changedIntIds{};
std::vector<std::uint32_t> changedFloatIds{};
std::uint32_t readableBoolIds{ 0 };
std::uint32_t readableIntIds{ 0 };
std::uint32_t readableFloatIds{ 0 };
bool candidateSummaryChanged{ false };
};
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 });
}
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) > 0.001F;
}
std::string FormatIdList(const std::vector<std::uint32_t>& a_ids)
{
std::string result{ "[" };
for (std::size_t index = 0; index < a_ids.size(); ++index) {
if (index != 0) {
result += ", ";
}
result += std::format("{}", a_ids[index]);
}
result += "]";
return result;
}
std::string FormatLocalStateContext(const LocalAnimationGraphState& a_state)
{
return std::format(
"moving={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, movementSpeed={:.1f}, actorStateStance={}, actorStateForceSneak={}",
a_state.isMoving,
a_state.isSprinting,
a_state.isSneaking,
a_state.isJumping,
a_state.weaponDrawn,
a_state.movementSpeed,
a_state.actorStateStance,
a_state.actorStateForceSneak);
}
bool AddUniqueId(std::vector<std::uint32_t>& a_ids, std::uint32_t a_id)
{
for (const auto existingId : a_ids) {
if (existingId == a_id) {
return false;
}
}
a_ids.push_back(a_id);
return true;
}
bool MatchesStateBool(bool a_value, bool a_stateValue)
{
return a_value == a_stateValue;
}
bool MatchesStateInt(std::int32_t a_value, bool a_stateValue)
{
if (a_value != 0 && a_value != 1) {
return false;
}
return (a_value != 0) == a_stateValue;
}
bool HasRelevantStateChange(
const std::optional<LocalAnimationGraphState>& a_lastState,
const LocalAnimationGraphState& a_currentState)
{
if (!a_lastState) {
return true;
}
const auto& lastState = *a_lastState;
return lastState.isMoving != a_currentState.isMoving ||
lastState.isSprinting != a_currentState.isSprinting ||
lastState.isSneaking != a_currentState.isSneaking ||
lastState.isJumping != a_currentState.isJumping ||
lastState.weaponDrawn != a_currentState.weaponDrawn ||
lastState.playerCharacterIsSneaking != a_currentState.playerCharacterIsSneaking ||
lastState.actorIsSneaking != a_currentState.actorIsSneaking ||
lastState.apiJumping != a_currentState.apiJumping ||
lastState.apiSprinting != a_currentState.apiSprinting ||
lastState.apiWeaponDrawn != a_currentState.apiWeaponDrawn ||
lastState.actorStateForceSneak != a_currentState.actorStateForceSneak ||
lastState.actorStateStance != a_currentState.actorStateStance ||
lastState.weaponState != a_currentState.weaponState ||
lastState.gunState != a_currentState.gunState;
}
bool HasGraphMetadataChange(
const std::optional<LocalAnimationGraphState>& a_lastState,
const LocalAnimationGraphState& a_currentState)
{
if (!a_lastState) {
return true;
}
const auto& lastState = *a_lastState;
return lastState.graphManagerAvailable != a_currentState.graphManagerAvailable ||
lastState.graphInTransition != a_currentState.graphInTransition ||
lastState.graphVariableCacheSize != a_currentState.graphVariableCacheSize;
}
bool HasSneakTransition(
const std::optional<LocalAnimationGraphState>& a_lastState,
const LocalAnimationGraphState& a_currentState)
{
return a_lastState && a_lastState->isSneaking != a_currentState.isSneaking;
}
bool HasMovementTransition(
const std::optional<LocalAnimationGraphState>& a_lastState,
const LocalAnimationGraphState& a_currentState)
{
if (!a_lastState) {
return false;
}
return a_lastState->isMoving != a_currentState.isMoving ||
AreFloatsDifferent(a_lastState->movementSpeed, a_currentState.movementSpeed);
}
bool HasJumpTransition(
const std::optional<LocalAnimationGraphState>& a_lastState,
const LocalAnimationGraphState& a_currentState)
{
return a_lastState && a_lastState->isJumping != a_currentState.isJumping;
}
bool HasWeaponTransition(
const std::optional<LocalAnimationGraphState>& a_lastState,
const LocalAnimationGraphState& a_currentState)
{
return a_lastState && a_lastState->weaponDrawn != a_currentState.weaponDrawn;
}
bool HasSprintTransition(
const std::optional<LocalAnimationGraphState>& a_lastState,
const LocalAnimationGraphState& a_currentState)
{
return a_lastState && a_lastState->isSprinting != a_currentState.isSprinting;
}
void LogStateSnapshot(
std::string_view a_reason,
const LocalAnimationGraphState& a_state)
{
const auto actorStateSneaking =
a_state.actorStateStance == static_cast<std::uint32_t>(RE::ACTOR_STANCE::kSneaking);
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph debug: {} moving={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, movementSpeed={:.1f}, playerCharacterIsSneaking={}, actorIsSneaking={}, actorStateSneaking={}, actorStateStance={}, actorStateForceSneak={}, apiJumping={}, apiSprinting={}, apiWeaponDrawn={}, weaponState={}, gunState={}, graphManagerAvailable={}, graphVariableCacheSize={}, graphInTransition={}.",
a_reason,
a_state.isMoving,
a_state.isSprinting,
a_state.isSneaking,
a_state.isJumping,
a_state.weaponDrawn,
a_state.movementSpeed,
a_state.playerCharacterIsSneaking,
a_state.actorIsSneaking,
actorStateSneaking,
a_state.actorStateStance,
a_state.actorStateForceSneak,
a_state.apiJumping,
a_state.apiSprinting,
a_state.apiWeaponDrawn,
a_state.weaponState,
a_state.gunState,
a_state.graphManagerAvailable,
a_state.graphVariableCacheSize,
a_state.graphInTransition));
}
void ProbeBoolCandidate(
const RE::IAnimationGraphManagerHolder& a_holder,
std::string_view a_candidate,
CandidateReadState& a_state,
bool a_shouldLogSuccessfulValues)
{
const RE::BSFixedString variableName{ a_candidate.data() };
bool value = false;
if (!a_holder.GetGraphVariableImplBool(variableName, value)) {
if (!a_state.boolUnavailableLogged) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph debug: candidate bool variable \"{}\" unavailable.",
a_candidate));
a_state.boolUnavailableLogged = true;
}
return;
}
const auto changed = !a_state.hasBoolValue || a_state.lastBoolValue != value;
if (changed || a_shouldLogSuccessfulValues) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph debug: candidate bool variable \"{}\"={}.",
a_candidate,
value));
}
a_state.hasBoolValue = true;
a_state.lastBoolValue = value;
}
void ProbeIntCandidate(
const RE::IAnimationGraphManagerHolder& a_holder,
std::string_view a_candidate,
CandidateReadState& a_state,
bool a_shouldLogSuccessfulValues)
{
const RE::BSFixedString variableName{ a_candidate.data() };
std::int32_t value = 0;
if (!a_holder.GetGraphVariableImplInt(variableName, value)) {
if (!a_state.intUnavailableLogged) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph debug: candidate int variable \"{}\" unavailable.",
a_candidate));
a_state.intUnavailableLogged = true;
}
return;
}
const auto changed = !a_state.hasIntValue || a_state.lastIntValue != value;
if (changed || a_shouldLogSuccessfulValues) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph debug: candidate int variable \"{}\"={}.",
a_candidate,
value));
}
a_state.hasIntValue = true;
a_state.lastIntValue = value;
}
void ProbeFloatCandidate(
const RE::IAnimationGraphManagerHolder& a_holder,
std::string_view a_candidate,
CandidateReadState& a_state,
bool a_shouldLogSuccessfulValues)
{
const RE::BSFixedString variableName{ a_candidate.data() };
float value = 0.0F;
if (!a_holder.GetGraphVariableImplFloat(variableName, value)) {
if (!a_state.floatUnavailableLogged) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph debug: candidate float variable \"{}\" unavailable.",
a_candidate));
a_state.floatUnavailableLogged = true;
}
return;
}
const auto changed = !a_state.hasFloatValue || AreFloatsDifferent(a_state.lastFloatValue, value);
if (changed || a_shouldLogSuccessfulValues) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph debug: candidate float variable \"{}\"={:.3f}.",
a_candidate,
value));
}
a_state.hasFloatValue = true;
a_state.lastFloatValue = value;
}
void ProbeCandidateGraphVariables(
const RE::IAnimationGraphManagerHolder& a_holder,
bool a_shouldLogSuccessfulValues)
{
static std::array<CandidateReadState, kCandidateGraphVariables.size()> candidateStates{};
for (std::size_t index = 0; index < kCandidateGraphVariables.size(); ++index) {
const auto candidate = kCandidateGraphVariables[index];
auto& state = candidateStates[index];
ProbeBoolCandidate(a_holder, candidate, state, a_shouldLogSuccessfulValues);
ProbeIntCandidate(a_holder, candidate, state, a_shouldLogSuccessfulValues);
ProbeFloatCandidate(a_holder, candidate, state, a_shouldLogSuccessfulValues);
}
}
void ResetCacheIdDebugState(CacheIdDebugState& a_debugState, std::uint32_t a_cacheSize)
{
a_debugState.hasCacheSize = true;
a_debugState.cacheSize = a_cacheSize;
a_debugState.idStates.clear();
a_debugState.idStates.resize(a_cacheSize);
a_debugState.candidateSneakBoolIds.clear();
a_debugState.candidateSneakIntIds.clear();
a_debugState.candidateMovementFloatIds.clear();
a_debugState.candidateWeaponBoolIds.clear();
a_debugState.candidateWeaponIntIds.clear();
a_debugState.candidateJumpBoolIds.clear();
a_debugState.candidateJumpIntIds.clear();
a_debugState.candidateSprintBoolIds.clear();
a_debugState.candidateSprintIntIds.clear();
}
void MaybeLogBoolCandidate(
CacheIdDebugState& a_debugState,
CacheIdProbeUpdate& a_update,
std::uint32_t a_id,
bool a_value,
const LocalAnimationGraphState& a_currentState,
bool a_sneakTransition,
bool a_weaponTransition,
bool a_jumpTransition,
bool a_sprintTransition)
{
if (a_sneakTransition && MatchesStateBool(a_value, a_currentState.isSneaking) &&
AddUniqueId(a_debugState.candidateSneakBoolIds, a_id)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: candidate sneak bool id {} mirrors local sneaking transition.",
a_id));
a_update.candidateSummaryChanged = true;
}
if (a_weaponTransition && MatchesStateBool(a_value, a_currentState.weaponDrawn) &&
AddUniqueId(a_debugState.candidateWeaponBoolIds, a_id)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: candidate weapon bool id {} mirrors local weapon drawn transition.",
a_id));
a_update.candidateSummaryChanged = true;
}
if (a_jumpTransition && MatchesStateBool(a_value, a_currentState.isJumping) &&
AddUniqueId(a_debugState.candidateJumpBoolIds, a_id)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: candidate jump bool id {} mirrors local jump transition.",
a_id));
a_update.candidateSummaryChanged = true;
}
if (a_sprintTransition && MatchesStateBool(a_value, a_currentState.isSprinting) &&
AddUniqueId(a_debugState.candidateSprintBoolIds, a_id)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: candidate sprint bool id {} mirrors local sprint transition.",
a_id));
a_update.candidateSummaryChanged = true;
}
}
void MaybeLogIntCandidate(
CacheIdDebugState& a_debugState,
CacheIdProbeUpdate& a_update,
std::uint32_t a_id,
std::int32_t a_value,
const LocalAnimationGraphState& a_currentState,
bool a_sneakTransition,
bool a_weaponTransition,
bool a_jumpTransition,
bool a_sprintTransition)
{
if (a_sneakTransition && MatchesStateInt(a_value, a_currentState.isSneaking) &&
AddUniqueId(a_debugState.candidateSneakIntIds, a_id)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: candidate sneak int id {} mirrors local sneaking transition.",
a_id));
a_update.candidateSummaryChanged = true;
}
if (a_weaponTransition && MatchesStateInt(a_value, a_currentState.weaponDrawn) &&
AddUniqueId(a_debugState.candidateWeaponIntIds, a_id)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: candidate weapon int id {} mirrors local weapon drawn transition.",
a_id));
a_update.candidateSummaryChanged = true;
}
if (a_jumpTransition && MatchesStateInt(a_value, a_currentState.isJumping) &&
AddUniqueId(a_debugState.candidateJumpIntIds, a_id)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: candidate jump int id {} mirrors local jump transition.",
a_id));
a_update.candidateSummaryChanged = true;
}
if (a_sprintTransition && MatchesStateInt(a_value, a_currentState.isSprinting) &&
AddUniqueId(a_debugState.candidateSprintIntIds, a_id)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: candidate sprint int id {} mirrors local sprint transition.",
a_id));
a_update.candidateSummaryChanged = true;
}
}
void MaybeLogFloatCandidate(
CacheIdDebugState& a_debugState,
CacheIdProbeUpdate& a_update,
std::uint32_t a_id,
bool a_movementTransition)
{
if (a_movementTransition && AddUniqueId(a_debugState.candidateMovementFloatIds, a_id)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: float id {} appears movement-speed-related.",
a_id));
a_update.candidateSummaryChanged = true;
}
}
void LogTransitionChangedIds(
std::string_view a_transition,
const CacheIdProbeUpdate& a_update,
const LocalAnimationGraphState& a_currentState)
{
if (a_update.changedBoolIds.empty() && a_update.changedIntIds.empty() && a_update.changedFloatIds.empty()) {
return;
}
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: {} transition changed boolIds={}, intIds={}, floatIds={} with {}.",
a_transition,
FormatIdList(a_update.changedBoolIds),
FormatIdList(a_update.changedIntIds),
FormatIdList(a_update.changedFloatIds),
FormatLocalStateContext(a_currentState)));
}
void LogCandidateSummary(const CacheIdDebugState& a_debugState)
{
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug summary: candidateSneakBoolIds={}, candidateSneakIntIds={}, candidateMovementFloatIds={}, candidateWeaponBoolIds={}, candidateWeaponIntIds={}, candidateJumpBoolIds={}, candidateJumpIntIds={}, candidateSprintBoolIds={}, candidateSprintIntIds={}.",
FormatIdList(a_debugState.candidateSneakBoolIds),
FormatIdList(a_debugState.candidateSneakIntIds),
FormatIdList(a_debugState.candidateMovementFloatIds),
FormatIdList(a_debugState.candidateWeaponBoolIds),
FormatIdList(a_debugState.candidateWeaponIntIds),
FormatIdList(a_debugState.candidateJumpBoolIds),
FormatIdList(a_debugState.candidateJumpIntIds),
FormatIdList(a_debugState.candidateSprintBoolIds),
FormatIdList(a_debugState.candidateSprintIntIds)));
}
void ProbeCacheIdGraphVariables(
const RE::IAnimationGraphManagerHolder& a_holder,
const LocalAnimationGraphState& a_currentState,
const std::optional<LocalAnimationGraphState>& a_lastState)
{
if constexpr (!kEnableLocalPlayerAnimationGraphCacheIdReadOnlyDebug) {
return;
}
static CacheIdDebugState debugState{};
const auto cacheSize = a_currentState.graphVariableCacheSize;
const auto cacheSizeChanged = !debugState.hasCacheSize || debugState.cacheSize != cacheSize;
if (cacheSizeChanged) {
ResetCacheIdDebugState(debugState, cacheSize);
}
CacheIdProbeUpdate update{};
const auto stateContext = FormatLocalStateContext(a_currentState);
const auto sneakTransition = HasSneakTransition(a_lastState, a_currentState);
const auto movementTransition = HasMovementTransition(a_lastState, a_currentState);
const auto jumpTransition = HasJumpTransition(a_lastState, a_currentState);
const auto weaponTransition = HasWeaponTransition(a_lastState, a_currentState);
const auto sprintTransition = HasSprintTransition(a_lastState, a_currentState);
for (std::uint32_t id = 0; id < cacheSize; ++id) {
auto& idState = debugState.idStates[id];
bool boolValue = false;
if (a_holder.GetGraphVariableImpl(id, boolValue)) {
++update.readableBoolIds;
const auto changed = !idState.hasBoolValue || idState.lastBoolValue != boolValue;
if (changed) {
if (idState.hasBoolValue) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: bool id {} changed {} -> {} with {}.",
id,
idState.lastBoolValue,
boolValue,
stateContext));
} else {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: bool id {} initial {} with {}.",
id,
boolValue,
stateContext));
}
update.changedBoolIds.push_back(id);
MaybeLogBoolCandidate(
debugState,
update,
id,
boolValue,
a_currentState,
sneakTransition,
weaponTransition,
jumpTransition,
sprintTransition);
}
idState.hasBoolValue = true;
idState.lastBoolValue = boolValue;
} else {
idState.boolUnavailableObserved = true;
}
std::int32_t intValue = 0;
if (a_holder.GetGraphVariableImpl(id, intValue)) {
++update.readableIntIds;
const auto changed = !idState.hasIntValue || idState.lastIntValue != intValue;
if (changed) {
if (idState.hasIntValue) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: int id {} changed {} -> {} with {}.",
id,
idState.lastIntValue,
intValue,
stateContext));
} else {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: int id {} initial {} with {}.",
id,
intValue,
stateContext));
}
update.changedIntIds.push_back(id);
MaybeLogIntCandidate(
debugState,
update,
id,
intValue,
a_currentState,
sneakTransition,
weaponTransition,
jumpTransition,
sprintTransition);
}
idState.hasIntValue = true;
idState.lastIntValue = intValue;
} else {
idState.intUnavailableObserved = true;
}
float floatValue = 0.0F;
if (a_holder.GetGraphVariableImpl(id, floatValue)) {
++update.readableFloatIds;
const auto changed = !idState.hasFloatValue || AreFloatsDifferent(idState.lastFloatValue, floatValue);
if (changed) {
if (idState.hasFloatValue) {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: float id {} changed {:.3f} -> {:.3f} with {}.",
id,
idState.lastFloatValue,
floatValue,
stateContext));
} else {
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: float id {} initial {:.3f} with {}.",
id,
floatValue,
stateContext));
}
update.changedFloatIds.push_back(id);
MaybeLogFloatCandidate(debugState, update, id, movementTransition);
}
idState.hasFloatValue = true;
idState.lastFloatValue = floatValue;
} else {
idState.floatUnavailableObserved = true;
}
}
if (cacheSizeChanged) {
const auto allUnavailable =
update.readableBoolIds == 0 && update.readableIntIds == 0 && update.readableFloatIds == 0;
LogInfoWithLocalPlayerPrefix(std::format(
"Local animation graph cache debug: cacheSize={}, readableBoolIds={}, readableIntIds={}, readableFloatIds={}{}.",
cacheSize,
update.readableBoolIds,
update.readableIntIds,
update.readableFloatIds,
allUnavailable ? ", all numeric cache ID reads unavailable" : ""));
}
if (sneakTransition) {
LogTransitionChangedIds("sneak", update, a_currentState);
}
if (movementTransition) {
LogTransitionChangedIds("movement", update, a_currentState);
}
if (jumpTransition) {
LogTransitionChangedIds("jump", update, a_currentState);
}
if (weaponTransition) {
LogTransitionChangedIds("weapon", update, a_currentState);
}
if (sprintTransition) {
LogTransitionChangedIds("sprint", update, a_currentState);
}
if (update.candidateSummaryChanged) {
LogCandidateSummary(debugState);
}
}
}
namespace F4T::LocalAnimationGraphDebug
{
void UpdateLocalPlayerAnimationGraphDebug(
RE::PlayerCharacter& a_player,
bool a_isMoving,
bool a_isSprinting,
bool a_isSneaking,
bool a_isJumping,
bool a_weaponDrawn,
float a_movementSpeed,
std::chrono::steady_clock::time_point a_currentTime)
{
(void)a_currentTime;
if constexpr (!kEnableLocalPlayerAnimationGraphReadOnlyDebug) {
return;
}
static_assert(!kEnableProxyAnimationGraphReadOnlyDebug);
static std::optional<LocalAnimationGraphState> lastState{};
auto& actor = static_cast<RE::Actor&>(a_player);
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_player);
RE::BSTSmartPointer<RE::BSAnimationGraphManager> graphManager;
const auto graphManagerAvailable =
graphHolder->GetAnimationGraphManagerImpl(graphManager) && static_cast<bool>(graphManager);
const auto graphVariableCacheSize = graphHolder->GetGraphVariableCacheSize();
const auto graphInTransition =
RE::BGSAnimationSystemUtils::IsActiveGraphInTransition(static_cast<RE::TESObjectREFR*>(&a_player));
const LocalAnimationGraphState currentState{
a_isMoving,
a_isSprinting,
a_isSneaking,
a_isJumping,
a_weaponDrawn,
a_player.IsSneaking(),
actor.IsSneaking(),
a_player.IsJumping(),
a_player.DoGetSprinting(),
a_player.GetWeaponMagicDrawn(),
a_player.forceSneak != 0,
static_cast<std::uint32_t>(a_player.stance),
static_cast<std::int32_t>(a_player.weaponState),
static_cast<std::int32_t>(a_player.gunState),
a_movementSpeed,
graphManagerAvailable,
graphInTransition,
graphVariableCacheSize
};
const auto relevantStateChanged = HasRelevantStateChange(lastState, currentState);
const auto graphMetadataChanged = HasGraphMetadataChange(lastState, currentState);
if (relevantStateChanged || graphMetadataChanged) {
LogStateSnapshot(
relevantStateChanged ? "state transition" : "graph metadata changed",
currentState);
}
ProbeCandidateGraphVariables(*graphHolder, relevantStateChanged || graphMetadataChanged);
ProbeCacheIdGraphVariables(*graphHolder, currentState, lastState);
lastState = currentState;
}
}
+10
View File
@@ -1,4 +1,5 @@
#include "F4TNetworking.h"
#include "F4TLocalAnimationGraphDebug.h"
#include "F4TProxyActorController.h"
#include "RE/A/ACTOR_STANCE.h"
@@ -399,6 +400,15 @@ namespace
const auto currentTime = std::chrono::steady_clock::now();
const auto currentMovementState =
GetPlayerMovementState(*player, hasLastTransform ? &lastSampleTransform : nullptr, lastSampleTime, currentTransform, currentTime);
F4T::LocalAnimationGraphDebug::UpdateLocalPlayerAnimationGraphDebug(
*player,
currentMovementState.isMoving,
currentMovementState.isSprinting,
currentMovementState.isSneaking,
currentMovementState.isJumping,
currentMovementState.weaponDrawn,
currentMovementState.movementSpeed,
currentTime);
if (!hasLastTransform) {
lastSentTransform = currentTransform;
lastSampleTransform = currentTransform;