Improve proxy mesh readiness and spawn logic

Introduce ProxyMeshReadyForPresentation to treat partially loaded but visible proxy meshes as ready (avoids headless A-poses). Track lastObservedRemoteCellId on proxy slots and use it to detect remote cell changes so proxies can be restored or snapped when needed. Harden spawn lifecycle handling: reset stale promoted state, add/clear fields (pendingSpawnHandle, spawnAttempted, spawnFailed, lifecycleState, holdReason, visibility flags) and ensure slots are properly reinitialized. Replace ProxyHasLoaded3D checks with the new readiness check in animation sync and movement updates. Also publish an initial transform (including cell/worldspace info) immediately on connect so host/client can resolve same-cell placements without waiting for movement.
This commit is contained in:
2026-06-16 16:08:59 +12:00
parent 31406a1275
commit 73d44a828b
3 changed files with 86 additions and 5 deletions
+51 -4
View File
@@ -315,6 +315,7 @@ namespace
std::optional<bool> lastObservedJumpingState; std::optional<bool> lastObservedJumpingState;
std::optional<bool> lastObservedWeaponDrawnState; std::optional<bool> lastObservedWeaponDrawnState;
std::optional<std::string> lastObservedMovementType; std::optional<std::string> lastObservedMovementType;
std::optional<std::string> lastObservedRemoteCellId;
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket; std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
std::optional<bool> lastAppliedVisualSneakingState; std::optional<bool> lastAppliedVisualSneakingState;
std::optional<RemotePlayer> lastRemotePlayerSnapshot; std::optional<RemotePlayer> lastRemotePlayerSnapshot;
@@ -1862,6 +1863,14 @@ namespace
a_proxy.GetCurrent3D() != nullptr; a_proxy.GetCurrent3D() != nullptr;
} }
// Partial Get3D without skin/head causes headless A-poses if movement or animation
// runs too early (especially on the second concurrent PlaceAtMe spawn).
bool ProxyMeshReadyForPresentation(const RE::Actor& a_proxy)
{
return a_proxy.GetFullyLoaded3D() != nullptr ||
(a_proxy.Get3D() != nullptr && a_proxy.IsVisible());
}
void RefreshRuntimeProxyVisibility(RE::Actor& a_proxy) void RefreshRuntimeProxyVisibility(RE::Actor& a_proxy)
{ {
if (a_proxy.IsDisabled()) { if (a_proxy.IsDisabled()) {
@@ -4169,10 +4178,22 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
return false; return false;
} }
if (a_slot.proxyHandle || a_slot.spawnFailed || a_slot.spawnState == PlaceAtMeBackedProxySpawnState::kPromoted) { if (a_slot.spawnFailed) {
return false; return false;
} }
if (a_slot.proxyHandle) {
return false;
}
if (a_slot.spawnState == PlaceAtMeBackedProxySpawnState::kPromoted) {
// Stale promoted state after local cell change or deleted proxy; restart spawn.
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kNotStarted;
a_slot.spawnSucceeded = false;
a_slot.spawnAttemptInProgress = false;
a_slot.spawnAttempted = false;
}
if (a_slot.spawnState == PlaceAtMeBackedProxySpawnState::kNotStarted) { if (a_slot.spawnState == PlaceAtMeBackedProxySpawnState::kNotStarted) {
if (!TryBeginSlotPlaceAtMeSpawn(a_slot, a_currentCell, a_player)) { if (!TryBeginSlotPlaceAtMeSpawn(a_slot, a_currentCell, a_player)) {
if (!g_runtimeProxySpawnAttemptPlayerId) { if (!g_runtimeProxySpawnAttemptPlayerId) {
@@ -5215,6 +5236,7 @@ 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.lastObservedRemoteCellId.reset();
} }
void ResetSlotVisualSneakTracking(ProxyActorSlot& a_slot) void ResetSlotVisualSneakTracking(ProxyActorSlot& a_slot)
@@ -5367,10 +5389,20 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
slot.proxyHandle.reset(); slot.proxyHandle.reset();
slot.cachedProxyActor = nullptr; slot.cachedProxyActor = nullptr;
slot.cachedProxyFormId.reset(); slot.cachedProxyFormId.reset();
slot.pendingSpawnHandle.reset();
slot.restoredForCurrentState = false; slot.restoredForCurrentState = false;
slot.spawnState = PlaceAtMeBackedProxySpawnState::kNotStarted; slot.spawnState = PlaceAtMeBackedProxySpawnState::kNotStarted;
slot.spawnAttemptInProgress = false; slot.spawnAttemptInProgress = false;
slot.spawnAttempted = false;
slot.spawnSucceeded = false; slot.spawnSucceeded = false;
slot.spawnFailed = false;
slot.lifecycleState = ProxyLifecycleState::kHeldNoRemote;
slot.holdReason.reset();
slot.proxyObservedLoaded3D = false;
slot.proxyPending3DRecovery = false;
slot.proxy3DResetQueued = false;
slot.visibilityRefreshPending = true;
slot.visibilityRefreshLogged = false;
g_dynamicProxyPool.erase(playerId); g_dynamicProxyPool.erase(playerId);
LogInfoWithLocalPlayerPrefix(std::format( LogInfoWithLocalPlayerPrefix(std::format(
@@ -5961,9 +5993,24 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
return true; return true;
} }
const bool remoteCellChanged =
a_slot.lastObservedRemoteCellId.has_value() &&
*a_slot.lastObservedRemoteCellId != a_remotePlayer.cellId;
a_slot.lastObservedRemoteCellId = a_remotePlayer.cellId;
if (!ProxyMeshReadyForPresentation(*proxy)) {
if (remoteCellChanged || ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {
a_slot.restoredForCurrentState = false;
}
return true;
}
const bool needsSnapRestore =
ShouldSnapRemoteMovement(a_remotePlayer.movementType) || remoteCellChanged;
if (!a_slot.restoredForCurrentState || if (!a_slot.restoredForCurrentState ||
a_slot.lifecycleState != ProxyLifecycleState::kActive || a_slot.lifecycleState != ProxyLifecycleState::kActive ||
ShouldSnapRemoteMovement(a_remotePlayer.movementType)) { needsSnapRestore) {
RestoreSlotProxyForRemotePlayer(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed); RestoreSlotProxyForRemotePlayer(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
return true; return true;
} }
@@ -6078,7 +6125,7 @@ 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) { if (!ProxyMeshReadyForPresentation(a_proxy) || a_slot.proxyPending3DRecovery) {
return; return;
} }
@@ -6145,7 +6192,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
continue; continue;
} }
if (!ProxyHasLoaded3D(*proxy) || slot.proxyPending3DRecovery) { if (!ProxyMeshReadyForPresentation(*proxy) || slot.proxyPending3DRecovery) {
continue; continue;
} }
+7 -1
View File
@@ -97,9 +97,15 @@ namespace
a_proxy.GetCurrent3D() != nullptr; a_proxy.GetCurrent3D() != nullptr;
} }
bool ProxyMeshReadyForPresentation(const RE::Actor& a_proxy)
{
return a_proxy.GetFullyLoaded3D() != nullptr ||
(a_proxy.Get3D() != nullptr && a_proxy.IsVisible());
}
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)
{ {
if (!ProxyHasLoaded3D(a_proxy)) { if (!ProxyMeshReadyForPresentation(a_proxy)) {
return false; return false;
} }
+28
View File
@@ -509,6 +509,34 @@ namespace
lastLogTime = currentTime; lastLogTime = currentTime;
lastDebugSampleTime = currentTime; lastDebugSampleTime = currentTime;
hasLastTransform = true; hasLastTransform = true;
// Publish cell/worldspace immediately on connect so host and client can
// resolve same-cell proxy placement without waiting for movement.
if (F4T::Networking::SendTransformPacket(
currentTransform.x,
currentTransform.y,
currentTransform.z,
currentTransform.angleZ,
"normal",
currentLocation.hasCellId,
currentLocation.cellId,
currentLocation.hasWorldspaceId,
currentLocation.worldspaceId,
debugMovementState.isMoving,
debugMovementState.isSprinting,
debugMovementState.isSneaking,
debugMovementState.isJumping,
debugMovementState.isCrouching,
debugMovementState.weaponDrawn,
debugMovementState.movementSpeed,
-1.0F)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Initial transform sent with cellId={:08X}{}.",
currentLocation.hasCellId ? currentLocation.cellId : 0U,
currentLocation.hasWorldspaceId ?
std::format(", worldspaceId={:08X}", currentLocation.worldspaceId) :
std::string{}));
}
return; return;
} }