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:
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user