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:
+17
-3
@@ -32,7 +32,7 @@ both send and receive paths:
|
||||
12. For each eligible remote `playerId`, the controller maintains a
|
||||
`remotePlayerId -> runtime proxy slot` mapping. Each slot stores its
|
||||
`ObjectRefHandle`, lifecycle state, PlaceAtMe-backed spawn/candidate state,
|
||||
movement smoothing state, and sneak observation state. Stage 4 caps runtime
|
||||
movement smoothing state, and data-only animation debug state. Stage 4 caps runtime
|
||||
proxy slots at four actors for testing.
|
||||
13. Runtime proxies are demand-driven. A slot executes the Stage 3.7
|
||||
PlaceAtMe-backed spawn flow only when that remote player needs a same-cell
|
||||
@@ -53,8 +53,9 @@ both send and receive paths:
|
||||
actor.
|
||||
15. Proxy visual updates are independent per slot. Normal movement is smoothed
|
||||
toward that player’s latest target, while `cell_change`, `worldspace_change`,
|
||||
and `teleport` snap directly. Sneak state is still observed and logged only;
|
||||
no visual crouch/sneak application is attempted.
|
||||
and `teleport` snap directly. Movement and animation-related remote state is
|
||||
still observed and logged only; no visual crouch/sneak, jump, weapon drawn,
|
||||
locomotion, animation graph, or actor-state application is attempted.
|
||||
16. When a remote player leaves the cell or disconnects, only that player’s
|
||||
runtime proxy is moved to the hidden holding position inside
|
||||
`F4TTestCell01`. Runtime actors are not disabled or deleted. Connected
|
||||
@@ -118,6 +119,15 @@ reused.
|
||||
The placed proxy reference remains as a single fallback path if runtime spawning
|
||||
fails for one selected remote player.
|
||||
|
||||
Current animation-state handling is data/debug only:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
RemotePlayerState[RemotePlayerState] --> ProxyActorSlot[ProxyActorSlot animation debug state]
|
||||
ProxyActorSlot --> Logs[Initial and transition logs]
|
||||
ProxyActorSlot --> FutureVisualAnimation[Future visual animation application]
|
||||
```
|
||||
|
||||
## Main Components
|
||||
|
||||
### F4SE Plugin
|
||||
@@ -153,6 +163,9 @@ The native plugin is responsible for:
|
||||
path after spawn promotion, during reusable-slot reassignment, periodically
|
||||
while active, and while held so they remain passive visual puppets rather than
|
||||
independent combat actors
|
||||
- Logging initial and transition-only data/debug animation state per resolved
|
||||
runtime proxy slot from `RemotePlayerState`, including moving, speed bucket,
|
||||
sprinting, sneaking, jumping, weapon drawn, and movement type
|
||||
- Suppressing exposed AI movement intent for runtime proxies by clearing safe
|
||||
process target handles and using validated package interruption/do-nothing
|
||||
calls when combat, pathing, flee/alarm, bump, or package movement state is
|
||||
@@ -237,6 +250,7 @@ Game update attempts at most one PlaceAtMe-backed runtime actor spawn at a time
|
||||
Game update snapshots matching candidates, isolates the new actor, and waits for the settle window
|
||||
Game update moves isolated Stage 4 runtime spawn candidates to the remote player's current transform
|
||||
Game update stores promoted runtime actors as ObjectRefHandle values in slots
|
||||
Game update logs data-only animation debug state for resolved runtime proxy slots
|
||||
Game update neutralizes resolved runtime proxy actors after spawn promotion, reuse, active throttled checks, and holding
|
||||
Game update suppresses exposed AI movement/pathing intent without forcing animation graph events
|
||||
Game update moves each valid proxy from its assigned remote player state
|
||||
|
||||
@@ -2257,6 +2257,63 @@ starting near the local player before snapping away.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03 - Proxy Animation State Debug Pass
|
||||
|
||||
### Summary
|
||||
|
||||
Added data-only per-slot animation state diagnostics for runtime proxy actors so
|
||||
the proxy controller can observe movement and animation-related state assigned to
|
||||
each remote `playerId` without visually applying animation.
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `plugin/src/F4TProxyActorController.cpp`
|
||||
- `docs/architecture.md`
|
||||
- `docs/dev-log.md`
|
||||
|
||||
### Details
|
||||
|
||||
- Added per-runtime-proxy-slot debug tracking for moving, speed bucket,
|
||||
sprinting, sneaking, jumping, weapon drawn, and movement type.
|
||||
- Added transition-only runtime proxy animation state logs that include the
|
||||
remote `playerId` and resolved proxy actor FormID.
|
||||
- Movement speed diagnostics use coarse buckets (`idle`, `slow`, `walk`, `run`,
|
||||
`sprint`) so small float changes do not spam the log.
|
||||
- Reset animation debug state when slots are created, reassigned, held, or pass
|
||||
through the existing slot observation reset helper.
|
||||
- Kept this pass data/debug only. It does not apply visual animation, force
|
||||
animation graph events, write animation graph variables, call `PerformAction`,
|
||||
write `ActorState`, or add package/pathing/AI movement changes.
|
||||
- Networking, protocol, Python server code, fake-client tooling, placed fallback
|
||||
behavior, spawn-at-remote-position, movement smoothing, snap behavior,
|
||||
same-cell gating, held/reusable lifecycle, and combat/AI neutralization were
|
||||
unchanged.
|
||||
|
||||
### Testing
|
||||
|
||||
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
|
||||
`Fallout4Together.dll`.
|
||||
- Pending manual validation: launch through F4SE, start the server, add fake
|
||||
clients in `F4TTestCell01`, and confirm per-player initial and transition
|
||||
animation state logs without visual animation changes.
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Speed bucket thresholds are conservative diagnostics and still need tuning
|
||||
against real Fallout 4 movement units before any future visual locomotion work.
|
||||
- Visual animation application remains intentionally unimplemented.
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Investigate safe visual sneak/crouch application.
|
||||
- Investigate weapon drawn visual application.
|
||||
- Investigate locomotion animation graph behavior.
|
||||
- Investigate jump animation behavior.
|
||||
- Research safe animation graph variable/event reads or writes before any visual
|
||||
animation milestone.
|
||||
|
||||
---
|
||||
|
||||
## Entry Template
|
||||
|
||||
Use this format for future updates:
|
||||
|
||||
@@ -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 (*a_slot.lastObservedSneakState == a_remotePlayer.isSneaking) {
|
||||
return;
|
||||
}
|
||||
|
||||
a_slot.lastObservedSneakState = a_remotePlayer.isSneaking;
|
||||
if (shouldLogInitial || changed) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Remote player {} sneak state changed: isSneaking={}",
|
||||
"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_remotePlayer.isSneaking));
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user