Add proxy visual sneak scaffolding
Introduce default-off scaffolding for proxy visual sneak experiments: add kEnableProxyVisualSneakSync and kEnableProxyVisualSneakExperimentalGraph flags (false), include ACTOR_STANCE, and add a non-mutating ApplyProxyVisualSneakIfEnabled helper that reads proxy sneak/stance/forceSneak and emits one-time diagnostics when visual sneak is disabled. Add per-slot tracking fields (hasAppliedVisualSneakingState, hasLoggedVisualSneakDisabled, lastAppliedVisualSneakingState), ResetSlotVisualSneakTracking and calls to reset tracking when holding/reusing/disconnecting slots, and switch UpdateProxyAnimationStateDebug to accept a non-const RE::Actor&. Update docs/dev-log.md with an entry describing the change. Comments/TODOs note a future, separately approved experiment to call RE::Actor::SetSneaking(...) behind a disabled gate; current scaffolding intentionally avoids mutating actors, animation graph writes/events, or AI/package crouch logic. Build tested locally (plugin rebuild).
This commit is contained in:
@@ -2314,6 +2314,67 @@ each remote `playerId` without visually applying animation.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03 - Proxy Visual Sneak Scaffolding
|
||||
|
||||
### Summary
|
||||
|
||||
Added default-off runtime proxy visual sneak scaffolding so future sneak/crouch
|
||||
visual experiments have isolated slot-local gates and reset tracking without
|
||||
mutating actors by default.
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `plugin/src/F4TProxyActorController.cpp`
|
||||
- `docs/dev-log.md`
|
||||
|
||||
### Details
|
||||
|
||||
- Added default-off `kEnableProxyVisualSneakSync` and
|
||||
`kEnableProxyVisualSneakExperimentalGraph` flags near the existing proxy
|
||||
controller constants.
|
||||
- Added runtime-proxy-slot visual sneak tracking separate from existing
|
||||
animation debug observation state.
|
||||
- Added a runtime-proxy-only game-thread helper that reads proxy-side
|
||||
`IsSneaking()`, stance, and `forceSneak` diagnostics and logs once per slot
|
||||
when no visual sneak method is enabled.
|
||||
- Kept the helper non-mutating with the default flags. It does not call
|
||||
animation graph events, write animation graph variables, call `PerformAction`,
|
||||
write `ActorState`, or use package/procedure/AI crouch behavior.
|
||||
- Left a TODO for a separately approved future experiment using
|
||||
`RE::Actor::SetSneaking(remoteState.isSneaking)` behind the existing disabled
|
||||
gate.
|
||||
- Reset visual sneak tracking through the existing slot observation reset path,
|
||||
when runtime proxies are moved to holding, and when disconnected slots become
|
||||
reusable.
|
||||
- Left networking, protocol, Python server code, fake-client tooling, placed
|
||||
fallback behavior, runtime spawning, movement smoothing, snap behavior,
|
||||
same-cell gating, held/reusable lifecycle, and combat/AI neutralization
|
||||
unchanged.
|
||||
|
||||
### Testing
|
||||
|
||||
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
|
||||
`Fallout4Together.dll`.
|
||||
- Pending manual validation: use the Dev Server GUI to add fake clients, toggle
|
||||
sneak on/off, and confirm existing animation debug logs plus one-time disabled
|
||||
visual sneak diagnostics without any visual crouch by default.
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Visual sneak/crouch application remains intentionally unimplemented.
|
||||
- The safe visual writer candidate still needs a manually approved
|
||||
`RE::Actor::SetSneaking(...)` experiment before any proxy crouch behavior is
|
||||
enabled.
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Manually validate fake-client sneak toggles in `F4TTestCell01`.
|
||||
- If approved later, test `RE::Actor::SetSneaking(...)` behind the disabled
|
||||
visual sneak gate with explicit clear handling for false, disconnect, hold,
|
||||
reassignment, and reuse.
|
||||
|
||||
---
|
||||
|
||||
## Entry Template
|
||||
|
||||
Use this format for future updates:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TRemotePlayerState.h"
|
||||
|
||||
#include "RE/A/ACTOR_STANCE.h"
|
||||
#include "RE/A/ActorValue.h"
|
||||
#include "RE/C/COMMAND_TYPE.h"
|
||||
#include "RE/C/Console.h"
|
||||
@@ -71,6 +72,8 @@ namespace
|
||||
constexpr auto kRuntimeProxyVisibilityDiagnosticLogInterval = 2s;
|
||||
constexpr auto kProxyNeutralizationInterval = 2s;
|
||||
constexpr bool kEnableProxyAIMovementIntentSuppression = false;
|
||||
constexpr bool kEnableProxyVisualSneakSync = false;
|
||||
constexpr bool kEnableProxyVisualSneakExperimentalGraph = false;
|
||||
constexpr auto kNotInTestCellLogInterval = 30s;
|
||||
|
||||
using RemotePlayer = F4T::RemotePlayerState::RemotePlayerState;
|
||||
@@ -228,6 +231,8 @@ namespace
|
||||
bool reusableLogged{ false };
|
||||
bool directRemoteSpawnLogged{ false };
|
||||
bool hasLoggedInitialAnimationState{ false };
|
||||
bool hasAppliedVisualSneakingState{ false };
|
||||
bool hasLoggedVisualSneakDisabled{ false };
|
||||
std::optional<bool> lastObservedMovingState;
|
||||
std::optional<bool> lastObservedSprintingState;
|
||||
std::optional<bool> lastObservedSneakingState;
|
||||
@@ -235,6 +240,7 @@ namespace
|
||||
std::optional<bool> lastObservedWeaponDrawnState;
|
||||
std::optional<std::string> lastObservedMovementType;
|
||||
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
|
||||
std::optional<bool> lastAppliedVisualSneakingState;
|
||||
};
|
||||
|
||||
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
|
||||
@@ -380,7 +386,7 @@ namespace
|
||||
void UpdateProxyAnimationStateDebug(
|
||||
ProxyActorSlot& a_slot,
|
||||
const RemotePlayer& a_remotePlayer,
|
||||
const RE::Actor& a_proxy);
|
||||
RE::Actor& a_proxy);
|
||||
|
||||
const char* GetProxyNeutralizationReasonLabel(ProxyNeutralizationReason a_reason)
|
||||
{
|
||||
@@ -4060,6 +4066,13 @@ namespace
|
||||
a_slot.remoteStateMovedLogged = false;
|
||||
}
|
||||
|
||||
void ResetSlotVisualSneakTracking(ProxyActorSlot& a_slot)
|
||||
{
|
||||
a_slot.hasAppliedVisualSneakingState = false;
|
||||
a_slot.hasLoggedVisualSneakDisabled = false;
|
||||
a_slot.lastAppliedVisualSneakingState.reset();
|
||||
}
|
||||
|
||||
void ResetSlotObservationState(ProxyActorSlot& a_slot)
|
||||
{
|
||||
a_slot.hasLoggedInitialAnimationState = false;
|
||||
@@ -4070,6 +4083,7 @@ namespace
|
||||
a_slot.lastObservedWeaponDrawnState.reset();
|
||||
a_slot.lastObservedMovementType.reset();
|
||||
a_slot.lastObservedMovementSpeedBucket.reset();
|
||||
ResetSlotVisualSneakTracking(a_slot);
|
||||
}
|
||||
|
||||
bool HasSlotHeldSinceTime(const ProxyActorSlot& a_slot)
|
||||
@@ -4130,6 +4144,9 @@ namespace
|
||||
}
|
||||
|
||||
a_proxy.SetPosition(GetSlotHoldingPosition(a_slot), true);
|
||||
// TODO: If a later approved visual writer is added, clear any applied
|
||||
// proxy sneak visual here before holding/reusing the actor.
|
||||
ResetSlotVisualSneakTracking(a_slot);
|
||||
if (auto* player = RE::PlayerCharacter::GetSingleton()) {
|
||||
NeutralizeProxyActor(a_slot, a_proxy, *player, ProxyNeutralizationReason::kHolding);
|
||||
}
|
||||
@@ -4198,6 +4215,7 @@ namespace
|
||||
slot.lifecycleState = ProxyLifecycleState::kReusable;
|
||||
slot.holdReason.reset();
|
||||
slot.previousRemotePlayerId = playerId;
|
||||
ResetSlotVisualSneakTracking(slot);
|
||||
if (!slot.reusableLogged) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy slot for disconnected remote player {} became reusable after {:.1f}s held.",
|
||||
@@ -4247,10 +4265,66 @@ namespace
|
||||
ProxyMovementDiagnosticMode::kRestore);
|
||||
}
|
||||
|
||||
void ApplyProxyVisualSneakIfEnabled(
|
||||
ProxyActorSlot& a_slot,
|
||||
RE::Actor& a_proxy,
|
||||
const RemotePlayer& a_remotePlayer)
|
||||
{
|
||||
if constexpr (!kEnableProxyVisualSneakSync && !kEnableProxyVisualSneakExperimentalGraph) {
|
||||
if (a_slot.hasLoggedVisualSneakDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto proxyIsSneaking = a_proxy.IsSneaking();
|
||||
const auto proxyStance = static_cast<std::uint32_t>(a_proxy.stance);
|
||||
const auto proxyForceSneak = a_proxy.forceSneak != 0;
|
||||
const auto proxyStanceIsSneaking =
|
||||
proxyStance == static_cast<std::uint32_t>(RE::ACTOR_STANCE::kSneaking);
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy visual sneak sync disabled for remote player {}: actor={:08X}, requestedSneaking={}, proxyIsSneaking={}, proxyStance={}, proxyStanceIsSneaking={}, proxyForceSneak={}. No visual sneak method is enabled.",
|
||||
a_remotePlayer.playerId,
|
||||
a_proxy.GetFormID(),
|
||||
a_remotePlayer.isSneaking,
|
||||
proxyIsSneaking,
|
||||
proxyStance,
|
||||
proxyStanceIsSneaking,
|
||||
proxyForceSneak));
|
||||
a_slot.hasLoggedVisualSneakDisabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (kEnableProxyVisualSneakExperimentalGraph) {
|
||||
if (!a_slot.hasLoggedVisualSneakDisabled) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy visual sneak experimental graph flag is enabled for remote player {}, but graph writes/events are intentionally not implemented in this scaffolding pass. actor={:08X}.",
|
||||
a_remotePlayer.playerId,
|
||||
a_proxy.GetFormID()));
|
||||
a_slot.hasLoggedVisualSneakDisabled = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (kEnableProxyVisualSneakSync) {
|
||||
if (!a_slot.hasLoggedVisualSneakDisabled) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy visual sneak sync flag is enabled for remote player {}, but no actor-mutating sneak writer is implemented in this scaffolding pass. actor={:08X}.",
|
||||
a_remotePlayer.playerId,
|
||||
a_proxy.GetFormID()));
|
||||
a_slot.hasLoggedVisualSneakDisabled = true;
|
||||
}
|
||||
// TODO: In a separately approved experiment, consider calling
|
||||
// RE::Actor::SetSneaking(a_remotePlayer.isSneaking) here behind this
|
||||
// flag, with explicit clear handling on false, disconnect, hold,
|
||||
// reassignment, and reuse. Do not add graph writes/events,
|
||||
// PerformAction, ActorState writes, or package/procedure crouch logic.
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateProxyAnimationStateDebug(
|
||||
ProxyActorSlot& a_slot,
|
||||
const RemotePlayer& a_remotePlayer,
|
||||
const RE::Actor& a_proxy)
|
||||
RE::Actor& a_proxy)
|
||||
{
|
||||
const auto speedBucket = GetProxyAnimationSpeedBucket(a_remotePlayer);
|
||||
const auto shouldLogInitial = !a_slot.hasLoggedInitialAnimationState;
|
||||
@@ -4289,6 +4363,8 @@ namespace
|
||||
a_slot.lastObservedMovementType = a_remotePlayer.movementType;
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user