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
+219
View File
@@ -2489,6 +2489,225 @@ Next investigation should focus on read-only animation graph variable discovery
--- ---
## 2026-06-03 - Proxy Animation Graph Read-Only Investigation
### Summary
Added a local-player-only, read-only animation graph diagnostic helper to probe
known candidate graph variable names and compare them with existing local
movement, sneak, jump, and weapon state signals.
### Files Changed
- `plugin/include/F4TLocalAnimationGraphDebug.h`
- `plugin/src/F4TLocalAnimationGraphDebug.cpp`
- `plugin/src/main.cpp`
- `docs/dev-log.md`
### Details
- Added `UpdateLocalPlayerAnimationGraphDebug(...)` on the existing game-thread
local polling path, immediately after `GetPlayerMovementState(...)` and
before any transform send decision.
- Added explicit read-only gates:
`kEnableLocalPlayerAnimationGraphReadOnlyDebug = true` and
`kEnableProxyAnimationGraphReadOnlyDebug = false`.
- The helper only inspects the local player. It does not inspect runtime
proxies by default and does not change proxy movement, lifecycle,
neutralization, placed fallback behavior, networking, protocol, server code,
or fake-client tooling.
- Graph access is limited to read-only CommonLibF4 APIs:
`GetAnimationGraphManagerImpl`, `GetGraphVariableCacheSize`,
`GetGraphVariableImplBool`, `GetGraphVariableImplInt`,
`GetGraphVariableImplFloat`, and
`BGSAnimationSystemUtils::IsActiveGraphInTransition`.
- Candidate variables are probed by curated name only. There is no raw cache
walking, no fake enumeration, no graph variable writes, no graph events, no
`NotifyAnimationGraphImpl`, no `PerformAction`, no `InitializeActorInstant`,
and no `ActorState` writes.
- Unavailable candidate reads are logged once per variable/type. Successful
reads are logged when their value changes or when local state / graph metadata
changes.
### Testing
- Ran `xmake build` from `plugin`; it succeeded and linked
`Fallout4Together.dll`.
- Manual Fallout 4 / F4SE testing is still pending:
- Launch through F4SE.
- `coc F4TTestCell01`.
- Toggle local sneak on/off.
- Move/stop and sprint if safe.
- Jump.
- Draw/holster weapon if safe.
- Check `Fallout4Together.log` for `Local animation graph debug` lines.
- Optionally add one fake client to confirm runtime proxies still spawn and
move while proxy graph diagnostics remain disabled.
### Known Issues
- Useful graph variables have not yet been confirmed in-game; log review from
the manual test sequence is required.
- Candidate variable failures will only prove that those names/types were
unavailable through the current read-by-name API, not that the underlying
graph has no equivalent state.
- CommonLibF4 does not expose a safe graph variable name enumeration API here,
so this pass intentionally uses curated read-by-name probes only.
### Rollback Note
- If the helper causes a crash, severe log spam, local control issues,
performance problems, proxy movement/lifecycle regressions, or unexpected
network send behavior, set `kEnableLocalPlayerAnimationGraphReadOnlyDebug`
back to `false` or remove the single helper call from `plugin/src/main.cpp`.
### Next Steps
- Run the manual F4SE test sequence and record which graph candidates are
available, unavailable, or transition with sneak/movement/jump/weapon state.
- If no safe graph reads produce useful values, investigate another read-only
route such as animation graph event observation research or known Fallout 4
graph variable references before planning any write/event milestone.
---
## 2026-06-03 - Animation Graph Cache ID Read-Only Investigation
### Summary
Added a local-player-only numeric animation graph cache ID diagnostic pass that
uses CommonLibF4 read-only virtual getter overloads to probe cache IDs from
`0` through `GetGraphVariableCacheSize() - 1`.
### Files Changed
- `plugin/src/F4TLocalAnimationGraphDebug.cpp`
- `docs/dev-log.md`
### Details
- Added `kEnableLocalPlayerAnimationGraphCacheIdReadOnlyDebug = true` as a
separate gate from the existing local-player read-only debug gate.
- Kept the existing read-by-name candidate probing intact.
- Numeric probing only uses:
`GetGraphVariableImpl(std::uint32_t, bool&)`,
`GetGraphVariableImpl(std::uint32_t, std::int32_t&)`, and
`GetGraphVariableImpl(std::uint32_t, float&)`.
- `GetGraphVariableCacheSize()` is the strict upper bound, so a cache size of
`64` probes only IDs `0` through `63`.
- Tracks per-ID/per-type availability and last value, logs initial readable
values once, and logs later values only when they change.
- Change logs include local movement, sprint, sneak, jump, weapon, speed,
actor stance, and force-sneak context.
- Added compact candidate buckets for sneak bool/int IDs, movement float IDs,
weapon bool/int IDs, jump bool/int IDs, and sprint bool/int IDs.
- Candidate logs intentionally use cautious wording such as candidate IDs and
movement-speed-related hints. No permanent graph variable names are claimed.
- This remains local-player-only. No proxy actor graph reads were added.
- No graph writes, graph events, `NotifyAnimationGraphImpl`, `PerformAction`,
raw memory reads, cache walking, guessed offsets, `ActorState` writes,
networking changes, protocol changes, Python server changes, fake-client
changes, proxy movement changes, proxy lifecycle changes, proxy
neutralization changes, or placed fallback behavior changes were added.
### Testing
- Ran `xmake build` from `plugin`; it succeeded and linked
`Fallout4Together.dll`.
- Linter diagnostics for `plugin/src/F4TLocalAnimationGraphDebug.cpp` reported
no errors.
- Manual Fallout 4 / F4SE testing is still pending:
- Launch through F4SE.
- `coc F4TTestCell01`.
- Confirm startup logs show graph manager availability, cache size, and
readable cache ID summary.
- Toggle local sneak on/off and check for transition logs plus related
numeric cache ID changes.
- Move/stop, sprint if safe, jump, and draw/holster weapon if safe.
- Optionally add one fake client and confirm runtime proxy spawning/movement
still works while proxy graph cache probing remains absent.
### Observations To Record After Manual Testing
- Cache size observed: not yet observed in-game for this milestone.
- Readable bool ID count: not yet observed in-game.
- Readable int ID count: not yet observed in-game.
- Readable float ID count: not yet observed in-game.
- Candidate IDs discovered: none recorded yet; requires manual log review.
### Known Issues
- Runtime usefulness is unknown until the F4SE manual test pass records actual
cache size, readable counts, and candidate IDs.
- If all numeric cache ID reads return unavailable, this path will only confirm
that the safe numeric read API is not useful for the local player in that
state; it does not justify raw memory probing or graph writes.
### Rollback Note
- If the cache ID diagnostic causes a crash, severe log spam, performance
drops, local control issues, proxy movement regressions, network cadence
changes, or unexpected outgoing transform JSON changes, set
`kEnableLocalPlayerAnimationGraphCacheIdReadOnlyDebug` to `false` or revert
this milestone.
### Next Steps
- Run the manual F4SE test sequence and record cache size, readable ID counts,
and any candidate sneak/movement/weapon/jump/sprint IDs from
`Fallout4Together.log`.
---
## 2026-06-03 - Animation Graph Cache ID Investigation Rollback
### Summary
Disabled numeric animation graph cache ID probing by default after a Fallout 4
crash during read-only testing. Safer local animation graph diagnostics remain
enabled.
### Files Changed
- `plugin/src/F4TLocalAnimationGraphDebug.cpp`
- `docs/dev-log.md`
### Details
- Set `kEnableLocalPlayerAnimationGraphCacheIdReadOnlyDebug = false` with a
crash-safety comment above the gate.
- Left numeric cache ID investigation code in place behind the disabled gate for
future manual single-ID experiments.
- No networking, protocol, Python server, fake-client, proxy, or
`main.cpp` changes.
### Animation Graph Cache ID Investigation Result
Numeric cache ID probing through `GetGraphVariableImpl(id, bool/int/float&)` was tested as a read-only investigation path. Fallout 4 crashed during/after the test, while the server and fake client continued running. The numeric cache ID path has been disabled by default.
The existing local animation graph debug helper remains active for safer diagnostics: local state transitions, graph manager availability, graph cache size, graph transition state, and read-by-name candidate probing.
Future graph investigation should avoid full cache scans and instead test one known/suspected variable or ID at a time behind a stricter manual gate.
### Testing
- Ran `xmake build` from `plugin`; build succeeded.
- Manual F4SE retest pending: `coc F4TTestCell01`, toggle sneak, move/jump/weapon,
add one fake client. Expect `Local animation graph debug:` logs without
`Local animation graph cache debug:` lines and no crash.
### Known Issues
- Full-cache numeric ID probing is treated as unsafe until single-ID manual
gates exist.
### Next Steps
- Run the manual F4SE test sequence to confirm no crash and that safer local
animation debug logs still appear.
---
## Entry Template ## Entry Template
Use this format for future updates: Use this format for future updates:
@@ -0,0 +1,23 @@
#pragma once
#include <chrono>
namespace RE
{
class PlayerCharacter;
}
namespace F4T::LocalAnimationGraphDebug
{
// Game-thread-only, read-only diagnostics for discovering local player
// animation graph variables before any proxy visual animation writes.
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);
}
+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 "F4TNetworking.h"
#include "F4TLocalAnimationGraphDebug.h"
#include "F4TProxyActorController.h" #include "F4TProxyActorController.h"
#include "RE/A/ACTOR_STANCE.h" #include "RE/A/ACTOR_STANCE.h"
@@ -399,6 +400,15 @@ namespace
const auto currentTime = std::chrono::steady_clock::now(); const auto currentTime = std::chrono::steady_clock::now();
const auto currentMovementState = const auto currentMovementState =
GetPlayerMovementState(*player, hasLastTransform ? &lastSampleTransform : nullptr, lastSampleTime, currentTransform, currentTime); 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) { if (!hasLastTransform) {
lastSentTransform = currentTransform; lastSentTransform = currentTransform;
lastSampleTransform = currentTransform; lastSampleTransform = currentTransform;