Log local sneak detection and proxy sneak observer
Add diagnostics and a log-only proxy observation for sneak state changes. - docs/dev-log.md: Add entries documenting proxy sneak observation and local sneak detection diagnostics, scope notes, and next steps. - plugin/src/main.cpp: Introduce SneakDetectionState, GetSneakDetectionState, and ShouldLogSneakDetection to combine API and actor-state signals; throttle diagnostic logs; use the combined finalIsSneaking in player movement state decisions; add include for ACTOR_STANCE. - plugin/src/F4TProxyActorController.cpp: Add controller-local tracking for the represented remote player sneak state, reset helpers, and ApplyRemoteMovementStateToProxy which logs sneak transitions for the represented remote player (no visual crouch applied yet). Notes: This change is log-only — it does not force animations or ActorState writes. Networking protocol and receive-thread behavior remain unchanged. A TODO is left to safely apply visual crouch once a supported API is confirmed.
This commit is contained in:
@@ -52,6 +52,8 @@ namespace
|
||||
bool g_firstSmoothedMovementLogged = false;
|
||||
auto g_lastSafetyOffsetMovementTime = std::chrono::steady_clock::time_point{};
|
||||
std::optional<std::uint32_t> g_representedPlayerId;
|
||||
std::optional<std::uint32_t> g_lastObservedSneakPlayerId;
|
||||
std::optional<bool> g_lastObservedSneakState;
|
||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_lastWarningLogTimes;
|
||||
|
||||
using RemotePlayer = F4T::RemotePlayerState::RemotePlayerState;
|
||||
@@ -124,6 +126,12 @@ namespace
|
||||
return a_movementType == "cell_change" || a_movementType == "worldspace_change" || a_movementType == "teleport";
|
||||
}
|
||||
|
||||
void ResetRemoteMovementStateTracking()
|
||||
{
|
||||
g_lastObservedSneakPlayerId.reset();
|
||||
g_lastObservedSneakState.reset();
|
||||
}
|
||||
|
||||
bool HasEditorId(const RE::TESForm& a_form, std::string_view a_editorId)
|
||||
{
|
||||
const auto* editorId = a_form.GetFormEditorID();
|
||||
@@ -420,6 +428,7 @@ namespace
|
||||
g_representedPlayerId.reset();
|
||||
g_remoteStateMovedLogged = false;
|
||||
g_firstSmoothedMovementLogged = false;
|
||||
ResetRemoteMovementStateTracking();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,6 +460,7 @@ namespace
|
||||
g_representedPlayerId = selectedPlayer->playerId;
|
||||
g_remoteStateMovedLogged = false;
|
||||
g_firstSmoothedMovementLogged = false;
|
||||
ResetRemoteMovementStateTracking();
|
||||
if (!g_remoteStateMovementStartedLogged) {
|
||||
LogInfoWithLocalPlayerPrefix("Remote-state-driven proxy movement started.");
|
||||
g_remoteStateMovementStartedLogged = true;
|
||||
@@ -470,6 +480,36 @@ namespace
|
||||
return *selectedPlayer;
|
||||
}
|
||||
|
||||
void ApplyRemoteMovementStateToProxy(RE::Actor& a_proxy, const RemotePlayer& a_remotePlayer)
|
||||
{
|
||||
(void)a_proxy;
|
||||
|
||||
if (g_lastObservedSneakPlayerId != a_remotePlayer.playerId) {
|
||||
g_lastObservedSneakPlayerId = a_remotePlayer.playerId;
|
||||
g_lastObservedSneakState = a_remotePlayer.isSneaking;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_lastObservedSneakState) {
|
||||
g_lastObservedSneakState = a_remotePlayer.isSneaking;
|
||||
return;
|
||||
}
|
||||
|
||||
if (*g_lastObservedSneakState == a_remotePlayer.isSneaking) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_lastObservedSneakState = a_remotePlayer.isSneaking;
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Remote player {} sneak state changed: isSneaking={}",
|
||||
a_remotePlayer.playerId,
|
||||
a_remotePlayer.isSneaking));
|
||||
|
||||
// TODO: Once a safe Fallout 4/CommonLibF4 API is confirmed, apply a visual
|
||||
// crouch/sneak state here. Do not force animation graph variables/events,
|
||||
// ActorState bits, or PerformAction(kActionSneak) without that proof.
|
||||
}
|
||||
|
||||
void MoveProxyToPlayerOffset(RE::Actor& a_proxy, const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
auto targetPosition = a_player.GetPosition();
|
||||
@@ -594,6 +634,7 @@ namespace F4T::ProxyActorController
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyRemoteMovementStateToProxy(*proxy, *selectedRemotePlayer);
|
||||
MoveProxyToRemotePlayer(*proxy, *selectedRemotePlayer);
|
||||
}
|
||||
|
||||
|
||||
+69
-1
@@ -1,6 +1,8 @@
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TProxyActorController.h"
|
||||
|
||||
#include "RE/A/ACTOR_STANCE.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
@@ -35,6 +37,15 @@ namespace
|
||||
float movementSpeed;
|
||||
};
|
||||
|
||||
struct SneakDetectionState
|
||||
{
|
||||
bool apiSneaking;
|
||||
bool actorStateSneaking;
|
||||
bool actorStateForceSneak;
|
||||
std::uint32_t actorStateStance;
|
||||
bool finalIsSneaking;
|
||||
};
|
||||
|
||||
constexpr auto kTransformSendInterval = 100ms;
|
||||
constexpr auto kTransformLogInterval = 1s;
|
||||
constexpr auto kTransformSendPositionThreshold = 3.0F;
|
||||
@@ -44,6 +55,7 @@ namespace
|
||||
constexpr auto kJumpVerticalSpeedThreshold = 80.0F;
|
||||
constexpr auto kJumpStateHoldDuration = 400ms;
|
||||
constexpr auto kJumpDiagnosticLogInterval = 1s;
|
||||
constexpr auto kSneakDiagnosticLogInterval = 2s;
|
||||
|
||||
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
||||
{
|
||||
@@ -182,6 +194,51 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
SneakDetectionState GetSneakDetectionState(RE::PlayerCharacter& a_player)
|
||||
{
|
||||
const auto apiSneaking = a_player.IsSneaking();
|
||||
const auto actorStateStance = a_player.stance;
|
||||
const auto actorStateSneaking =
|
||||
actorStateStance == static_cast<std::uint32_t>(RE::ACTOR_STANCE::kSneaking);
|
||||
const auto actorStateForceSneak = a_player.forceSneak != 0;
|
||||
|
||||
return {
|
||||
apiSneaking,
|
||||
actorStateSneaking,
|
||||
actorStateForceSneak,
|
||||
actorStateStance,
|
||||
apiSneaking || actorStateSneaking || actorStateForceSneak
|
||||
};
|
||||
}
|
||||
|
||||
bool ShouldLogSneakDetection(
|
||||
const SneakDetectionState& a_sneakDetection,
|
||||
std::chrono::steady_clock::time_point a_currentTime)
|
||||
{
|
||||
static bool hasLastLoggedDetection = false;
|
||||
static SneakDetectionState lastLoggedDetection{};
|
||||
static auto lastLogTime = std::chrono::steady_clock::time_point{};
|
||||
|
||||
const auto detectionChanged = !hasLastLoggedDetection ||
|
||||
lastLoggedDetection.apiSneaking != a_sneakDetection.apiSneaking ||
|
||||
lastLoggedDetection.actorStateSneaking != a_sneakDetection.actorStateSneaking ||
|
||||
lastLoggedDetection.actorStateForceSneak != a_sneakDetection.actorStateForceSneak ||
|
||||
lastLoggedDetection.actorStateStance != a_sneakDetection.actorStateStance ||
|
||||
lastLoggedDetection.finalIsSneaking != a_sneakDetection.finalIsSneaking;
|
||||
const auto diagnosticThrottled =
|
||||
lastLogTime.time_since_epoch().count() == 0 ||
|
||||
a_currentTime - lastLogTime >= kSneakDiagnosticLogInterval;
|
||||
|
||||
if (!detectionChanged && !diagnosticThrottled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hasLastLoggedDetection = true;
|
||||
lastLoggedDetection = a_sneakDetection;
|
||||
lastLogTime = a_currentTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
PlayerMovementState GetPlayerMovementState(
|
||||
RE::PlayerCharacter& a_player,
|
||||
const PlayerTransform* a_previousSample,
|
||||
@@ -194,6 +251,7 @@ namespace
|
||||
const auto elapsedSeconds = GetSampleElapsedSeconds(a_previousSample, a_previousSampleTime, a_currentTime);
|
||||
const auto movementSpeed = GetMovementSpeed(a_previousSample, a_currentTransform, elapsedSeconds);
|
||||
const auto verticalSpeed = GetVerticalSpeed(a_previousSample, a_currentTransform, elapsedSeconds);
|
||||
const auto sneakDetection = GetSneakDetectionState(a_player);
|
||||
const auto apiJumping = a_player.IsJumping();
|
||||
const auto derivedJumping = verticalSpeed >= kJumpVerticalSpeedThreshold;
|
||||
if (apiJumping || derivedJumping) {
|
||||
@@ -209,10 +267,20 @@ namespace
|
||||
heldJumping));
|
||||
}
|
||||
|
||||
if (ShouldLogSneakDetection(sneakDetection, a_currentTime)) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Local sneak detection: apiSneaking={}, actorStateSneaking={}, actorStateForceSneak={}, actorStateStance={}, finalIsSneaking={}",
|
||||
sneakDetection.apiSneaking,
|
||||
sneakDetection.actorStateSneaking,
|
||||
sneakDetection.actorStateForceSneak,
|
||||
sneakDetection.actorStateStance,
|
||||
sneakDetection.finalIsSneaking));
|
||||
}
|
||||
|
||||
return {
|
||||
movementSpeed >= kMovingSpeedThreshold,
|
||||
a_player.DoGetSprinting(),
|
||||
a_player.IsSneaking(),
|
||||
sneakDetection.finalIsSneaking,
|
||||
heldJumping,
|
||||
a_player.GetWeaponMagicDrawn(),
|
||||
movementSpeed
|
||||
|
||||
Reference in New Issue
Block a user