Improve runtime proxy 3D recovery and head refresh

Make proxy 3D/visibility handling more robust to avoid headless A-poses and corrupted movement after PlaceAtMe spawns or 3D reloads. Added helpers to check mesh readiness, queue targeted 3D updates, and refresh head/face nodes; introduce settle/wait timing (2s default, 12s max) and recovery state tracking on proxy slots. Retain proxies for prior exterior cells when appropriate, reset/clear additional slot flags on lifecycle changes, and block puppet updates while proxies are settling. Adjusted animation sync and puppet logic to require visible/loaded meshes before applying motion feedback. Added related includes and expanded logging to aid diagnostics.
This commit is contained in:
2026-06-16 16:36:43 +12:00
parent 73d44a828b
commit cd3679e548
3 changed files with 292 additions and 66 deletions
+274 -58
View File
@@ -13,10 +13,12 @@
#include "RE/A/ACTOR_STANCE.h" #include "RE/A/ACTOR_STANCE.h"
#include "RE/A/ActorValue.h" #include "RE/A/ActorValue.h"
#include "RE/B/bhkCharacterController.h" #include "RE/B/bhkCharacterController.h"
#include "RE/A/AIProcess.h"
#include "RE/C/COMMAND_TYPE.h" #include "RE/C/COMMAND_TYPE.h"
#include "RE/C/Console.h" #include "RE/C/Console.h"
#include "RE/H/hkVector4.h" #include "RE/H/hkVector4.h"
#include "RE/N/NEW_REFR_DATA.h" #include "RE/N/NEW_REFR_DATA.h"
#include "RE/M/MiddleHighProcessData.h"
#include "RE/P/ProcessLists.h" #include "RE/P/ProcessLists.h"
#include "RE/P/PTYPE.h" #include "RE/P/PTYPE.h"
#include "RE/R/RESET_3D_FLAGS.h" #include "RE/R/RESET_3D_FLAGS.h"
@@ -100,6 +102,7 @@ namespace
constexpr bool kDebugPlaceAtMeSpawnUseCustomProxyBase = true; constexpr bool kDebugPlaceAtMeSpawnUseCustomProxyBase = true;
constexpr bool kDebugPlaceAtMeSpawnFallbackToCodsworth = true; constexpr bool kDebugPlaceAtMeSpawnFallbackToCodsworth = true;
constexpr float kPlaceAtMeProxySettleSeconds = 2.0F; constexpr float kPlaceAtMeProxySettleSeconds = 2.0F;
constexpr float kPlaceAtMeProxyMaxSettleSeconds = 12.0F;
constexpr float kPlaceAtMeProxyCandidateSearchSeconds = 5.0F; constexpr float kPlaceAtMeProxyCandidateSearchSeconds = 5.0F;
constexpr auto kRuntimeProxyDebugForwardDistance = 250.0F; constexpr auto kRuntimeProxyDebugForwardDistance = 250.0F;
constexpr auto kRuntimeProxyDebugFallbackOffsetX = 200.0F; constexpr auto kRuntimeProxyDebugFallbackOffsetX = 200.0F;
@@ -303,6 +306,10 @@ namespace
bool proxyObservedLoaded3D{ false }; bool proxyObservedLoaded3D{ false };
bool proxyPending3DRecovery{ false }; bool proxyPending3DRecovery{ false };
bool proxy3DResetQueued{ false }; bool proxy3DResetQueued{ false };
bool proxy3DRecoverySettling{ false };
std::chrono::steady_clock::time_point proxy3DRecoverySettleStartTime{};
bool headMeshRefreshAttempted{ false };
bool headMeshPendingStable{ false };
bool skipSmoothFrameOnce{ false }; bool skipSmoothFrameOnce{ false };
std::optional<std::uint32_t> cachedProxyFormId; std::optional<std::uint32_t> cachedProxyFormId;
RE::NiPointer<RE::Actor> cachedProxyActor; RE::NiPointer<RE::Actor> cachedProxyActor;
@@ -509,6 +516,13 @@ namespace
} }
void ResetRemoteMovementStateTracking(); void ResetRemoteMovementStateTracking();
bool HasValidRemoteTransform(const RemotePlayer& a_remotePlayer);
void RestoreSlotProxyForRemotePlayer(
ProxyActorSlot& a_slot,
RE::Actor& a_proxy,
const RemotePlayer& a_remotePlayer,
const RE::PlayerCharacter& a_player,
bool a_sameCellPassed);
void UpdateProxyAnimationStateDebug( void UpdateProxyAnimationStateDebug(
ProxyActorSlot& a_slot, ProxyActorSlot& a_slot,
const RemotePlayer& a_remotePlayer, const RemotePlayer& a_remotePlayer,
@@ -1863,12 +1877,105 @@ namespace
a_proxy.GetCurrent3D() != nullptr; a_proxy.GetCurrent3D() != nullptr;
} }
// Partial Get3D without skin/head causes headless A-poses if movement or animation // Partial Get3D without IsVisible causes headless A-poses if movement or animation
// runs too early (especially on the second concurrent PlaceAtMe spawn). // runs too early (especially after PlaceAtMe spawn or load-range reload).
bool ProxyMeshReadyForPresentation(const RE::Actor& a_proxy) bool ProxyMeshReadyForPresentation(const RE::Actor& a_proxy)
{ {
return a_proxy.GetFullyLoaded3D() != nullptr || if (!a_proxy.IsVisible()) {
(a_proxy.Get3D() != nullptr && a_proxy.IsVisible()); return false;
}
return a_proxy.GetFullyLoaded3D() != nullptr || a_proxy.Get3D() != nullptr;
}
bool ShouldRetainProxyInPriorLocalCell(
std::uint32_t a_playerId,
const RE::TESObjectCELL& a_proxyCell,
RE::TESObjectCELL& a_currentCell,
const RE::PlayerCharacter& a_player)
{
if (!a_currentCell.IsExterior() || !a_proxyCell.IsExterior()) {
return false;
}
for (const auto& remotePlayer : F4T::RemotePlayerState::GetRemotePlayerSnapshot()) {
if (remotePlayer.playerId != a_playerId || !HasValidRemoteTransform(remotePlayer)) {
continue;
}
if (!IsRemotePlayerInSameLocation(remotePlayer, a_player, a_currentCell)) {
continue;
}
const auto remoteCellId = ParseHexFormId(remotePlayer.cellId);
if (remoteCellId && *remoteCellId == a_proxyCell.GetFormID()) {
return true;
}
// Remote is in same worldspace but a different exterior cell; the proxy may
// legitimately live in the cell the local player just left.
return true;
}
return false;
}
std::uint32_t GetProxyHumanoidUpdate3DFlags()
{
return static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kModel) |
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkin) |
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kHead) |
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kFace) |
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkeleton) |
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kInitDefault);
}
std::uint32_t GetProxyHeadUpdate3DFlags()
{
return static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kHead) |
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kFace);
}
std::uint32_t GetProxyVisibilityUpdate3DFlags()
{
return static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kModel) |
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkin) |
GetProxyHeadUpdate3DFlags();
}
void QueueProxyUpdate3D(RE::Actor& a_proxy, std::uint32_t a_flags)
{
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kModel);
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kSkin);
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kHead);
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kFace);
if ((a_flags & static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkeleton)) != 0) {
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kSkeleton);
}
if ((a_flags & static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kInitDefault)) != 0) {
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kInitDefault);
}
if (auto* taskQueue = RE::TaskQueueInterface::GetSingleton()) {
taskQueue->QueueUpdate3D(std::addressof(a_proxy), a_flags);
}
}
bool ProxyHeadNodesMissing(const RE::Actor& a_proxy)
{
const auto* npc = a_proxy.GetNPC();
if (!npc || npc->numHeadParts <= 0) {
return false;
}
if (const auto* process = a_proxy.currentProcess) {
if (const auto* middleHigh = process->middleHigh) {
return middleHigh->headNode == nullptr && middleHigh->faceNodeSkinned == nullptr;
}
}
return true;
} }
void RefreshRuntimeProxyVisibility(RE::Actor& a_proxy) void RefreshRuntimeProxyVisibility(RE::Actor& a_proxy)
@@ -1885,19 +1992,43 @@ namespace
if (ProxyHasLoaded3D(a_proxy)) { if (ProxyHasLoaded3D(a_proxy)) {
a_proxy.MoveHavok(true); a_proxy.MoveHavok(true);
} }
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kModel);
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kSkin);
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kSkeleton);
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kInitDefault);
if (auto* taskQueue = RE::TaskQueueInterface::GetSingleton()) { QueueProxyUpdate3D(a_proxy, GetProxyVisibilityUpdate3DFlags());
const auto updateFlags = }
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kModel) |
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkin) | void EnsureProxyHeadMesh(ProxyActorSlot& a_slot, RE::Actor& a_proxy)
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkeleton) | {
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kInitDefault); if (auto* process = a_proxy.currentProcess) {
taskQueue->QueueUpdate3D(std::addressof(a_proxy), updateFlags); if (auto* middleHigh = process->middleHigh) {
if (middleHigh->headDeferredHideLimb) {
middleHigh->headDeferredHideLimb = nullptr;
}
}
} }
// Head/face only — kSkeleton/kInitDefault reset the behavior graph and break
// locomotion sync if run during or after restore.
QueueProxyUpdate3D(a_proxy, GetProxyHeadUpdate3DFlags());
a_slot.headMeshRefreshAttempted = true;
a_slot.headMeshPendingStable = true;
a_slot.restoredForCurrentState = false;
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
a_slot.hasLoggedInitialAnimationState = false;
const auto* process = a_proxy.currentProcess;
const auto* middleHigh = process ? process->middleHigh : nullptr;
LogThrottledInfo(
"runtime_proxy_head_mesh_refresh_" + std::to_string(a_slot.remotePlayerId),
std::format(
"Runtime proxy head mesh refresh for remote player {}: actor={:08X}, headNode={}, faceNodeSkinned={}, Get3D={}, IsVisible={}.",
a_slot.remotePlayerId,
a_proxy.GetFormID(),
middleHigh && middleHigh->headNode != nullptr,
middleHigh && middleHigh->faceNodeSkinned != nullptr,
a_proxy.Get3D() != nullptr,
a_proxy.IsVisible()),
kRuntimeProxyVisibilityDiagnosticLogInterval);
} }
void LogRuntimeProxyVisibilityState(RE::Actor& a_proxy, std::string_view a_context) void LogRuntimeProxyVisibilityState(RE::Actor& a_proxy, std::string_view a_context)
@@ -1933,52 +2064,59 @@ namespace
ProxyActorSlot& a_slot, ProxyActorSlot& a_slot,
RE::Actor& a_proxy, RE::Actor& a_proxy,
const RemotePlayer& a_remotePlayer, const RemotePlayer& a_remotePlayer,
const RE::PlayerCharacter& a_player) const RE::PlayerCharacter& a_player,
bool a_sameCellPassed)
{ {
if (!a_slot.proxy3DResetQueued) { if (!ProxyHasLoaded3D(a_proxy)) {
RefreshRuntimeProxyVisibility(a_proxy);
a_proxy.Reset3D(true, 0, true, 0);
a_slot.proxy3DResetQueued = true;
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor {:08X} for remote player {} queued 3D recovery after mesh reload (Reset3D).",
a_proxy.GetFormID(),
a_slot.remotePlayerId));
}
if (a_slot.lifecycleState == ProxyLifecycleState::kActive) {
const auto targetPosition =
GetRuntimeProxyVisibleTargetPosition(a_slot, a_remotePlayer, a_player);
a_slot.targetPosition = targetPosition;
a_slot.targetHeading = a_remotePlayer.angleZ;
F4T::ProxyGameAPI::PositionRemoteActor(
a_proxy,
targetPosition,
a_remotePlayer.angleZ,
true);
a_slot.skipSmoothFrameOnce = true;
}
const bool meshReady =
a_proxy.GetFullyLoaded3D() != nullptr ||
(a_proxy.Get3D() != nullptr && a_proxy.IsVisible());
if (!meshReady) {
return; return;
} }
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState); if (!a_slot.proxy3DResetQueued) {
EnsureProxyHeadMesh(a_slot, a_proxy);
a_slot.proxy3DResetQueued = true;
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor {:08X} for remote player {} queued 3D recovery after mesh reload (head/face refresh).",
a_proxy.GetFormID(),
a_slot.remotePlayerId));
return;
}
if (!ProxyMeshReadyForPresentation(a_proxy)) {
return;
}
if (!a_slot.proxy3DRecoverySettling) {
a_slot.proxy3DRecoverySettling = true;
a_slot.proxy3DRecoverySettleStartTime = std::chrono::steady_clock::now();
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
F4T::ProxyPuppet::SetProxyLocomotionState(
a_proxy.GetFormID(),
-1.0F,
a_slot.puppetDirection);
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor {:08X} for remote player {} entered 3D recovery settle; deferring movement for {:.1f}s.",
a_proxy.GetFormID(),
a_slot.remotePlayerId,
kPlaceAtMeProxySettleSeconds));
return;
}
const auto elapsedSeconds = std::chrono::duration<float>(
std::chrono::steady_clock::now() - a_slot.proxy3DRecoverySettleStartTime).count();
if (elapsedSeconds < kPlaceAtMeProxySettleSeconds) {
return;
}
a_slot.proxyPending3DRecovery = false;
a_slot.proxy3DResetQueued = false;
a_slot.proxy3DRecoverySettling = false;
a_slot.visibilityRefreshPending = false;
a_slot.restoredForCurrentState = false;
a_slot.hasLoggedInitialAnimationState = false; a_slot.hasLoggedInitialAnimationState = false;
a_slot.hasAppliedVisualSneakingState = false; a_slot.hasAppliedVisualSneakingState = false;
a_slot.hasLoggedVisualSneakDisabled = false; a_slot.hasLoggedVisualSneakDisabled = false;
a_slot.puppetGraphSpeed = -1.0F;
F4T::ProxyPuppet::SetProxyLocomotionState(
a_proxy.GetFormID(),
-1.0F,
a_slot.puppetDirection);
UpdateProxyAnimationStateDebug(a_slot, a_remotePlayer, a_proxy); RestoreSlotProxyForRemotePlayer(a_slot, a_proxy, a_remotePlayer, a_player, a_sameCellPassed);
a_slot.proxyPending3DRecovery = false;
a_slot.proxy3DResetQueued = false;
a_slot.visibilityRefreshPending = false;
LogInfoWithLocalPlayerPrefix(std::format( LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor {:08X} for remote player {} finished 3D recovery: Get3D={}, GetFullyLoaded3D={}, IsVisible={}.", "Runtime proxy actor {:08X} for remote player {} finished 3D recovery: Get3D={}, GetFullyLoaded3D={}, IsVisible={}.",
@@ -1993,13 +2131,18 @@ namespace
ProxyActorSlot& a_slot, ProxyActorSlot& a_slot,
RE::Actor& a_proxy, RE::Actor& a_proxy,
const RemotePlayer& a_remotePlayer, const RemotePlayer& a_remotePlayer,
const RE::PlayerCharacter& a_player) const RE::PlayerCharacter& a_player,
bool a_sameCellPassed)
{ {
const bool has3D = ProxyHasLoaded3D(a_proxy); const bool has3D = ProxyHasLoaded3D(a_proxy);
if (a_slot.proxyObservedLoaded3D && !has3D) { if (a_slot.proxyObservedLoaded3D && !has3D) {
a_slot.proxyPending3DRecovery = true; a_slot.proxyPending3DRecovery = true;
a_slot.proxy3DResetQueued = false; a_slot.proxy3DResetQueued = false;
a_slot.proxy3DRecoverySettling = false;
a_slot.restoredForCurrentState = false;
a_slot.headMeshRefreshAttempted = false;
a_slot.headMeshPendingStable = false;
a_slot.visibilityRefreshPending = true; a_slot.visibilityRefreshPending = true;
a_slot.visibilityRefreshLogged = false; a_slot.visibilityRefreshLogged = false;
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState); F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
@@ -2017,7 +2160,7 @@ namespace
} }
if (has3D && a_slot.proxyPending3DRecovery) { if (has3D && a_slot.proxyPending3DRecovery) {
RecoverRuntimeProxyAfter3DReload(a_slot, a_proxy, a_remotePlayer, a_player); RecoverRuntimeProxyAfter3DReload(a_slot, a_proxy, a_remotePlayer, a_player, a_sameCellPassed);
} }
a_slot.proxyObservedLoaded3D = has3D; a_slot.proxyObservedLoaded3D = has3D;
@@ -4122,11 +4265,12 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
{ {
const auto has3D = a_actor.Get3D() != nullptr; const auto has3D = a_actor.Get3D() != nullptr;
const auto isVisible = a_actor.IsVisible(); const auto isVisible = a_actor.IsVisible();
if (!has3D || !isVisible) { if (!ProxyMeshReadyForPresentation(a_actor)) {
LogWarningWithLocalPlayerPrefix(std::format( LogWarningWithLocalPlayerPrefix(std::format(
"Stage 4 runtime proxy for remote player {} settled with partial visibility: Get3D={}, IsVisible={}. Continuing to remote movement.", "Stage 4 runtime proxy for remote player {} promoted with partial visibility: Get3D={}, GetFullyLoaded3D={}, IsVisible={}. Continuing to remote movement.",
a_slot.remotePlayerId, a_slot.remotePlayerId,
has3D, has3D,
a_actor.GetFullyLoaded3D() != nullptr,
isVisible)); isVisible));
} }
@@ -4159,6 +4303,9 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
if (auto* player = RE::PlayerCharacter::GetSingleton()) { if (auto* player = RE::PlayerCharacter::GetSingleton()) {
NeutralizeProxyActor(a_slot, a_actor, *player, ProxyNeutralizationReason::kSpawnPromoted); NeutralizeProxyActor(a_slot, a_actor, *player, ProxyNeutralizationReason::kSpawnPromoted);
if (ProxyHeadNodesMissing(a_actor)) {
EnsureProxyHeadMesh(a_slot, a_actor);
}
EnsureRuntimeProxyVisibility(a_slot, a_actor, "placeatme-promoted"); EnsureRuntimeProxyVisibility(a_slot, a_actor, "placeatme-promoted");
} }
LogInfoWithLocalPlayerPrefix(std::format( LogInfoWithLocalPlayerPrefix(std::format(
@@ -4270,6 +4417,27 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
return true; return true;
} }
if (!ProxyMeshReadyForPresentation(*actor)) {
if (elapsedSeconds < kPlaceAtMeProxyMaxSettleSeconds) {
LogThrottledInfo(
"stage4_placeatme_proxy_waiting_for_visibility_" + std::to_string(a_slot.remotePlayerId),
std::format(
"Stage 4 runtime proxy for remote player {} is waiting for visible 3D before promotion: Get3D={}, GetFullyLoaded3D={}, IsVisible={}, settleElapsed={:.1f}s.",
a_slot.remotePlayerId,
actor->Get3D() != nullptr,
actor->GetFullyLoaded3D() != nullptr,
actor->IsVisible(),
elapsedSeconds),
kRuntimeProxyVisibilityDiagnosticLogInterval);
return true;
}
LogWarningWithLocalPlayerPrefix(std::format(
"Stage 4 runtime proxy for remote player {} exceeded max settle wait ({:.1f}s) without visible 3D; promoting anyway.",
a_slot.remotePlayerId,
kPlaceAtMeProxyMaxSettleSeconds));
}
PromoteSlotPlaceAtMeProxy(a_slot, *actor); PromoteSlotPlaceAtMeProxy(a_slot, *actor);
return false; return false;
} }
@@ -5236,7 +5404,10 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_slot.proxyObservedLoaded3D = false; a_slot.proxyObservedLoaded3D = false;
a_slot.proxyPending3DRecovery = false; a_slot.proxyPending3DRecovery = false;
a_slot.proxy3DResetQueued = false; a_slot.proxy3DResetQueued = false;
a_slot.proxy3DRecoverySettling = false;
a_slot.lastObservedRemoteCellId.reset(); a_slot.lastObservedRemoteCellId.reset();
a_slot.headMeshRefreshAttempted = false;
a_slot.headMeshPendingStable = false;
} }
void ResetSlotVisualSneakTracking(ProxyActorSlot& a_slot) void ResetSlotVisualSneakTracking(ProxyActorSlot& a_slot)
@@ -5372,6 +5543,25 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
continue; continue;
} }
if (ShouldRetainProxyInPriorLocalCell(playerId, *proxyCell, a_currentCell, a_player)) {
slot.restoredForCurrentState = false;
slot.visibilityRefreshPending = true;
slot.visibilityRefreshLogged = false;
slot.proxyObservedLoaded3D = ProxyHasLoaded3D(*proxy);
if (!ProxyMeshReadyForPresentation(*proxy)) {
slot.proxyPending3DRecovery = true;
slot.proxy3DResetQueued = false;
slot.proxy3DRecoverySettling = false;
}
LogInfoWithLocalPlayerPrefix(std::format(
"Retained runtime proxy {:08X} for remote player {} in prior local cell {:08X} after local player moved to cell {:08X}; proxy stays at remote location for same-worldspace exterior play.",
proxy->GetFormID(),
playerId,
a_oldCellId,
a_currentCell.GetFormID()));
continue;
}
const auto releasedFormId = proxy->GetFormID(); const auto releasedFormId = proxy->GetFormID();
ClearSlotVisualSneakIfApplied(slot, *proxy, "local cell change"); ClearSlotVisualSneakIfApplied(slot, *proxy, "local cell change");
ClearSlotProxyAnimationIfApplied(slot, *proxy, "local cell change"); ClearSlotProxyAnimationIfApplied(slot, *proxy, "local cell change");
@@ -5401,6 +5591,9 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
slot.proxyObservedLoaded3D = false; slot.proxyObservedLoaded3D = false;
slot.proxyPending3DRecovery = false; slot.proxyPending3DRecovery = false;
slot.proxy3DResetQueued = false; slot.proxy3DResetQueued = false;
slot.proxy3DRecoverySettling = false;
slot.headMeshRefreshAttempted = false;
slot.headMeshPendingStable = false;
slot.visibilityRefreshPending = true; slot.visibilityRefreshPending = true;
slot.visibilityRefreshLogged = false; slot.visibilityRefreshLogged = false;
g_dynamicProxyPool.erase(playerId); g_dynamicProxyPool.erase(playerId);
@@ -5983,7 +6176,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
EnsureRuntimeProxyVisibility(a_slot, *proxy, "active-refresh"); EnsureRuntimeProxyVisibility(a_slot, *proxy, "active-refresh");
} }
MaintainRuntimeProxy3DState(a_slot, *proxy, a_remotePlayer, a_player); MaintainRuntimeProxy3DState(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
if (a_slot.proxyPending3DRecovery) { if (a_slot.proxyPending3DRecovery) {
return true; return true;
@@ -6005,6 +6198,20 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
return true; return true;
} }
if (ProxyHeadNodesMissing(*proxy)) {
if (!a_slot.headMeshRefreshAttempted) {
EnsureProxyHeadMesh(a_slot, *proxy);
}
return true;
}
if (a_slot.headMeshPendingStable) {
a_slot.headMeshPendingStable = false;
a_slot.restoredForCurrentState = false;
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
a_slot.hasLoggedInitialAnimationState = false;
}
const bool needsSnapRestore = const bool needsSnapRestore =
ShouldSnapRemoteMovement(a_remotePlayer.movementType) || remoteCellChanged; ShouldSnapRemoteMovement(a_remotePlayer.movementType) || remoteCellChanged;
@@ -6320,6 +6527,15 @@ namespace F4T::ProxyActorController
}; };
for (const auto& [_, slot] : g_proxySlots) { for (const auto& [_, slot] : g_proxySlots) {
const bool puppetBlocked =
slot.proxyPending3DRecovery ||
slot.proxy3DRecoverySettling ||
slot.spawnState == PlaceAtMeBackedProxySpawnState::kSettling ||
IsPlaceAtMeBackedProxyAttempting(slot.spawnState);
if (puppetBlocked) {
continue;
}
appendActorHandleFormId(slot.proxyHandle); appendActorHandleFormId(slot.proxyHandle);
appendActorHandleFormId(slot.pendingSpawnHandle); appendActorHandleFormId(slot.pendingSpawnHandle);
if (slot.cachedProxyFormId && if (slot.cachedProxyFormId &&
+5 -2
View File
@@ -99,8 +99,11 @@ namespace
bool ProxyMeshReadyForPresentation(const RE::Actor& a_proxy) bool ProxyMeshReadyForPresentation(const RE::Actor& a_proxy)
{ {
return a_proxy.GetFullyLoaded3D() != nullptr || if (!a_proxy.IsVisible()) {
(a_proxy.Get3D() != nullptr && a_proxy.IsVisible()); return false;
}
return a_proxy.GetFullyLoaded3D() != nullptr || a_proxy.Get3D() != nullptr;
} }
bool CanWriteProxyGraph(RE::Actor& a_proxy, std::uint32_t a_remotePlayerId, std::uint32_t a_actorId) bool CanWriteProxyGraph(RE::Actor& a_proxy, std::uint32_t a_remotePlayerId, std::uint32_t a_actorId)
+13 -6
View File
@@ -156,11 +156,15 @@ namespace F4T::ProxyPuppet
return; return;
} }
// Driving motion feedback while the mesh is unloaded corrupts PlaceAtMe // Driving motion feedback while the mesh is unloaded or only partially loaded
// proxies on reload (A-pose, missing head). Wait for 3D recovery instead. // corrupts PlaceAtMe proxies on reload (A-pose, missing head). Wait for a
if (!a_this->Get3D() && !a_this->GetFullyLoaded3D()) { // fully loaded mesh before UpdateNoAI / motion feedback runs.
return; const bool meshReadyForPuppet =
} a_this->IsVisible() &&
(a_this->GetFullyLoaded3D() != nullptr || a_this->Get3D() != nullptr);
if (!meshReadyForPuppet) {
return;
}
const auto locomotion = GetProxyLocomotionState(a_this->GetFormID()); const auto locomotion = GetProxyLocomotionState(a_this->GetFormID());
@@ -257,7 +261,10 @@ namespace F4T::ProxyPuppet
return; return;
} }
if (!a_actor.Get3D() && !a_actor.GetFullyLoaded3D()) { const bool meshReadyForPuppet =
a_actor.IsVisible() &&
(a_actor.GetFullyLoaded3D() != nullptr || a_actor.Get3D() != nullptr);
if (!meshReadyForPuppet) {
return; return;
} }