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:
2026-06-03 11:51:04 +12:00
parent 44ea1b98da
commit 18379f2b28
2 changed files with 139 additions and 2 deletions
+78 -2
View File
@@ -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