Handle runtime proxy 3D unload/recovery

Track and recover runtime proxy actors when their 3D meshes unload and reload. Adds slot flags (proxyObservedLoaded3D, proxyPending3DRecovery, proxy3DResetQueued), MaintainRuntimeProxy3DState and RecoverRuntimeProxyAfter3DReload to defer animation/puppet updates while mesh is missing, queue Reset3D and restore animation/locomotion state once fully loaded. Integrates checks throughout controller logic to skip smoothing/updates for proxies without 3D and updates slot initialization/reset. Also prevents puppet-driven motion/graph speed writes when the actor's 3D is unloaded to avoid corrupted PlaceAtMe proxies, with logging for diagnostics.
This commit is contained in:
2026-06-07 21:31:28 +12:00
parent 9b133b3109
commit 2ced73f7b9
2 changed files with 132 additions and 0 deletions
+122
View File
@@ -300,6 +300,9 @@ namespace
bool directRemoteSpawnLogged{ false }; bool directRemoteSpawnLogged{ false };
bool visibilityRefreshPending{ true }; bool visibilityRefreshPending{ true };
bool visibilityRefreshLogged{ false }; bool visibilityRefreshLogged{ false };
bool proxyObservedLoaded3D{ false };
bool proxyPending3DRecovery{ false };
bool proxy3DResetQueued{ 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 +512,10 @@ namespace
ProxyActorSlot& a_slot, ProxyActorSlot& a_slot,
const RemotePlayer& a_remotePlayer, const RemotePlayer& a_remotePlayer,
RE::Actor& a_proxy); RE::Actor& a_proxy);
RE::NiPoint3 GetRuntimeProxyVisibleTargetPosition(
ProxyActorSlot& a_slot,
const RemotePlayer& a_remotePlayer,
const RE::PlayerCharacter& a_player);
const char* GetProxyNeutralizationReasonLabel(ProxyNeutralizationReason a_reason) const char* GetProxyNeutralizationReasonLabel(ProxyNeutralizationReason a_reason)
{ {
@@ -1913,6 +1920,100 @@ namespace
} }
} }
void RecoverRuntimeProxyAfter3DReload(
ProxyActorSlot& a_slot,
RE::Actor& a_proxy,
const RemotePlayer& a_remotePlayer,
const RE::PlayerCharacter& a_player)
{
if (!a_slot.proxy3DResetQueued) {
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;
}
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
a_slot.hasLoggedInitialAnimationState = false;
a_slot.hasAppliedVisualSneakingState = 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);
a_slot.proxyPending3DRecovery = false;
a_slot.proxy3DResetQueued = false;
a_slot.visibilityRefreshPending = false;
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor {:08X} for remote player {} finished 3D recovery: Get3D={}, GetFullyLoaded3D={}, IsVisible={}.",
a_proxy.GetFormID(),
a_slot.remotePlayerId,
a_proxy.Get3D() != nullptr,
a_proxy.GetFullyLoaded3D() != nullptr,
a_proxy.IsVisible()));
}
void MaintainRuntimeProxy3DState(
ProxyActorSlot& a_slot,
RE::Actor& a_proxy,
const RemotePlayer& a_remotePlayer,
const RE::PlayerCharacter& a_player)
{
const bool has3D = ProxyHasLoaded3D(a_proxy);
if (a_slot.proxyObservedLoaded3D && !has3D) {
a_slot.proxyPending3DRecovery = true;
a_slot.proxy3DResetQueued = false;
a_slot.visibilityRefreshPending = true;
a_slot.visibilityRefreshLogged = false;
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
F4T::ProxyPuppet::SetProxyLocomotionState(
a_proxy.GetFormID(),
-1.0F,
a_slot.puppetDirection);
LogThrottledInfo(
"runtime_proxy_3d_unloaded_" + std::to_string(a_slot.remotePlayerId),
std::format(
"Runtime proxy actor {:08X} for remote player {} lost loaded 3D (likely out of load range); deferring puppet/animation until reload.",
a_proxy.GetFormID(),
a_slot.remotePlayerId),
kRuntimeProxyVisibilityDiagnosticLogInterval);
}
if (has3D && a_slot.proxyPending3DRecovery) {
RecoverRuntimeProxyAfter3DReload(a_slot, a_proxy, a_remotePlayer, a_player);
}
a_slot.proxyObservedLoaded3D = has3D;
}
bool TryDiscardInvisibleDynamicProxy(ProxyActorSlot& a_slot, RE::Actor& a_proxy) bool TryDiscardInvisibleDynamicProxy(ProxyActorSlot& a_slot, RE::Actor& a_proxy)
{ {
if constexpr (!kUsePlaceAtMeBackedRuntimeProxySpawn) { if constexpr (!kUsePlaceAtMeBackedRuntimeProxySpawn) {
@@ -5111,6 +5212,9 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_slot.restoredForCurrentState = false; a_slot.restoredForCurrentState = false;
a_slot.firstSmoothedMovementLogged = false; a_slot.firstSmoothedMovementLogged = false;
a_slot.remoteStateMovedLogged = false; a_slot.remoteStateMovedLogged = false;
a_slot.proxyObservedLoaded3D = false;
a_slot.proxyPending3DRecovery = false;
a_slot.proxy3DResetQueued = false;
} }
void ResetSlotVisualSneakTracking(ProxyActorSlot& a_slot) void ResetSlotVisualSneakTracking(ProxyActorSlot& a_slot)
@@ -5847,6 +5951,16 @@ 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);
if (a_slot.proxyPending3DRecovery) {
return true;
}
if (!ProxyHasLoaded3D(*proxy)) {
return true;
}
if (!a_slot.restoredForCurrentState || if (!a_slot.restoredForCurrentState ||
a_slot.lifecycleState != ProxyLifecycleState::kActive || a_slot.lifecycleState != ProxyLifecycleState::kActive ||
ShouldSnapRemoteMovement(a_remotePlayer.movementType)) { ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {
@@ -5964,6 +6078,10 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
RE::Actor& a_proxy, RE::Actor& a_proxy,
float a_measuredSpeed) float a_measuredSpeed)
{ {
if (!ProxyHasLoaded3D(a_proxy) || a_slot.proxyPending3DRecovery) {
return;
}
if (!a_slot.lastRemotePlayerSnapshot) { if (!a_slot.lastRemotePlayerSnapshot) {
return; return;
} }
@@ -6027,6 +6145,10 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
continue; continue;
} }
if (!ProxyHasLoaded3D(*proxy) || slot.proxyPending3DRecovery) {
continue;
}
RE::NiPoint3 currentPosition = proxy->GetPosition(); RE::NiPoint3 currentPosition = proxy->GetPosition();
RE::NiPoint3 deltaToTarget = slot.targetPosition - currentPosition; RE::NiPoint3 deltaToTarget = slot.targetPosition - currentPosition;
float distanceToTarget = deltaToTarget.Length(); float distanceToTarget = deltaToTarget.Length();
+10
View File
@@ -156,6 +156,12 @@ namespace F4T::ProxyPuppet
return; return;
} }
// Driving motion feedback while the mesh is unloaded corrupts PlaceAtMe
// proxies on reload (A-pose, missing head). Wait for 3D recovery instead.
if (!a_this->Get3D() && !a_this->GetFullyLoaded3D()) {
return;
}
const auto locomotion = GetProxyLocomotionState(a_this->GetFormID()); const auto locomotion = GetProxyLocomotionState(a_this->GetFormID());
WriteProxyGraphSpeed(a_this, locomotion.speed); WriteProxyGraphSpeed(a_this, locomotion.speed);
@@ -251,6 +257,10 @@ namespace F4T::ProxyPuppet
return; return;
} }
if (!a_actor.Get3D() && !a_actor.GetFullyLoaded3D()) {
return;
}
const float safeDelta = std::clamp(a_deltaSeconds, 1.0F / 120.0F, 0.1F); const float safeDelta = std::clamp(a_deltaSeconds, 1.0F / 120.0F, 0.1F);
RE::NiPoint3 velocity{}; RE::NiPoint3 velocity{};
if (safeDelta > 0.0F) { if (safeDelta > 0.0F) {