Implement proxy weapon animation sync for ranged guns
Milestone 1: Proxy actors now equip remote players' right-hand weapons and play armed idle/locomotion poses when weaponDrawn=true. Adds rightHand weapon slot to equippedItems protocol, weapon graph variable support (iSyncGunDown: 0=drawn, 1=holstered), and weapon FSM events (weaponDraw, readyStateEnter, gunDownStateEnter). Fixes two critical animation state churn bugs: frustum-visibility flicker no longer resets animation state on every update tick, and idle proxies no longer register as changed on every frame. Includes WeaponBehavior graph loading via AIProcess::RequestLoadAnimationsForWeaponChange() and manual weapon attachment fallback. New docs/weapon-animation-sync.md reference guide maps animation events and variables from F4-Animation-Research. Experimental alert-state reassertion for armed proxies to test NPC combat state gating of weapon-ready poses.
This commit is contained in:
@@ -46,6 +46,15 @@ namespace
|
||||
"bInJumpState",
|
||||
"WeaponDrawn",
|
||||
"bWeaponDrawn",
|
||||
// Weapon animation candidates (RaiderRootBehavior, WeaponBehavior)
|
||||
"iSyncGunDown",
|
||||
"iRifleDrawnStateID",
|
||||
"RifleDrawnCurrentState",
|
||||
"iAttackState",
|
||||
"isFiring",
|
||||
"iSyncFireState",
|
||||
"IsAttackReady",
|
||||
"GunGripPointer",
|
||||
});
|
||||
|
||||
struct LocalAnimationGraphState
|
||||
|
||||
@@ -136,6 +136,8 @@ namespace
|
||||
constexpr auto kRuntimeProxyVisibilityNudgeInterval = 250ms;
|
||||
constexpr float kRuntimeProxyForceVisibilityRange = 8192.0F;
|
||||
constexpr auto kProxyNeutralizationInterval = 2s;
|
||||
constexpr bool kEnableProxyWeaponAlertExperiment = true;
|
||||
constexpr auto kProxyWeaponAlertReassertInterval = 750ms;
|
||||
constexpr bool kEnableProxyAIMovementIntentSuppression = false;
|
||||
bool g_preserveProxyAIForAnimationTesting = true;
|
||||
// Stage 1 tested RE::Actor::SetSneaking(bool). It compiled and did not
|
||||
@@ -2118,6 +2120,62 @@ namespace
|
||||
nullptr);
|
||||
}
|
||||
|
||||
bool UnequipSyncedWeapon(
|
||||
RE::ActorEquipManager& a_equipManager,
|
||||
RE::Actor& a_proxy)
|
||||
{
|
||||
RE::BGSEquipIndex rightHandIndex;
|
||||
rightHandIndex.index = 0; // Right-hand equip slot
|
||||
RE::BGSObjectInstance equippedItem{ nullptr, nullptr };
|
||||
const auto* item = a_proxy.GetEquippedItem(&equippedItem, rightHandIndex);
|
||||
if (!item || !item->object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return a_equipManager.UnequipObject(
|
||||
std::addressof(a_proxy),
|
||||
&equippedItem,
|
||||
1,
|
||||
nullptr,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
bool EquipSyncedWeapon(
|
||||
RE::ActorEquipManager& a_equipManager,
|
||||
RE::Actor& a_proxy,
|
||||
RE::TESObjectWEAP& a_weapon)
|
||||
{
|
||||
// First, add the weapon to the proxy's inventory so EquipObject can find it.
|
||||
// Use an empty ExtraDataList so the item has no special properties.
|
||||
a_proxy.AddObjectToContainer(
|
||||
&a_weapon,
|
||||
RE::BSTSmartPointer<RE::ExtraDataList>{},
|
||||
1,
|
||||
nullptr,
|
||||
RE::ITEM_REMOVE_REASON::kNone);
|
||||
|
||||
// Now equip it. The system should find it in the inventory we just added it to.
|
||||
RE::BGSObjectInstance objectInstance{ &a_weapon, nullptr };
|
||||
const bool equipSuccess = a_equipManager.EquipObject(
|
||||
std::addressof(a_proxy),
|
||||
objectInstance,
|
||||
0,
|
||||
1,
|
||||
nullptr,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false);
|
||||
|
||||
return equipSuccess;
|
||||
}
|
||||
|
||||
void ResetSlotEquipmentTracking(ProxyActorSlot& a_slot)
|
||||
{
|
||||
a_slot.hasAppliedEquipmentState = false;
|
||||
@@ -2178,6 +2236,11 @@ namespace
|
||||
changed = UnequipSyncedArmor(*equipManager, a_proxy, *bipedObject) || changed;
|
||||
}
|
||||
|
||||
// Clear right-hand weapon
|
||||
if (UnequipSyncedWeapon(*equipManager, a_proxy)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
RefreshProxyAfterEquipmentChange(a_slot, a_proxy);
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
@@ -2204,29 +2267,51 @@ namespace
|
||||
return;
|
||||
}
|
||||
|
||||
auto* equipManager = RE::ActorEquipManager::GetSingleton();
|
||||
if (!equipManager) {
|
||||
LogThrottledWarning(
|
||||
"proxy_equipment_manager_missing_" + std::to_string(a_remotePlayer.playerId),
|
||||
std::format(
|
||||
"Could not apply synced equipment for remote player {}; ActorEquipManager unavailable.",
|
||||
a_remotePlayer.playerId));
|
||||
return;
|
||||
auto* equipManager = RE::ActorEquipManager::GetSingleton();
|
||||
if (!equipManager) {
|
||||
LogThrottledWarning(
|
||||
"proxy_equipment_manager_missing_" + std::to_string(a_remotePlayer.playerId),
|
||||
std::format(
|
||||
"Could not apply synced equipment for remote player {}; ActorEquipManager unavailable.",
|
||||
a_remotePlayer.playerId));
|
||||
return;
|
||||
}
|
||||
|
||||
// Log received equippedItems for diagnostics
|
||||
{
|
||||
std::string itemsDebug;
|
||||
for (const auto& item : a_remotePlayer.equippedItems) {
|
||||
if (!itemsDebug.empty()) itemsDebug += ", ";
|
||||
itemsDebug += std::format("{}={:08X}", item.slot, item.formId);
|
||||
}
|
||||
LogThrottledInfo(
|
||||
"proxy_equip_received_items_" + std::to_string(a_remotePlayer.playerId),
|
||||
std::format(
|
||||
"ApplyProxyEquipmentFromRemoteState for remote player {}: received=[{}]",
|
||||
a_remotePlayer.playerId,
|
||||
itemsDebug),
|
||||
1s);
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
bool weaponVisualAttached = false;
|
||||
std::vector<std::uint32_t> targetFormIds;
|
||||
for (const auto& item : a_remotePlayer.equippedItems) {
|
||||
// Skip rightHand weapon slot - handle separately below
|
||||
if (item.slot == "rightHand") {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
std::vector<std::uint32_t> targetFormIds;
|
||||
for (const auto& item : a_remotePlayer.equippedItems) {
|
||||
const auto bipedObject = GetSyncedBipedObjectForSlot(item.slot);
|
||||
if (!bipedObject) {
|
||||
LogThrottledWarning(
|
||||
"proxy_equipment_unknown_slot_" + item.slot,
|
||||
std::format(
|
||||
"Ignoring equipment slot '{}' for remote player {}; slot is not tracked.",
|
||||
item.slot,
|
||||
a_remotePlayer.playerId));
|
||||
continue;
|
||||
}
|
||||
const auto bipedObject = GetSyncedBipedObjectForSlot(item.slot);
|
||||
if (!bipedObject) {
|
||||
LogThrottledWarning(
|
||||
"proxy_equipment_unknown_slot_" + item.slot,
|
||||
std::format(
|
||||
"Ignoring equipment slot '{}' for remote player {}; slot is not tracked.",
|
||||
item.slot,
|
||||
a_remotePlayer.playerId));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* currentBipedObject = GetEquippedArmorBipedObject(a_proxy, *bipedObject);
|
||||
const auto currentFormId =
|
||||
@@ -2284,11 +2369,104 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
// Handle right-hand weapon
|
||||
std::uint32_t rightHandWeaponFormId = 0;
|
||||
for (const auto& item : a_remotePlayer.equippedItems) {
|
||||
if (item.slot == "rightHand") {
|
||||
rightHandWeaponFormId = item.formId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check current right-hand weapon
|
||||
RE::BGSEquipIndex rightHandIndex;
|
||||
rightHandIndex.index = 0;
|
||||
RE::BGSObjectInstance currentEquippedItem{ nullptr, nullptr };
|
||||
const auto* currentItem = a_proxy.GetEquippedItem(¤tEquippedItem, rightHandIndex);
|
||||
const auto currentWeaponFormId =
|
||||
currentItem && currentItem->object && currentItem->object->Is(RE::ENUM_FORM_ID::kWEAP)
|
||||
? currentItem->object->GetFormID()
|
||||
: 0U;
|
||||
|
||||
// Unequip if different
|
||||
if (currentWeaponFormId != 0 && currentWeaponFormId != rightHandWeaponFormId) {
|
||||
if (UnequipSyncedWeapon(*equipManager, a_proxy)) {
|
||||
changed = true;
|
||||
// Revert animation graphs when unequipping weapon
|
||||
if (a_proxy.currentProcess) {
|
||||
a_proxy.currentProcess->RequestLoadAnimationsForWeaponChange(a_proxy);
|
||||
}
|
||||
} else {
|
||||
LogThrottledWarning(
|
||||
"proxy_weapon_unequip_failed_" + std::to_string(a_remotePlayer.playerId),
|
||||
std::format(
|
||||
"ActorEquipManager failed to unequip weapon {:08X} on proxy {:08X} for remote player {}.",
|
||||
currentWeaponFormId,
|
||||
a_proxy.GetFormID(),
|
||||
a_remotePlayer.playerId));
|
||||
}
|
||||
}
|
||||
|
||||
// Equip new weapon if specified
|
||||
if (rightHandWeaponFormId != 0 && currentWeaponFormId != rightHandWeaponFormId) {
|
||||
auto* weapon = RE::TESForm::GetFormByID<RE::TESObjectWEAP>(rightHandWeaponFormId);
|
||||
if (!weapon) {
|
||||
LogThrottledWarning(
|
||||
"proxy_weapon_form_missing_" + std::to_string(a_remotePlayer.playerId) + "_" + std::to_string(rightHandWeaponFormId),
|
||||
std::format(
|
||||
"Could not apply weapon form {:08X} for remote player {}; no local WEAP form resolved.",
|
||||
rightHandWeaponFormId,
|
||||
a_remotePlayer.playerId));
|
||||
} else {
|
||||
const bool equipSuccess = EquipSyncedWeapon(*equipManager, a_proxy, *weapon);
|
||||
LogThrottledInfo(
|
||||
"proxy_weapon_equip_attempt_" + std::to_string(a_remotePlayer.playerId) + "_" + std::to_string(rightHandWeaponFormId),
|
||||
std::format(
|
||||
"EquipSyncedWeapon for remote player {}: weaponFormId={:08X}, proxyActor={:08X}, result={}.",
|
||||
a_remotePlayer.playerId,
|
||||
rightHandWeaponFormId,
|
||||
a_proxy.GetFormID(),
|
||||
equipSuccess),
|
||||
1s);
|
||||
if (equipSuccess) {
|
||||
changed = true;
|
||||
RE::BGSEquipIndex equipIndex;
|
||||
equipIndex.index = 0;
|
||||
RE::BGSObjectInstanceT<RE::TESObjectWEAP> weaponInstance{ weapon, nullptr };
|
||||
a_proxy.HandleItemEquip(false);
|
||||
a_proxy.AttachWeapon(weaponInstance, equipIndex);
|
||||
a_proxy.DoReparentWeapon(weapon, equipIndex, a_remotePlayer.weaponDrawn);
|
||||
a_proxy.DrawWeaponMagicHands(a_remotePlayer.weaponDrawn);
|
||||
a_proxy.Update3DPosition(true);
|
||||
|
||||
// Request load of WeaponBehavior animation graph for weapon events
|
||||
if (a_proxy.currentProcess) {
|
||||
a_proxy.currentProcess->RequestLoadAnimationsForWeaponChange(a_proxy);
|
||||
}
|
||||
|
||||
weaponVisualAttached = true;
|
||||
} else {
|
||||
LogThrottledWarning(
|
||||
"proxy_weapon_equip_failed_" + std::to_string(a_remotePlayer.playerId) + "_" + std::to_string(rightHandWeaponFormId),
|
||||
std::format(
|
||||
"ActorEquipManager failed to equip weapon form {:08X} on proxy {:08X} for remote player {}.",
|
||||
rightHandWeaponFormId,
|
||||
a_proxy.GetFormID(),
|
||||
a_remotePlayer.playerId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a_slot.hasAppliedEquipmentState = true;
|
||||
a_slot.lastAppliedEquipmentItems = a_remotePlayer.equippedItems;
|
||||
|
||||
if (changed) {
|
||||
RefreshProxyAfterEquipmentChange(a_slot, a_proxy);
|
||||
if (weaponVisualAttached) {
|
||||
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
|
||||
a_slot.hasLoggedInitialAnimationState = false;
|
||||
} else {
|
||||
RefreshProxyAfterEquipmentChange(a_slot, a_proxy);
|
||||
}
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Applied synced equipment to runtime proxy actor {:08X} for remote player {}: slots={}, equips={}.",
|
||||
a_proxy.GetFormID(),
|
||||
@@ -2795,12 +2973,18 @@ namespace
|
||||
}
|
||||
if (a_slot.lastObservedProxyVisible.has_value() && !wasVisible) {
|
||||
a_slot.restoredForCurrentState = false;
|
||||
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
|
||||
a_slot.hasLoggedInitialAnimationState = false;
|
||||
// NOTE: previously this also called ResetAppliedAnimationState() here, but
|
||||
// IsVisible() toggles on frustum culling alone (no 3D/graph teardown), and it
|
||||
// was flapping far more often than the throttled log below implied (the reset
|
||||
// itself was NOT throttled). That forced every edge-triggered animation event,
|
||||
// including weaponDraw/readyStateEnter/gunDownStateEnter, to be treated as a
|
||||
// fresh "initial" application and re-fired on effectively every update tick.
|
||||
// Genuine 3D teardown/reload is already handled separately by
|
||||
// RecoverRuntimeProxyAfter3DReload, which owns resetting animation state.
|
||||
LogThrottledInfo(
|
||||
"runtime_proxy_visibility_regained_" + std::to_string(a_slot.remotePlayerId),
|
||||
std::format(
|
||||
"Runtime proxy actor {:08X} for remote player {} regained visibility after frustum cull; forcing animation re-apply.",
|
||||
"Runtime proxy actor {:08X} for remote player {} regained visibility after frustum cull.",
|
||||
a_proxy.GetFormID(),
|
||||
a_slot.remotePlayerId),
|
||||
kRuntimeProxyVisibilityDiagnosticLogInterval);
|
||||
@@ -6666,6 +6850,48 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyProxyWeaponAlertExperimentIfEnabled(
|
||||
ProxyActorSlot& a_slot,
|
||||
RE::Actor& a_proxy,
|
||||
const RemotePlayer& a_remotePlayer)
|
||||
{
|
||||
if constexpr (!kEnableProxyWeaponAlertExperiment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto actorId = a_proxy.GetFormID();
|
||||
const auto playerId = a_remotePlayer.playerId;
|
||||
const auto alertValue = a_remotePlayer.weaponDrawn ? 1 : 0;
|
||||
const auto alertStateChanged =
|
||||
!a_slot.lastObservedWeaponDrawnState ||
|
||||
*a_slot.lastObservedWeaponDrawnState != a_remotePlayer.weaponDrawn;
|
||||
|
||||
const auto commandKey = std::format(
|
||||
"runtime_proxy_weapon_alert_command_{}_{}",
|
||||
playerId,
|
||||
actorId);
|
||||
const auto shouldReassert =
|
||||
a_remotePlayer.weaponDrawn &&
|
||||
ShouldLog(commandKey, kProxyWeaponAlertReassertInterval);
|
||||
|
||||
if (!alertStateChanged && !shouldReassert) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto command = std::format("{:08X}.setalert {}", actorId, alertValue);
|
||||
RE::Console::ExecuteCommand(command.c_str());
|
||||
|
||||
LogThrottledInfo(
|
||||
"runtime_proxy_weapon_alert_experiment_" + std::to_string(playerId) + "_" + std::to_string(actorId),
|
||||
std::format(
|
||||
"Runtime proxy weapon alert experiment for remote player {}: actor={:08X}, weaponDrawn={}, command='{}'.",
|
||||
playerId,
|
||||
actorId,
|
||||
a_remotePlayer.weaponDrawn,
|
||||
command),
|
||||
kProxyNeutralizationInterval);
|
||||
}
|
||||
|
||||
void UpdateProxyAnimationStateDebug(
|
||||
ProxyActorSlot& a_slot,
|
||||
const RemotePlayer& a_remotePlayer,
|
||||
@@ -6699,6 +6925,8 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
a_remotePlayer.movementType));
|
||||
}
|
||||
|
||||
ApplyProxyWeaponAlertExperimentIfEnabled(a_slot, a_proxy, a_remotePlayer);
|
||||
|
||||
a_slot.hasLoggedInitialAnimationState = true;
|
||||
a_slot.lastObservedMovingState = a_remotePlayer.isMoving;
|
||||
a_slot.lastObservedSprintingState = a_remotePlayer.isSprinting;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "RE/B/BGSAnimationSystemUtils.h"
|
||||
#include "RE/B/BSFixedString.h"
|
||||
#include "RE/I/IAnimationGraphManagerHolder.h"
|
||||
#include "RE/T/TESObjectWEAP.h"
|
||||
#include "RE/T/TESObjectREFR.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -37,8 +38,8 @@ namespace
|
||||
constexpr bool kEnableProxyJumpSync = true;
|
||||
// Crouch is not a separate FO4 graph state; sneaking (iIsInSneak) covers it.
|
||||
constexpr bool kEnableProxyCrouchSync = false;
|
||||
// Weapon-drawn lives in the weapon wrapping behavior, not MTBehavior; needs more research.
|
||||
constexpr bool kEnableProxyWeaponDrawnSync = false;
|
||||
// Weapon-drawn now has protocol support; enable to drive WeaponBehavior when weapon equipped.
|
||||
constexpr bool kEnableProxyWeaponDrawnSync = true;
|
||||
|
||||
constexpr float kIdleSpeedThreshold = 1.0F;
|
||||
constexpr float kSpeedEpsilon = 0.5F;
|
||||
@@ -172,6 +173,12 @@ namespace
|
||||
inline constexpr const char* kJumpEnd = "jumpEnd";
|
||||
inline constexpr const char* kCrouchStart = "CrouchStart";
|
||||
inline constexpr const char* kCrouchStop = "CrouchStop";
|
||||
// Weapon behavior events (from RaiderRootBehavior / WeaponBehavior)
|
||||
inline constexpr const char* kWeaponDraw = "weaponDraw";
|
||||
inline constexpr const char* kReadyStateEnter = "readyStateEnter";
|
||||
inline constexpr const char* kGunDownStateEnter = "gunDownStateEnter";
|
||||
inline constexpr const char* kGunDownStateExit = "gunDownStateExit";
|
||||
inline constexpr const char* kFireSingle = "fireSingle";
|
||||
}
|
||||
|
||||
bool TrySendGraphEvent(RE::Actor& a_proxy, const char* a_eventName)
|
||||
@@ -190,6 +197,15 @@ namespace
|
||||
return a_holder.SetGraphVariableBool(variableName, a_value);
|
||||
}
|
||||
|
||||
bool TrySetGraphInt(
|
||||
RE::IAnimationGraphManagerHolder& a_holder,
|
||||
const char* a_variableName,
|
||||
std::int32_t a_value)
|
||||
{
|
||||
const RE::BSFixedString variableName{ a_variableName };
|
||||
return a_holder.SetGraphVariableInt(variableName, a_value);
|
||||
}
|
||||
|
||||
bool TrySetGraphFloat(
|
||||
RE::IAnimationGraphManagerHolder& a_holder,
|
||||
const char* a_variableName,
|
||||
@@ -333,13 +349,22 @@ namespace
|
||||
const auto currentTier =
|
||||
F4T::ProxyAnimationSync::ComputeLocomotionTier(a_desired.graphSpeed, a_desired.isSprinting);
|
||||
|
||||
// Tier only means something while actually moving. lastLocomotionTier is reset to
|
||||
// nullopt whenever the proxy goes idle (see caller), so checking "!lastLocomotionTier"
|
||||
// unconditionally here made every idle tick register as "changed" (constant log/graph
|
||||
// churn and spurious re-firing of edge-triggered events like weaponDraw/gunDownStateEnter
|
||||
// every single update while an idle proxy sat on screen).
|
||||
const bool tierChanged =
|
||||
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 ||
|
||||
tierChanged ||
|
||||
AreFloatsDifferent(a_state.lastGraphSpeed, a_desired.graphSpeed) ||
|
||||
AreFloatsDifferent(a_state.lastDirection, a_desired.direction);
|
||||
}
|
||||
@@ -380,9 +405,15 @@ namespace
|
||||
}
|
||||
|
||||
(void)kEnableProxySneakSync;
|
||||
(void)kEnableProxyWeaponDrawnSync;
|
||||
(void)a_desired.isSneaking;
|
||||
(void)a_desired.weaponDrawn;
|
||||
|
||||
// Write weapon draw state if enabled
|
||||
if constexpr (kEnableProxyWeaponDrawnSync) {
|
||||
RE::IAnimationGraphManagerHolder* weaponGraphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||
// iSyncGunDown: 0 = ready/drawn, 1 = holstered/gun-down
|
||||
const std::int32_t gunDownValue = a_desired.weaponDrawn ? 0 : 1;
|
||||
success &= TrySetGraphInt(*weaponGraphHolder, F4T::ProxyAnimationSync::GraphVar::kSyncGunDown, gunDownValue);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
static std::unordered_set<std::uint32_t> loggedWriteFailures;
|
||||
@@ -427,6 +458,15 @@ namespace
|
||||
return ApplyJumpStateToGraph(*graphHolder, true);
|
||||
}
|
||||
|
||||
// The descriptor snapshot initializes every bool/int not explicitly assigned below
|
||||
// to false/0. That is fine for unarmed locomotion, but while a weapon is drawn it
|
||||
// clobbers weapon-state variables and the proxy drops back to default idle/holster
|
||||
// immediately after the draw animation starts. Use targeted writes for armed states
|
||||
// so unknown weapon graph state is preserved.
|
||||
if (a_desired.weaponDrawn) {
|
||||
return ApplyDesiredStateToGraph(a_proxy, a_desired, a_remotePlayerId);
|
||||
}
|
||||
|
||||
auto& descriptor = F4T::AnimationDescriptor::F4AnimationDescriptor::GetHumanoidDescriptor();
|
||||
F4T::AnimationDescriptor::AnimationVariableSnapshot snapshot;
|
||||
|
||||
@@ -600,6 +640,63 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
// Handle weapon drawn state transitions
|
||||
if constexpr (kEnableProxyWeaponDrawnSync) {
|
||||
// Only fire weapon events if a weapon is actually equipped on the proxy
|
||||
RE::BGSEquipIndex rightHandIndex;
|
||||
rightHandIndex.index = 0;
|
||||
RE::BGSObjectInstance equippedItem{ nullptr, nullptr };
|
||||
const auto* equippedWeapon = a_proxy.GetEquippedItem(&equippedItem, rightHandIndex);
|
||||
const bool hasWeaponEquipped = equippedWeapon && equippedWeapon->object && equippedWeapon->object->Is(RE::ENUM_FORM_ID::kWEAP);
|
||||
|
||||
// DIAGNOSTIC: weapon FSM events (weaponDraw/readyStateEnter/gunDownStateEnter) have
|
||||
// been rejected (graphAccepted=false) on every observed attempt. Log the loaded
|
||||
// behavior graph count once per proxy so we can tell whether WeaponBehavior is ever
|
||||
// actually attached (root-only graphs won't recognize these event names at all).
|
||||
if (hasWeaponEquipped) {
|
||||
static std::unordered_set<std::uint32_t> loggedWeaponGraphDiag;
|
||||
if (loggedWeaponGraphDiag.insert(a_remotePlayerId).second) {
|
||||
RE::BSTSmartPointer<RE::BSAnimationGraphManager> graphManager;
|
||||
if (static_cast<RE::IAnimationGraphManagerHolder&>(a_proxy).GetAnimationGraphManagerImpl(graphManager) && graphManager) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy weapon graph DIAGNOSTIC for remote player {}: actor={:08X}, weaponFormId={:08X}, loadedGraphCount={}, boundChannelCount={}.",
|
||||
a_remotePlayerId,
|
||||
actorId,
|
||||
equippedWeapon->object->GetFormID(),
|
||||
graphManager->graph.size(),
|
||||
graphManager->boundChannel.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWeaponEquipped && a_desired.weaponDrawn) {
|
||||
RE::BGSEquipIndex equipIndex;
|
||||
equipIndex.index = 0;
|
||||
auto* weapon = static_cast<RE::TESObjectWEAP*>(equippedWeapon->object);
|
||||
RE::BGSObjectInstanceT<RE::TESObjectWEAP> weaponInstance{ weapon, nullptr };
|
||||
a_proxy.AttachWeapon(weaponInstance, equipIndex);
|
||||
a_proxy.DoReparentWeapon(weapon, equipIndex, true);
|
||||
a_proxy.DrawWeaponMagicHands(true);
|
||||
}
|
||||
|
||||
if (hasWeaponEquipped && (!wasInitialized || a_previous.lastWeaponDrawn != a_desired.weaponDrawn)) {
|
||||
RE::BGSEquipIndex equipIndex;
|
||||
equipIndex.index = 0;
|
||||
auto* weapon = static_cast<RE::TESObjectWEAP*>(equippedWeapon->object);
|
||||
RE::BGSObjectInstanceT<RE::TESObjectWEAP> weaponInstance{ weapon, nullptr };
|
||||
a_proxy.DoReparentWeapon(weapon, equipIndex, a_desired.weaponDrawn);
|
||||
a_proxy.DrawWeaponMagicHands(a_desired.weaponDrawn);
|
||||
if (a_desired.weaponDrawn) {
|
||||
// Weapon is drawn - enter ready state
|
||||
fireEvent(GraphEvent::kWeaponDraw);
|
||||
fireEvent(GraphEvent::kReadyStateEnter);
|
||||
} else {
|
||||
// Weapon is holstered - enter gun-down state
|
||||
fireEvent(GraphEvent::kGunDownStateEnter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (kEnableProxySneakSync) {
|
||||
if (!wasInitialized || a_previous.lastIsSneaking != a_desired.isSneaking) {
|
||||
fireEvent(a_desired.isSneaking ? GraphEvent::kSneakStart : GraphEvent::kSneakStop);
|
||||
|
||||
+28
-3
@@ -215,18 +215,43 @@ namespace
|
||||
return bipedObject->parent.object->GetFormID();
|
||||
}
|
||||
|
||||
std::vector<RemoteEquippedItem> CaptureLocalEquippedItems(const RE::PlayerCharacter& a_player)
|
||||
std::uint32_t GetEquippedWeaponFormId(RE::Actor& a_actor, RE::BGSEquipIndex a_equipIndex)
|
||||
{
|
||||
RE::BGSObjectInstance objectInstance{ nullptr, nullptr };
|
||||
const auto* item = a_actor.GetEquippedItem(&objectInstance, a_equipIndex);
|
||||
|
||||
if (!item || !item->object) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!item->object->Is(RE::ENUM_FORM_ID::kWEAP)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return item->object->GetFormID();
|
||||
}
|
||||
|
||||
std::vector<RemoteEquippedItem> CaptureLocalEquippedItems(RE::PlayerCharacter& a_player)
|
||||
{
|
||||
std::vector<RemoteEquippedItem> equippedItems;
|
||||
equippedItems.reserve(kSyncedEquipmentSlots.size());
|
||||
equippedItems.reserve(kSyncedEquipmentSlots.size() + 1); // +1 for rightHand weapon
|
||||
|
||||
for (const auto& slot : kSyncedEquipmentSlots) {
|
||||
equippedItems.push_back({
|
||||
std::string{ slot.name },
|
||||
GetEquippedArmorFormId(a_player, slot.bipedObject)
|
||||
GetEquippedArmorFormId(const_cast<const RE::PlayerCharacter&>(a_player), slot.bipedObject)
|
||||
});
|
||||
}
|
||||
|
||||
// Capture right-hand weapon
|
||||
RE::BGSEquipIndex rightHandEquipIndex;
|
||||
rightHandEquipIndex.index = 0; // Right-hand equip index
|
||||
const auto rightHandWeaponFormId = GetEquippedWeaponFormId(a_player, rightHandEquipIndex);
|
||||
equippedItems.push_back({
|
||||
"rightHand",
|
||||
rightHandWeaponFormId
|
||||
});
|
||||
|
||||
return equippedItems;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user