Improve runtime proxy slot lifecycle & spawn

Refine Stage 4 runtime proxy management: introduce explicit lifecycle states (HeldNoRemote, HeldDisconnected, HeldLeftCell, Reusable, SpawnFailed), add a 30s disconnected-slot reuse delay, and implement per-slot hidden holding positions with spacing. Add a held-position correction pass (correct drift >25 units) and make disconnected-held slots become reusable after the grace period; reusable slots can be reassigned to later playerIds. Capture a remote player's current transform before performing the PlaceAtMe-backed Stage 4 spawn and apply that transform immediately when the isolated proxy is promoted, preventing a brief visual spawn near the local player. Misc: improved logging/throttling, slot assignment/reassignment logic, and various helper functions. Docs updated (architecture.md, dev-log.md) and plugin/src/F4TProxyActorController.cpp implements the behavior.
This commit is contained in:
2026-06-03 10:47:15 +12:00
parent b4ec928c67
commit 5394250ef2
3 changed files with 540 additions and 61 deletions
+341 -58
View File
@@ -56,9 +56,12 @@ namespace
constexpr auto kDebugRuntimeProxyScale = 1.50F;
constexpr float kProxyPositionLerpAlpha = 0.15F;
constexpr std::size_t kMaxRuntimeProxyActors = 4;
constexpr float kDisconnectedProxyReuseDelaySeconds = 30.0F;
// Keep inactive proxies inside the loaded test cell instead of disabling actor
// refs before a safe lifecycle cleanup path is validated in-game.
constexpr RE::NiPoint3 kProxyHiddenHoldingPosition{ -100000.0F, -100000.0F, -100000.0F };
constexpr RE::NiPoint3 kProxyHiddenHoldingPosition{ -200000.0F, -200000.0F, -50000.0F };
constexpr float kProxyHiddenHoldingPositionSpacing = 500.0F;
constexpr float kHeldProxyPositionCorrectionDistance = 25.0F;
constexpr auto kSafetyOffsetMovementInterval = 200ms;
constexpr auto kWarningLogInterval = 5s;
constexpr auto kProxyMovementDiagnosticLogInterval = 2s;
@@ -110,11 +113,12 @@ namespace
enum class ProxyLifecycleState
{
kNoRemotePlayer,
kRepresentingRemotePlayer,
kRemotePlayerDisconnected,
kRemotePlayerLeftCell,
kProxyHiddenOrIdle
kHeldNoRemote,
kActive,
kHeldDisconnected,
kHeldLeftCell,
kReusable,
kSpawnFailed
};
enum class ProxyMovementDiagnosticMode
@@ -177,16 +181,21 @@ namespace
struct ProxyActorSlot
{
std::uint32_t remotePlayerId{};
std::uint32_t holdingPositionIndex{};
RE::ObjectRefHandle proxyHandle;
RE::ObjectRefHandle pendingSpawnHandle;
ProxyLifecycleState lifecycleState{ ProxyLifecycleState::kNoRemotePlayer };
ProxyLifecycleState lifecycleState{ ProxyLifecycleState::kHeldNoRemote };
std::optional<ProxyLifecycleState> holdReason;
std::optional<std::uint32_t> previousRemotePlayerId;
PlaceAtMeBackedProxySpawnState spawnState{ PlaceAtMeBackedProxySpawnState::kNotStarted };
RuntimeActorBaseSource spawnSource{ RuntimeActorBaseSource::kCustomProxy };
std::uint32_t spawnBaseFormId{ 0 };
std::unordered_set<std::uint32_t> preexistingCandidateRefs;
std::optional<RE::NiPoint3> initialRemoteSpawnPosition;
std::chrono::steady_clock::time_point candidateSearchStartTime{};
std::chrono::steady_clock::time_point settleStartTime{};
std::chrono::steady_clock::time_point heldSinceTime{};
float initialRemoteSpawnHeading{ 0.0F };
bool spawnAttempted{ false };
bool spawnAttemptInProgress{ false };
bool spawnSucceeded{ false };
@@ -194,6 +203,8 @@ namespace
bool restoredForCurrentState{ false };
bool firstSmoothedMovementLogged{ false };
bool remoteStateMovedLogged{ false };
bool reusableLogged{ false };
bool directRemoteSpawnLogged{ false };
std::optional<bool> lastObservedSneakState;
};
@@ -216,7 +227,7 @@ namespace
bool g_proxySmoothingEnabledLogged = false;
bool g_firstSmoothedMovementLogged = false;
auto g_lastSafetyOffsetMovementTime = std::chrono::steady_clock::time_point{};
ProxyLifecycleState g_proxyLifecycleState = ProxyLifecycleState::kNoRemotePlayer;
ProxyLifecycleState g_proxyLifecycleState = ProxyLifecycleState::kHeldNoRemote;
std::optional<ProxyLifecycleState> g_proxyHoldReason;
std::optional<std::uint32_t> g_proxyHoldPlayerId;
std::optional<ActiveProxyKind> g_proxyHoldKind;
@@ -282,6 +293,7 @@ namespace
RuntimeActorBaseSource g_runtimeProxyActorBaseSource = RuntimeActorBaseSource::kCustomProxy;
std::unordered_map<std::uint32_t, ProxyActorSlot> g_proxySlots;
std::optional<std::uint32_t> g_runtimeProxySpawnAttemptPlayerId;
std::uint32_t g_nextProxyHoldingPositionIndex = 0;
bool g_stage4StartedLogged = false;
bool g_fallbackRuntimeUnavailableLogged = false;
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_lastWarningLogTimes;
@@ -442,7 +454,7 @@ namespace
std::optional<std::uint32_t> a_playerId,
ActiveProxyKind a_proxyKind)
{
return g_proxyLifecycleState == ProxyLifecycleState::kProxyHiddenOrIdle &&
return g_proxyLifecycleState == a_reason &&
g_proxyHoldReason == a_reason &&
g_proxyHoldPlayerId == a_playerId &&
g_proxyHoldKind == a_proxyKind;
@@ -451,12 +463,12 @@ namespace
void LogProxyStillHeld(ProxyLifecycleState a_reason, std::optional<std::uint32_t> a_playerId)
{
switch (a_reason) {
case ProxyLifecycleState::kNoRemotePlayer:
case ProxyLifecycleState::kHeldNoRemote:
LogThrottledInfo(
"proxy_held_no_remote_player",
"No suitable remote player is available; proxy actor is already at the holding position.");
break;
case ProxyLifecycleState::kRemotePlayerDisconnected:
case ProxyLifecycleState::kHeldDisconnected:
if (a_playerId) {
LogThrottledInfo(
"proxy_held_disconnected_" + std::to_string(*a_playerId),
@@ -465,7 +477,7 @@ namespace
*a_playerId));
}
break;
case ProxyLifecycleState::kRemotePlayerLeftCell:
case ProxyLifecycleState::kHeldLeftCell:
if (a_playerId) {
LogThrottledInfo(
"proxy_held_left_cell_" + std::to_string(*a_playerId),
@@ -491,7 +503,7 @@ namespace
}
a_proxy.SetPosition(kProxyHiddenHoldingPosition, true);
g_proxyLifecycleState = ProxyLifecycleState::kProxyHiddenOrIdle;
g_proxyLifecycleState = a_reason;
g_proxyHoldReason = a_reason;
g_proxyHoldPlayerId = a_playerId;
g_proxyHoldKind = a_proxyKind;
@@ -500,12 +512,12 @@ namespace
ResetRemoteMovementStateTracking();
switch (a_reason) {
case ProxyLifecycleState::kNoRemotePlayer:
case ProxyLifecycleState::kHeldNoRemote:
LogInfoWithLocalPlayerPrefix(std::format(
"No suitable remote player is available; moving {} to holding position.",
GetActiveProxyLogName(a_proxyKind)));
break;
case ProxyLifecycleState::kRemotePlayerDisconnected:
case ProxyLifecycleState::kHeldDisconnected:
if (a_playerId) {
LogInfoWithLocalPlayerPrefix(std::format(
"Remote player {} disappeared; moving {} to holding position.",
@@ -513,7 +525,7 @@ namespace
GetActiveProxyLogName(a_proxyKind)));
}
break;
case ProxyLifecycleState::kRemotePlayerLeftCell:
case ProxyLifecycleState::kHeldLeftCell:
if (a_playerId) {
LogInfoWithLocalPlayerPrefix(std::format(
"Remote player {} is not in the same cell; moving {} to holding position.",
@@ -2517,6 +2529,48 @@ namespace
return candidates;
}
RE::NiPoint3 GetRemotePlayerPosition(const RemotePlayer& a_remotePlayer)
{
return RE::NiPoint3{
a_remotePlayer.x,
a_remotePlayer.y,
a_remotePlayer.z
};
}
void SetSlotInitialRemoteSpawnTransform(ProxyActorSlot& a_slot, const RemotePlayer& a_remotePlayer)
{
a_slot.initialRemoteSpawnPosition = GetRemotePlayerPosition(a_remotePlayer);
a_slot.initialRemoteSpawnHeading = a_remotePlayer.angleZ;
}
void CaptureSlotInitialRemoteSpawnTransform(ProxyActorSlot& a_slot, const RemotePlayer& a_remotePlayer)
{
if (a_slot.proxyHandle || a_slot.spawnState == PlaceAtMeBackedProxySpawnState::kPromoted) {
return;
}
SetSlotInitialRemoteSpawnTransform(a_slot, a_remotePlayer);
}
void ApplySlotInitialRemoteSpawnTransform(ProxyActorSlot& a_slot, RE::Actor& a_actor)
{
if (!a_slot.initialRemoteSpawnPosition) {
return;
}
a_actor.SetPosition(*a_slot.initialRemoteSpawnPosition, true);
a_actor.SetHeading(a_slot.initialRemoteSpawnHeading);
if (!a_slot.directRemoteSpawnLogged) {
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor {:08X} for remote player {} spawned directly at remote transform position {}.",
a_actor.GetFormID(),
a_slot.remotePlayerId,
FormatPosition(*a_slot.initialRemoteSpawnPosition)));
a_slot.directRemoteSpawnLogged = true;
}
}
bool IsSlotSpawnCandidateActor(
const ProxyActorSlot& a_slot,
const RE::Actor& a_actor,
@@ -2723,6 +2777,7 @@ namespace
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kFailed;
a_slot.spawnAttemptInProgress = false;
a_slot.spawnFailed = true;
a_slot.lifecycleState = ProxyLifecycleState::kSpawnFailed;
a_slot.pendingSpawnHandle.reset();
if (g_runtimeProxySpawnAttemptPlayerId == a_slot.remotePlayerId) {
g_runtimeProxySpawnAttemptPlayerId.reset();
@@ -2731,6 +2786,7 @@ namespace
void StartSlotPlaceAtMeSettle(ProxyActorSlot& a_slot, RE::Actor& a_actor)
{
ApplySlotInitialRemoteSpawnTransform(a_slot, a_actor);
a_slot.pendingSpawnHandle = a_actor.GetHandle();
a_slot.settleStartTime = std::chrono::steady_clock::now();
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kSettling;
@@ -2755,14 +2811,17 @@ namespace
isVisible));
}
ApplySlotInitialRemoteSpawnTransform(a_slot, a_actor);
a_slot.proxyHandle = a_actor.GetHandle();
a_slot.pendingSpawnHandle.reset();
a_slot.spawnSucceeded = true;
a_slot.spawnAttemptInProgress = false;
a_slot.spawnFailed = false;
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kPromoted;
a_slot.lifecycleState = ProxyLifecycleState::kProxyHiddenOrIdle;
a_slot.holdReason = ProxyLifecycleState::kProxyHiddenOrIdle;
a_slot.lifecycleState = ProxyLifecycleState::kHeldNoRemote;
a_slot.holdReason = ProxyLifecycleState::kHeldNoRemote;
a_slot.heldSinceTime = {};
a_slot.reusableLogged = false;
a_slot.restoredForCurrentState = false;
if (g_runtimeProxySpawnAttemptPlayerId == a_slot.remotePlayerId) {
g_runtimeProxySpawnAttemptPlayerId.reset();
@@ -3490,7 +3549,7 @@ namespace
a_proxy.SetPosition(targetPosition, true);
a_proxy.SetHeading(a_remotePlayer.angleZ);
g_proxyLifecycleState = ProxyLifecycleState::kRepresentingRemotePlayer;
g_proxyLifecycleState = ProxyLifecycleState::kActive;
g_proxyHoldReason.reset();
g_proxyHoldPlayerId.reset();
g_proxyHoldKind.reset();
@@ -3602,38 +3661,103 @@ namespace
a_slot.spawnAttemptInProgress = false;
a_slot.spawnSucceeded = false;
a_slot.restoredForCurrentState = false;
if (a_slot.lifecycleState != ProxyLifecycleState::kHeldDisconnected) {
a_slot.heldSinceTime = {};
a_slot.reusableLogged = false;
}
if (g_runtimeProxySpawnAttemptPlayerId == a_slot.remotePlayerId) {
g_runtimeProxySpawnAttemptPlayerId.reset();
}
return nullptr;
}
void MoveSlotProxyToHoldingPosition(
void ResetSlotMovementState(ProxyActorSlot& a_slot)
{
a_slot.restoredForCurrentState = false;
a_slot.firstSmoothedMovementLogged = false;
a_slot.remoteStateMovedLogged = false;
}
void ResetSlotObservationState(ProxyActorSlot& a_slot)
{
a_slot.lastObservedSneakState.reset();
}
bool HasSlotHeldSinceTime(const ProxyActorSlot& a_slot)
{
return a_slot.heldSinceTime.time_since_epoch().count() != 0;
}
bool IsHeldSlotState(ProxyLifecycleState a_state)
{
return a_state == ProxyLifecycleState::kHeldNoRemote ||
a_state == ProxyLifecycleState::kHeldLeftCell ||
a_state == ProxyLifecycleState::kHeldDisconnected ||
a_state == ProxyLifecycleState::kReusable;
}
RE::NiPoint3 GetSlotHoldingPosition(const ProxyActorSlot& a_slot)
{
auto holdingPosition = kProxyHiddenHoldingPosition;
holdingPosition.x += static_cast<float>(a_slot.holdingPositionIndex) * kProxyHiddenHoldingPositionSpacing;
return holdingPosition;
}
void CorrectHeldSlotPositionIfNeeded(ProxyActorSlot& a_slot, RE::Actor& a_proxy)
{
if (!IsHeldSlotState(a_slot.lifecycleState)) {
return;
}
const auto holdingPosition = GetSlotHoldingPosition(a_slot);
const auto currentPosition = a_proxy.GetPosition();
if (GetDistance(currentPosition, holdingPosition) <= kHeldProxyPositionCorrectionDistance) {
return;
}
a_proxy.SetPosition(holdingPosition, true);
LogThrottledInfo(
"held_runtime_proxy_position_corrected_" + std::to_string(a_slot.remotePlayerId),
std::format(
"Held runtime proxy actor {:08X} for remote player {} drifted from holding position; correcting.",
a_proxy.GetFormID(),
a_slot.remotePlayerId));
}
void MarkSlotHeld(
ProxyActorSlot& a_slot,
RE::Actor& a_proxy,
ProxyLifecycleState a_reason)
{
if (a_slot.lifecycleState == ProxyLifecycleState::kProxyHiddenOrIdle && a_slot.holdReason == a_reason) {
if (a_slot.lifecycleState == a_reason && a_slot.holdReason == a_reason) {
CorrectHeldSlotPositionIfNeeded(a_slot, a_proxy);
LogProxyStillHeld(a_reason, a_slot.remotePlayerId);
return;
}
a_proxy.SetPosition(kProxyHiddenHoldingPosition, true);
a_slot.lifecycleState = ProxyLifecycleState::kProxyHiddenOrIdle;
a_proxy.SetPosition(GetSlotHoldingPosition(a_slot), true);
a_slot.lifecycleState = a_reason;
a_slot.holdReason = a_reason;
a_slot.restoredForCurrentState = false;
a_slot.firstSmoothedMovementLogged = false;
a_slot.remoteStateMovedLogged = false;
a_slot.lastObservedSneakState.reset();
ResetSlotMovementState(a_slot);
ResetSlotObservationState(a_slot);
if (a_reason == ProxyLifecycleState::kHeldDisconnected) {
if (!HasSlotHeldSinceTime(a_slot)) {
a_slot.heldSinceTime = std::chrono::steady_clock::now();
}
a_slot.reusableLogged = false;
} else {
a_slot.heldSinceTime = {};
a_slot.reusableLogged = false;
}
switch (a_reason) {
case ProxyLifecycleState::kRemotePlayerDisconnected:
case ProxyLifecycleState::kHeldDisconnected:
LogInfoWithLocalPlayerPrefix(std::format(
"Remote player {} disappeared; moving runtime proxy actor {:08X} to holding position.",
a_slot.remotePlayerId,
a_proxy.GetFormID()));
break;
case ProxyLifecycleState::kRemotePlayerLeftCell:
case ProxyLifecycleState::kHeldLeftCell:
LogInfoWithLocalPlayerPrefix(std::format(
"Remote player {} is not in the same cell; moving runtime proxy actor {:08X} to holding position.",
a_slot.remotePlayerId,
@@ -3648,6 +3772,45 @@ namespace
}
}
void CorrectHeldSlotPositions(RE::TESObjectCELL& a_parentCell, const RE::PlayerCharacter& a_player)
{
for (auto& [_, slot] : g_proxySlots) {
if (!IsHeldSlotState(slot.lifecycleState)) {
continue;
}
if (auto* proxy = TryResolveSlotProxy(slot, a_parentCell, a_player)) {
CorrectHeldSlotPositionIfNeeded(slot, *proxy);
}
}
}
void MarkReusableSlots()
{
const auto now = std::chrono::steady_clock::now();
for (auto& [playerId, slot] : g_proxySlots) {
if (slot.lifecycleState != ProxyLifecycleState::kHeldDisconnected || !HasSlotHeldSinceTime(slot)) {
continue;
}
const auto heldSeconds = std::chrono::duration<float>(now - slot.heldSinceTime).count();
if (heldSeconds < kDisconnectedProxyReuseDelaySeconds) {
continue;
}
slot.lifecycleState = ProxyLifecycleState::kReusable;
slot.holdReason.reset();
slot.previousRemotePlayerId = playerId;
if (!slot.reusableLogged) {
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy slot for disconnected remote player {} became reusable after {:.1f}s held.",
playerId,
heldSeconds));
slot.reusableLogged = true;
}
}
}
void RestoreSlotProxyForRemotePlayer(
ProxyActorSlot& a_slot,
RE::Actor& a_proxy,
@@ -3662,8 +3825,10 @@ namespace
a_proxy.SetPosition(targetPosition, true);
a_proxy.SetHeading(a_remotePlayer.angleZ);
a_slot.lifecycleState = ProxyLifecycleState::kRepresentingRemotePlayer;
a_slot.lifecycleState = ProxyLifecycleState::kActive;
a_slot.holdReason.reset();
a_slot.heldSinceTime = {};
a_slot.reusableLogged = false;
a_slot.restoredForCurrentState = true;
a_slot.firstSmoothedMovementLogged = false;
a_slot.remoteStateMovedLogged = false;
@@ -3720,8 +3885,10 @@ namespace
a_proxy.SetPosition(nextPosition, true);
a_proxy.SetHeading(a_remotePlayer.angleZ);
a_slot.lifecycleState = ProxyLifecycleState::kRepresentingRemotePlayer;
a_slot.lifecycleState = ProxyLifecycleState::kActive;
a_slot.holdReason.reset();
a_slot.heldSinceTime = {};
a_slot.reusableLogged = false;
if (shouldSnap) {
LogThrottledInfo(
@@ -3770,31 +3937,125 @@ namespace
return slot != g_proxySlots.end() ? std::addressof(slot->second) : nullptr;
}
ProxyActorSlot* GetOrCreateSlot(std::uint32_t a_remotePlayerId)
std::size_t CountSlotsInState(ProxyLifecycleState a_state)
{
if (auto* existingSlot = FindSlot(a_remotePlayerId)) {
return existingSlot;
}
return static_cast<std::size_t>(std::ranges::count_if(
g_proxySlots,
[a_state](const auto& a_entry) {
return a_entry.second.lifecycleState == a_state;
}));
}
if (g_proxySlots.size() >= kMaxRuntimeProxyActors) {
LogThrottledWarning(
"stage4_max_runtime_proxy_limit",
std::format(
"Runtime proxy max limit reached: max={}, activeSlots={}. Remote player {} will not receive a visual proxy yet.",
kMaxRuntimeProxyActors,
g_proxySlots.size(),
a_remotePlayerId));
std::optional<std::uint32_t> FindReusableSlot()
{
std::optional<std::uint32_t> reusablePlayerId;
for (const auto& [playerId, slot] : g_proxySlots) {
if (slot.lifecycleState != ProxyLifecycleState::kReusable) {
continue;
}
if (!reusablePlayerId || playerId < *reusablePlayerId) {
reusablePlayerId = playerId;
}
}
return reusablePlayerId;
}
ProxyActorSlot* AssignSlotToRemotePlayer(
std::uint32_t a_reusablePlayerId,
const RemotePlayer& a_remotePlayer,
RE::TESObjectCELL& a_parentCell,
const RE::PlayerCharacter& a_player,
bool a_sameCellPassed)
{
auto node = g_proxySlots.extract(a_reusablePlayerId);
if (node.empty()) {
return nullptr;
}
auto [slot, inserted] = g_proxySlots.emplace(a_remotePlayerId, ProxyActorSlot{});
slot->second.remotePlayerId = a_remotePlayerId;
if (inserted) {
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy slot created for remote player {}.",
a_remotePlayerId));
auto& slotValue = node.mapped();
const auto previousRemotePlayerId = slotValue.remotePlayerId;
slotValue.previousRemotePlayerId = previousRemotePlayerId;
slotValue.remotePlayerId = a_remotePlayer.playerId;
slotValue.lifecycleState = ProxyLifecycleState::kHeldNoRemote;
slotValue.holdReason.reset();
slotValue.heldSinceTime = {};
slotValue.reusableLogged = false;
slotValue.directRemoteSpawnLogged = false;
SetSlotInitialRemoteSpawnTransform(slotValue, a_remotePlayer);
slotValue.pendingSpawnHandle.reset();
slotValue.spawnAttemptInProgress = false;
slotValue.spawnFailed = false;
if (slotValue.proxyHandle) {
slotValue.spawnState = PlaceAtMeBackedProxySpawnState::kPromoted;
slotValue.spawnSucceeded = true;
} else {
slotValue.spawnState = PlaceAtMeBackedProxySpawnState::kNotStarted;
slotValue.spawnAttempted = false;
slotValue.spawnSucceeded = false;
slotValue.preexistingCandidateRefs.clear();
}
return std::addressof(slot->second);
ResetSlotMovementState(slotValue);
ResetSlotObservationState(slotValue);
if (g_runtimeProxySpawnAttemptPlayerId == previousRemotePlayerId) {
g_runtimeProxySpawnAttemptPlayerId.reset();
}
node.key() = a_remotePlayer.playerId;
auto insertResult = g_proxySlots.insert(std::move(node));
auto& slot = insertResult.position->second;
LogInfoWithLocalPlayerPrefix(std::format(
"Reusable runtime proxy slot reassigned from disconnected remote player {} to remote player {}.",
previousRemotePlayerId,
a_remotePlayer.playerId));
if (auto* proxy = TryResolveSlotProxy(slot, a_parentCell, a_player)) {
RestoreSlotProxyForRemotePlayer(slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
}
return std::addressof(slot);
}
ProxyActorSlot* GetOrCreateSlot(
const RemotePlayer& a_remotePlayer,
RE::TESObjectCELL& a_parentCell,
const RE::PlayerCharacter& a_player,
bool a_sameCellPassed)
{
if (auto* existingSlot = FindSlot(a_remotePlayer.playerId)) {
return existingSlot;
}
if (g_proxySlots.size() < kMaxRuntimeProxyActors) {
auto [slot, inserted] = g_proxySlots.emplace(a_remotePlayer.playerId, ProxyActorSlot{});
slot->second.remotePlayerId = a_remotePlayer.playerId;
CaptureSlotInitialRemoteSpawnTransform(slot->second, a_remotePlayer);
if (inserted) {
slot->second.holdingPositionIndex = g_nextProxyHoldingPositionIndex++;
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy slot created for remote player {} with holding index {}.",
a_remotePlayer.playerId,
slot->second.holdingPositionIndex));
}
return std::addressof(slot->second);
}
if (auto reusablePlayerId = FindReusableSlot()) {
return AssignSlotToRemotePlayer(*reusablePlayerId, a_remotePlayer, a_parentCell, a_player, a_sameCellPassed);
}
LogThrottledWarning(
"stage4_max_runtime_proxy_limit",
std::format(
"Runtime proxy max limit reached: max={}, totalSlots={}, active={}, heldLeftCell={}, heldDisconnectedWaiting={}, reusable={}. Remote player {} will not receive a visual proxy yet.",
kMaxRuntimeProxyActors,
g_proxySlots.size(),
CountSlotsInState(ProxyLifecycleState::kActive),
CountSlotsInState(ProxyLifecycleState::kHeldLeftCell),
CountSlotsInState(ProxyLifecycleState::kHeldDisconnected),
CountSlotsInState(ProxyLifecycleState::kReusable),
a_remotePlayer.playerId));
return nullptr;
}
bool UpdateRuntimeSlotForRemotePlayer(
@@ -3804,6 +4065,7 @@ namespace
const RE::PlayerCharacter& a_player,
bool a_sameCellPassed)
{
CaptureSlotInitialRemoteSpawnTransform(a_slot, a_remotePlayer);
auto* proxy = TryResolveSlotProxy(a_slot, a_parentCell, a_player);
if (!proxy) {
MaintainSlotPlaceAtMeSpawn(a_slot, a_parentCell, a_player);
@@ -3814,7 +4076,7 @@ namespace
}
if (!a_slot.restoredForCurrentState ||
a_slot.lifecycleState != ProxyLifecycleState::kRepresentingRemotePlayer ||
a_slot.lifecycleState != ProxyLifecycleState::kActive ||
ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {
RestoreSlotProxyForRemotePlayer(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
return true;
@@ -3834,13 +4096,31 @@ namespace
if (a_seenRemotePlayerIds.contains(playerId)) {
continue;
}
if (slot.lifecycleState == ProxyLifecycleState::kReusable) {
continue;
}
if (slot.spawnAttemptInProgress || slot.spawnState == PlaceAtMeBackedProxySpawnState::kSettling) {
MaintainSlotPlaceAtMeSpawn(slot, a_parentCell, a_player);
}
if (auto* proxy = TryResolveSlotProxy(slot, a_parentCell, a_player)) {
MoveSlotProxyToHoldingPosition(slot, *proxy, ProxyLifecycleState::kRemotePlayerDisconnected);
MarkSlotHeld(slot, *proxy, ProxyLifecycleState::kHeldDisconnected);
} else if (slot.lifecycleState != ProxyLifecycleState::kHeldDisconnected || !HasSlotHeldSinceTime(slot)) {
const auto newlyHeldDisconnected = slot.lifecycleState != ProxyLifecycleState::kHeldDisconnected;
slot.lifecycleState = ProxyLifecycleState::kHeldDisconnected;
slot.holdReason = ProxyLifecycleState::kHeldDisconnected;
if (!HasSlotHeldSinceTime(slot)) {
slot.heldSinceTime = std::chrono::steady_clock::now();
}
slot.reusableLogged = false;
ResetSlotMovementState(slot);
ResetSlotObservationState(slot);
if (newlyHeldDisconnected) {
LogInfoWithLocalPlayerPrefix(std::format(
"Remote player {} disappeared; runtime proxy slot is held without a valid proxy actor and can become reusable after the grace period.",
playerId));
}
}
}
}
@@ -3857,7 +4137,7 @@ namespace
if (auto* fallbackProxy = ResolveProxy(a_parentCell, a_player)) {
MoveProxyToHoldingPosition(
*fallbackProxy,
ProxyLifecycleState::kRemotePlayerDisconnected,
ProxyLifecycleState::kHeldDisconnected,
g_representedPlayerId,
ActiveProxyKind::kPlacedFallback);
}
@@ -3885,7 +4165,7 @@ namespace
const auto representedPlayerChanged = g_representedPlayerId != a_remotePlayer.playerId;
g_representedPlayerId = a_remotePlayer.playerId;
if (representedPlayerChanged ||
g_proxyLifecycleState != ProxyLifecycleState::kRepresentingRemotePlayer ||
g_proxyLifecycleState != ProxyLifecycleState::kActive ||
ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {
RestoreProxyForRemotePlayer(
*fallbackProxy,
@@ -3949,6 +4229,7 @@ namespace F4T::ProxyActorController
}
const auto remotePlayers = GetSortedRemotePlayerSnapshot();
MarkReusableSlots();
std::unordered_set<std::uint32_t> seenRemotePlayerIds;
seenRemotePlayerIds.reserve(remotePlayers.size());
std::optional<RemotePlayer> fallbackCandidate;
@@ -3960,7 +4241,7 @@ namespace F4T::ProxyActorController
if (!HasValidRemoteTransform(remotePlayer)) {
if (auto* slot = FindSlot(remotePlayer.playerId)) {
if (auto* proxy = TryResolveSlotProxy(*slot, *parentCell, *player)) {
MoveSlotProxyToHoldingPosition(*slot, *proxy, ProxyLifecycleState::kNoRemotePlayer);
MarkSlotHeld(*slot, *proxy, ProxyLifecycleState::kHeldNoRemote);
}
}
continue;
@@ -3970,13 +4251,13 @@ namespace F4T::ProxyActorController
if (!sameCellPassed) {
if (auto* slot = FindSlot(remotePlayer.playerId)) {
if (auto* proxy = TryResolveSlotProxy(*slot, *parentCell, *player)) {
MoveSlotProxyToHoldingPosition(*slot, *proxy, ProxyLifecycleState::kRemotePlayerLeftCell);
MarkSlotHeld(*slot, *proxy, ProxyLifecycleState::kHeldLeftCell);
}
}
continue;
}
auto* slot = GetOrCreateSlot(remotePlayer.playerId);
auto* slot = GetOrCreateSlot(remotePlayer, *parentCell, *player, sameCellPassed);
if (!slot) {
continue;
}
@@ -3991,7 +4272,7 @@ namespace F4T::ProxyActorController
if (auto* fallbackProxy = ResolveProxy(*parentCell, *player)) {
MoveProxyToHoldingPosition(
*fallbackProxy,
ProxyLifecycleState::kNoRemotePlayer,
ProxyLifecycleState::kHeldNoRemote,
g_representedPlayerId,
ActiveProxyKind::kPlacedFallback);
}
@@ -4005,6 +4286,8 @@ namespace F4T::ProxyActorController
}
HoldDisconnectedSlots(seenRemotePlayerIds, *parentCell, *player);
MarkReusableSlots();
CorrectHeldSlotPositions(*parentCell, *player);
HoldFallbackIfRepresentedPlayerDisconnected(seenRemotePlayerIds, *parentCell, *player);
if (fallbackCandidate) {