Harden runtime proxy adoption

Ignore deleted actors during proxy candidate selection, delete stale saved runtime proxies before spawning fresh ones, and promote proxies based on loaded 3D instead of camera visibility. Updated docs and changelog accordingly.
This commit is contained in:
2026-06-29 15:37:05 +12:00
parent b505477e08
commit d0649667f7
4 changed files with 148 additions and 15 deletions
+58 -15
View File
@@ -1888,15 +1888,12 @@ namespace
a_proxy.GetCurrent3D() != nullptr;
}
// Partial Get3D without IsVisible causes headless A-poses if movement or animation
// runs too early (especially after PlaceAtMe spawn or load-range reload).
bool ProxyMeshReadyForPresentation(const RE::Actor& a_proxy)
// IsVisible is frustum-sensitive and can stay false until the local player turns
// toward the actor, so use loaded 3D as the promotion gate and let the active
// visibility refresh path handle frustum nudges after assignment.
bool Proxy3DReadyForPresentation(const RE::Actor& a_proxy)
{
if (!a_proxy.IsVisible()) {
return false;
}
return a_proxy.GetFullyLoaded3D() != nullptr || a_proxy.Get3D() != nullptr;
return ProxyHasLoaded3D(a_proxy);
}
bool ShouldRetainProxyInPriorLocalCell(
@@ -2150,7 +2147,7 @@ namespace
return;
}
if (!ProxyMeshReadyForPresentation(a_proxy)) {
if (!Proxy3DReadyForPresentation(a_proxy)) {
return;
}
@@ -2984,6 +2981,15 @@ namespace
if (assignedRef && assignedRef.get() == std::addressof(a_actor)) {
return true;
}
const auto pendingRef = slot.pendingSpawnHandle.get();
if (pendingRef && pendingRef.get() == std::addressof(a_actor)) {
return true;
}
if (slot.cachedProxyFormId && *slot.cachedProxyFormId == a_actor.GetFormID()) {
return true;
}
}
return false;
@@ -4067,10 +4073,33 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
return false;
}
// A stale proxy marked for deletion is not removed from the cell the same frame,
// so it must never be isolated as the freshly spawned proxy; otherwise the slot
// adopts and drives the invisible deleted actor while the real spawn stays idle.
// IsDeleted() is non-const in CommonLibF4; const_cast keeps this predicate const.
if (const_cast<RE::Actor&>(a_actor).IsDeleted()) {
return false;
}
const auto* baseActor = a_actor.GetNPC();
return baseActor && baseActor->GetFormID() == a_slot.spawnBaseFormId;
}
bool IsStaleSavedRuntimeProxyCandidate(
const ProxyActorSlot& a_slot,
const RE::TESObjectREFR& a_ref,
const RE::Actor& a_actor,
const RE::PlayerCharacter& a_player)
{
// IsSlotSpawnCandidateActor already rejects deleted actors, so this only matches
// live, runtime-created custom proxies that no current slot owns.
return a_slot.spawnSource == RuntimeActorBaseSource::kCustomProxy &&
a_actor.IsCreated() &&
!IsRuntimeProxyRef(a_ref) &&
!IsProxyPoolActorAssigned(a_actor) &&
IsSlotSpawnCandidateActor(a_slot, a_actor, a_player);
}
void SnapshotPreexistingSlotCandidates(
ProxyActorSlot& a_slot,
RE::TESObjectCELL& a_currentCell,
@@ -4088,6 +4117,20 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
return RE::BSContainer::ForEachResult::kContinue;
}
if (IsStaleSavedRuntimeProxyCandidate(a_slot, *a_ref, *actor, a_player)) {
const auto staleFormId = actor->GetFormID();
const auto position = actor->GetPosition();
LogInfoWithLocalPlayerPrefix(std::format(
"Stage 4 deleting stale saved runtime proxy candidate before spawning fresh proxy for remote player {}: actor={:08X}, base={:08X}, position={}, distanceFromLocalPlayer={:.2f}.",
a_slot.remotePlayerId,
staleFormId,
actor->GetNPC() ? actor->GetNPC()->GetFormID() : 0,
FormatPosition(position),
GetDistance(position, playerPosition)));
actor->MarkAsDeleted();
return RE::BSContainer::ForEachResult::kContinue;
}
const auto position = actor->GetPosition();
a_slot.preexistingCandidateRefs.insert(actor->GetFormID());
LogInfoWithLocalPlayerPrefix(std::format(
@@ -4334,7 +4377,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
{
const auto has3D = a_actor.Get3D() != nullptr;
const auto isVisible = a_actor.IsVisible();
if (!ProxyMeshReadyForPresentation(a_actor)) {
if (!Proxy3DReadyForPresentation(a_actor)) {
LogWarningWithLocalPlayerPrefix(std::format(
"Stage 4 runtime proxy for remote player {} promoted with partial visibility: Get3D={}, GetFullyLoaded3D={}, IsVisible={}. Continuing to remote movement.",
a_slot.remotePlayerId,
@@ -4486,12 +4529,12 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
return true;
}
if (!ProxyMeshReadyForPresentation(*actor)) {
if (!Proxy3DReadyForPresentation(*actor)) {
if (elapsedSeconds < kPlaceAtMeProxyMaxSettleSeconds) {
LogThrottledInfo(
"stage4_placeatme_proxy_waiting_for_visibility_" + std::to_string(a_slot.remotePlayerId),
"stage4_placeatme_proxy_waiting_for_loaded_3d_" + 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.",
"Stage 4 runtime proxy for remote player {} is waiting for loaded 3D before promotion: Get3D={}, GetFullyLoaded3D={}, IsVisible={}, settleElapsed={:.1f}s.",
a_slot.remotePlayerId,
actor->Get3D() != nullptr,
actor->GetFullyLoaded3D() != nullptr,
@@ -4502,7 +4545,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
}
LogWarningWithLocalPlayerPrefix(std::format(
"Stage 4 runtime proxy for remote player {} exceeded max settle wait ({:.1f}s) without visible 3D; promoting anyway.",
"Stage 4 runtime proxy for remote player {} exceeded max settle wait ({:.1f}s) without loaded 3D; promoting anyway.",
a_slot.remotePlayerId,
kPlaceAtMeProxyMaxSettleSeconds));
}
@@ -5618,7 +5661,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
slot.visibilityRefreshPending = true;
slot.visibilityRefreshLogged = false;
slot.proxyObservedLoaded3D = ProxyHasLoaded3D(*proxy);
if (!ProxyMeshReadyForPresentation(*proxy)) {
if (!Proxy3DReadyForPresentation(*proxy)) {
slot.proxyPending3DRecovery = true;
slot.proxy3DResetQueued = false;
slot.proxy3DRecoverySettling = false;