Add Pip-Boy state to transform sync

Adds optional `isPipboyOpen` to transform packets, remote player state, proxy animation logging, and fake client/player samples. Updates protocol and architecture docs to describe the new Pip-Boy open/raising/closing state and related movement-state handling.
This commit is contained in:
2026-06-30 16:00:07 +12:00
parent 4b8f5246c4
commit 4ed671464e
18 changed files with 196 additions and 19 deletions
+5 -1
View File
@@ -723,6 +723,7 @@ namespace
ReadBool(a_packet, "isJumping").value_or(false),
ReadBool(a_packet, "isCrouching").value_or(false),
ReadBool(a_packet, "weaponDrawn").value_or(false),
ReadBool(a_packet, "isPipboyOpen").value_or(false),
static_cast<float>(movementSpeed),
static_cast<float>(animationGraphSpeed),
hasEquipmentUpdate,
@@ -755,7 +756,7 @@ namespace
if (ShouldLogRemoteMovementState(*playerId)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Remote player {} movement state: moving={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, speed={:.1f}, flags1={:08X}, flags2={:08X}",
"Remote player {} movement state: moving={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, pipboyOpen={}, speed={:.1f}, flags1={:08X}, flags2={:08X}",
remoteState.playerId,
remoteState.isMoving,
remoteState.isSprinting,
@@ -763,6 +764,7 @@ namespace
remoteState.isJumping,
remoteState.isCrouching,
remoteState.weaponDrawn,
remoteState.isPipboyOpen,
remoteState.movementSpeed,
remoteState.actorStateFlags1,
remoteState.actorStateFlags2));
@@ -1083,6 +1085,7 @@ namespace F4T::Networking
bool a_isJumping,
bool a_isCrouching,
bool a_weaponDrawn,
bool a_isPipboyOpen,
float a_movementSpeed,
float a_animationGraphSpeed,
std::span<const F4T::RemotePlayerState::RemoteEquippedItem> a_equippedItems,
@@ -1109,6 +1112,7 @@ namespace F4T::Networking
{ "isJumping", a_isJumping },
{ "isCrouching", a_isCrouching },
{ "weaponDrawn", a_weaponDrawn },
{ "isPipboyOpen", a_isPipboyOpen },
{ "movementSpeed", a_movementSpeed }
};
+6 -1
View File
@@ -360,6 +360,7 @@ namespace
std::optional<bool> lastObservedSneakingState;
std::optional<bool> lastObservedJumpingState;
std::optional<bool> lastObservedWeaponDrawnState;
std::optional<bool> lastObservedPipboyOpenState;
std::optional<std::string> lastObservedMovementType;
std::optional<std::string> lastObservedRemoteCellId;
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
@@ -6052,6 +6053,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_slot.lastObservedSneakingState.reset();
a_slot.lastObservedJumpingState.reset();
a_slot.lastObservedWeaponDrawnState.reset();
a_slot.lastObservedPipboyOpenState.reset();
a_slot.lastObservedMovementType.reset();
a_slot.lastObservedMovementSpeedBucket.reset();
ResetSlotVisualSneakTracking(a_slot);
@@ -6462,12 +6464,13 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
!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.lastObservedPipboyOpenState || *a_slot.lastObservedPipboyOpenState != a_remotePlayer.isPipboyOpen ||
!a_slot.lastObservedMovementType || *a_slot.lastObservedMovementType != a_remotePlayer.movementType ||
!a_slot.lastObservedMovementSpeedBucket || *a_slot.lastObservedMovementSpeedBucket != speedBucket;
if (shouldLogInitial || changed) {
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy animation state {} for remote player {}: actor={:08X}, holdingIndex={}, moving={}, speed={:.1f}, speedBucket={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, movementType={}.",
"Runtime proxy animation state {} for remote player {}: actor={:08X}, holdingIndex={}, moving={}, speed={:.1f}, speedBucket={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, pipboyOpen={}, movementType={}.",
shouldLogInitial ? "initial" : "changed",
a_remotePlayer.playerId,
a_proxy.GetFormID(),
@@ -6479,6 +6482,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_remotePlayer.isSneaking,
a_remotePlayer.isJumping,
a_remotePlayer.weaponDrawn,
a_remotePlayer.isPipboyOpen,
a_remotePlayer.movementType));
}
@@ -6488,6 +6492,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_slot.lastObservedSneakingState = a_remotePlayer.isSneaking;
a_slot.lastObservedJumpingState = a_remotePlayer.isJumping;
a_slot.lastObservedWeaponDrawnState = a_remotePlayer.weaponDrawn;
a_slot.lastObservedPipboyOpenState = a_remotePlayer.isPipboyOpen;
a_slot.lastObservedMovementType = a_remotePlayer.movementType;
a_slot.lastObservedMovementSpeedBucket = speedBucket;
+13 -4
View File
@@ -12,7 +12,6 @@
#include "RE/T/TESObjectREFR.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
@@ -257,6 +256,7 @@ namespace
bool isJumping{ false };
bool isCrouching{ false };
bool weaponDrawn{ false };
bool isPipboyOpen{ false };
float graphSpeed{ 0.0F };
float direction{ 0.0F };
};
@@ -273,6 +273,7 @@ namespace
desired.isJumping = a_remotePlayer.isJumping;
desired.isCrouching = a_remotePlayer.isCrouching;
desired.weaponDrawn = a_remotePlayer.weaponDrawn;
desired.isPipboyOpen = a_remotePlayer.isPipboyOpen;
desired.graphSpeed = ComputeGraphSpeed(a_remotePlayer);
desired.direction = a_remotePlayer.angleZ;
return desired;
@@ -333,13 +334,18 @@ namespace
const auto currentTier =
F4T::ProxyAnimationSync::ComputeLocomotionTier(a_desired.graphSpeed, a_desired.isSprinting);
const auto locomotionTierChanged =
a_desired.isMoving &&
(!a_state.lastLocomotionTier || *a_state.lastLocomotionTier != currentTier);
return a_state.lastIsMoving != a_desired.isMoving ||
a_state.lastIsSprinting != a_desired.isSprinting ||
a_state.lastIsSneaking != a_desired.isSneaking ||
a_state.lastIsJumping != a_desired.isJumping ||
a_state.lastIsCrouching != a_desired.isCrouching ||
a_state.lastWeaponDrawn != a_desired.weaponDrawn ||
!a_state.lastLocomotionTier || *a_state.lastLocomotionTier != currentTier ||
a_state.lastIsPipboyOpen != a_desired.isPipboyOpen ||
locomotionTierChanged ||
AreFloatsDifferent(a_state.lastGraphSpeed, a_desired.graphSpeed) ||
AreFloatsDifferent(a_state.lastDirection, a_desired.direction);
}
@@ -630,7 +636,7 @@ namespace
}
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, tier={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, graphSpeed={:.1f}, direction={:.3f}.",
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, tier={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, pipboyOpen={}, graphSpeed={:.1f}, direction={:.3f}.",
a_initial ? "initial" : "changed",
a_remotePlayerId,
a_actorId,
@@ -641,6 +647,7 @@ namespace
a_desired.isJumping,
a_desired.isCrouching,
a_desired.weaponDrawn,
a_desired.isPipboyOpen,
a_desired.graphSpeed,
a_desired.direction));
}
@@ -735,11 +742,12 @@ namespace F4T::ProxyAnimationSync
const auto haveSpeedSmoothed =
diagHolder->GetGraphVariableImplFloat(RE::BSFixedString{ F4T::ProxyAnimationSync::GraphVar::kSpeedSmoothed }, readSpeedSmoothed);
LogInfoWithLocalPlayerPrefix(std::format(
"Proxy anim diag p{} actor={:08X}: recvMoving={}, recvSprint={}, recvMovementSpeed={:.1f}, recvAnimGraphSpeed={:.1f}, computedGraphSpeed={:.1f}, graphSpeedReadBack={}({:.1f}), speedSmoothedReadBack={}({:.1f}).",
"Proxy anim diag p{} actor={:08X}: recvMoving={}, recvSprint={}, recvPipboyOpen={}, recvMovementSpeed={:.1f}, recvAnimGraphSpeed={:.1f}, computedGraphSpeed={:.1f}, graphSpeedReadBack={}({:.1f}), speedSmoothedReadBack={}({:.1f}).",
a_remotePlayer.playerId,
actorId,
a_remotePlayer.isMoving,
a_remotePlayer.isSprinting,
a_remotePlayer.isPipboyOpen,
a_remotePlayer.movementSpeed,
a_remotePlayer.animationGraphSpeed,
desired.graphSpeed,
@@ -780,6 +788,7 @@ namespace F4T::ProxyAnimationSync
a_state.lastIsJumping = desired.isJumping;
a_state.lastIsCrouching = desired.isCrouching;
a_state.lastWeaponDrawn = desired.weaponDrawn;
a_state.lastIsPipboyOpen = desired.isPipboyOpen;
a_state.lastGraphSpeed = desired.graphSpeed;
a_state.lastDirection = desired.direction;
if (desired.isJumping) {
+30 -1
View File
@@ -14,6 +14,9 @@
#include "RE/B/BipedAnim.h"
#include "RE/B/BSFixedString.h"
#include "RE/I/IAnimationGraphManagerHolder.h"
#include "RE/P/PipboyManager.h"
#include "RE/P/PipboyMenu.h"
#include "RE/U/UI.h"
#include <array>
#include <cmath>
@@ -51,6 +54,7 @@ namespace
bool isJumping;
bool isCrouching;
bool weaponDrawn;
bool isPipboyOpen;
float movementSpeed;
};
@@ -422,6 +426,22 @@ namespace
return false;
}
bool IsLocalPipboyOpen()
{
if (const auto* ui = RE::UI::GetSingleton(); ui && ui->GetMenuOpen<RE::PipboyMenu>()) {
return true;
}
if (auto* pipboyManager = RE::PipboyManager::GetSingleton()) {
return pipboyManager->QPipboyActive() ||
pipboyManager->pipboyOpening ||
pipboyManager->pipboyClosing ||
pipboyManager->pipboyRaising;
}
return false;
}
bool ShouldLogSneakDetection(
const SneakDetectionState& a_sneakDetection,
std::chrono::steady_clock::time_point a_currentTime)
@@ -512,6 +532,7 @@ namespace
heldJumping,
GetCrouchState(a_player),
a_player.GetWeaponMagicDrawn(),
IsLocalPipboyOpen(),
a_movementSpeed
};
}
@@ -545,7 +566,8 @@ namespace
a_previous.isSneaking != a_current.isSneaking ||
a_previous.isJumping != a_current.isJumping ||
a_previous.isCrouching != a_current.isCrouching ||
a_previous.weaponDrawn != a_current.weaponDrawn;
a_previous.weaponDrawn != a_current.weaponDrawn ||
a_previous.isPipboyOpen != a_current.isPipboyOpen;
}
bool HasMeaningfulTransformDelta(const PlayerTransform& a_previous, const PlayerTransform& a_current)
@@ -726,6 +748,7 @@ namespace
debugMovementState.isJumping,
debugMovementState.isCrouching,
debugMovementState.weaponDrawn,
debugMovementState.isPipboyOpen,
debugMovementState.movementSpeed,
-1.0F,
currentEquipmentState,
@@ -818,6 +841,7 @@ namespace
networkMovementState.isJumping,
networkMovementState.isCrouching,
networkMovementState.weaponDrawn,
networkMovementState.isPipboyOpen,
networkMovementState.movementSpeed,
animationGraphSpeed,
currentEquipmentState,
@@ -839,6 +863,11 @@ namespace
} else if (hasAppearanceChanged) {
LogInfoWithLocalPlayerPrefix("Detected local appearance change. Sending immediate transform update.");
lastLogTime = currentTime;
} else if (hasMovementStateChanged && lastSentMovementState.isPipboyOpen != networkMovementState.isPipboyOpen) {
LogInfoWithLocalPlayerPrefix(std::format(
"Detected local Pip-Boy state change. Sending immediate transform update: isPipboyOpen={}.",
networkMovementState.isPipboyOpen));
lastLogTime = currentTime;
} else if (currentTime - lastLogTime >= kTransformLogInterval) {
LogPlayerTransform(currentTransform);
lastLogTime = currentTime;