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