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