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:
+24
-3
@@ -37,15 +37,21 @@ both send and receive paths:
|
||||
13. Runtime proxies are demand-driven. A slot executes the Stage 3.7
|
||||
PlaceAtMe-backed spawn flow only when that remote player needs a same-cell
|
||||
representation, and only one PlaceAtMe spawn attempt may run at a time so
|
||||
candidate isolation remains unambiguous.
|
||||
candidate isolation remains unambiguous. Stage 4 captures the remote
|
||||
player's current valid transform before spawning and moves the isolated
|
||||
runtime actor to that transform immediately, so normal remote representation
|
||||
does not visually start at the local player offset.
|
||||
14. Proxy visual updates are independent per slot. Normal movement is smoothed
|
||||
toward that player’s latest target, while `cell_change`, `worldspace_change`,
|
||||
and `teleport` snap directly. Sneak state is still observed and logged only;
|
||||
no visual crouch/sneak application is attempted.
|
||||
15. When a remote player leaves the cell or disconnects, only that player’s
|
||||
runtime proxy is moved to the hidden holding position inside
|
||||
`F4TTestCell01`. Runtime actors are not disabled, deleted, or reassigned to a
|
||||
different `playerId` during Stage 4.
|
||||
`F4TTestCell01`. Runtime actors are not disabled or deleted. Connected
|
||||
players who leave the cell keep their slot reserved, while disconnected
|
||||
held slots become reusable only after the configured grace period. Runtime
|
||||
slots use per-slot holding positions with spacing and a game-thread
|
||||
correction check so held actors do not stack or drift back into view.
|
||||
16. The placed fallback reference `F4TProxyRemotePlayer01REF` remains a single
|
||||
backup representation. If runtime spawning fails for one deterministic
|
||||
selected remote player, the fallback can represent that one player only; it
|
||||
@@ -96,6 +102,9 @@ both send and receive paths:
|
||||
|
||||
The Fallout 4 plugin now dynamically spawns one runtime proxy actor per
|
||||
represented remote `playerId` in the test cell, up to a small Stage 4 test cap.
|
||||
Disconnected held slots can be reassigned to later server `playerId` values after
|
||||
the grace period, but slots held for connected players outside the cell are not
|
||||
reused.
|
||||
The placed proxy reference remains as a single fallback path if runtime spawning
|
||||
fails for one selected remote player.
|
||||
|
||||
@@ -117,11 +126,19 @@ The native plugin is responsible for:
|
||||
for one selected player only if runtime spawning fails
|
||||
- Holding only the affected proxy actor at a hidden in-cell position when its
|
||||
remote player disconnects or leaves the same cell
|
||||
- Assigning each runtime proxy slot a unique hidden holding position and
|
||||
correcting held actors back to that position if collision, AI, or physics drift
|
||||
moves them away
|
||||
- Reusing only disconnected runtime proxy slots after a grace period; slots held
|
||||
because a still-connected remote player left the cell remain reserved for that
|
||||
original `playerId`
|
||||
- Logging staged runtime proxy actor base lookup diagnostics on the same
|
||||
game-thread controller path
|
||||
- Attempting one game-thread-only PlaceAtMe-backed runtime spawn from the
|
||||
selected Stage 3 actor base in `F4TTestCell01`, normally
|
||||
`F4T_RemotePlayerProxy`
|
||||
- Capturing the remote player's current valid transform before Stage 4 runtime
|
||||
spawn and applying it as soon as the PlaceAtMe-backed actor is isolated
|
||||
- Logging temporary Stage 3.1 visibility and movement diagnostics for the single
|
||||
runtime proxy, including active proxy source, runtime FormID, local distance,
|
||||
and safe loaded-3D checks
|
||||
@@ -200,9 +217,13 @@ Game update validates transform data and same-cell compatibility
|
||||
Game update creates or reuses a runtime proxy slot per eligible remote playerId
|
||||
Game update attempts at most one PlaceAtMe-backed runtime actor spawn at a time
|
||||
Game update snapshots matching candidates, isolates the new actor, and waits for the settle window
|
||||
Game update moves isolated Stage 4 runtime spawn candidates to the remote player's current transform
|
||||
Game update stores promoted runtime actors as ObjectRefHandle values in slots
|
||||
Game update moves each valid proxy from its assigned remote player state
|
||||
Game update holds only proxies whose assigned remote player left the cell or disappeared
|
||||
Game update corrects held proxy slots back to their per-slot hidden positions
|
||||
Game update marks disconnected held slots reusable after the grace period
|
||||
Game update may reassign reusable disconnected slots to later server playerIds
|
||||
Game update may use F4TProxyRemotePlayer01REF as one placed fallback if runtime spawning fails
|
||||
```
|
||||
|
||||
|
||||
+175
@@ -1903,6 +1903,181 @@ proxy actors only when valid same-cell remote state needs representation.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03 - Runtime Proxy Manager Cleanup and Stability Pass
|
||||
|
||||
### Summary
|
||||
|
||||
Cleaned up the Stage 4 runtime proxy slot lifecycle so disconnected held slots
|
||||
can become reusable after a grace period instead of occupying the small runtime
|
||||
proxy cap forever.
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `plugin/src/F4TProxyActorController.cpp`
|
||||
- `docs/architecture.md`
|
||||
- `docs/dev-log.md`
|
||||
|
||||
### Details
|
||||
|
||||
- Refined runtime proxy slot lifecycle states to distinguish active, held because
|
||||
no valid remote transform, held because the remote player left the cell, held
|
||||
because the remote player disconnected/disappeared, reusable, and spawn-failed
|
||||
slots.
|
||||
- Added a 30 second disconnected-slot reuse delay. Disconnected slots are first
|
||||
moved to the holding position and marked held; only after the delay do they
|
||||
become reusable.
|
||||
- Updated slot acquisition to prefer an existing slot for the same `playerId`,
|
||||
create a new slot while under `kMaxRuntimeProxyActors`, and reuse an eligible
|
||||
disconnected slot at the cap before logging a throttled max-cap warning.
|
||||
- Preserved connected-but-left-cell slots for their original `playerId`, so
|
||||
Leave Cell / Return To Cell keeps the same proxy assignment.
|
||||
- Reset movement smoothing flags and sneak observation state when a reusable slot
|
||||
is reassigned, then snap the proxy to the new remote player's current transform
|
||||
before normal smoothing resumes.
|
||||
- Kept server `playerId` allocation, networking packets, Python server code, fake
|
||||
client tooling, placed fallback behavior, same-cell gating, movement
|
||||
smoothing/snap behavior, and data-only sneak observation unchanged.
|
||||
- Continued to avoid disabling, deleting, alpha-hiding, or otherwise marking
|
||||
runtime actors for deletion; held actors remain at the hidden in-cell holding
|
||||
position.
|
||||
|
||||
### Testing
|
||||
|
||||
- `xmake build` from the repository root prompts because there is no root
|
||||
`xmake.lua`.
|
||||
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
|
||||
`Fallout4Together.dll`.
|
||||
- Pending manual validation: launch Fallout 4 through F4SE, load
|
||||
`Fallout4Together_Test.esp`, `coc F4TTestCell01`, start the Dev Server GUI,
|
||||
add fake clients up to the cap, disconnect clients, wait for the reuse delay,
|
||||
and confirm a later higher `playerId` can reuse an old disconnected slot.
|
||||
- Pending manual validation: confirm Leave Cell / Return To Cell preserves a
|
||||
connected player's original slot and that Walk Circle, Walk To Player,
|
||||
Teleport Test, and sneak-state logging still behave as before.
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Runtime actors are still held instead of safely despawned or deleted.
|
||||
- The placed fallback remains single-player only by design.
|
||||
- In-game validation for this cleanup pass is still pending.
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Run the manual GUI fake-client lifecycle test in `F4TTestCell01`.
|
||||
- If reuse validates cleanly, keep the next milestone focused on diagnostics or
|
||||
safe actor cleanup research rather than gameplay sync expansion.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03 - Held Runtime Proxy Stability Patch
|
||||
|
||||
### Summary
|
||||
|
||||
After manual testing showed some disconnected held runtime proxies could visually
|
||||
reappear even while their remote players were absent, tightened the held-proxy
|
||||
positioning path.
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `plugin/src/F4TProxyActorController.cpp`
|
||||
- `docs/architecture.md`
|
||||
- `docs/dev-log.md`
|
||||
|
||||
### Details
|
||||
|
||||
- Kept the hidden holding-position approach and continued to avoid disabling,
|
||||
deleting, alpha-hiding, or marking actors for deletion.
|
||||
- Moved the shared holding base farther away/lower and gave each runtime proxy
|
||||
slot a stable holding-position index with 500 unit spacing so held actors do
|
||||
not stack on the same coordinates.
|
||||
- Added a game-thread held-position correction pass for `HeldNoRemote`,
|
||||
`HeldLeftCell`, `HeldDisconnected`, and `Reusable` slots. If a held proxy
|
||||
drifts more than 25 units from its assigned holding position, the controller
|
||||
moves it back and logs a throttled correction message.
|
||||
- Preserved disconnected-slot reuse behavior: `HeldDisconnected` still becomes
|
||||
`Reusable` after the delay, reusable slots can still be reassigned, and
|
||||
`HeldLeftCell` slots remain reserved for connected players.
|
||||
- Networking, protocol, Python server code, fake-client tooling, placed fallback
|
||||
behavior, movement smoothing/snap behavior, same-cell gating, and data-only
|
||||
sneak observation were unchanged.
|
||||
|
||||
### Testing
|
||||
|
||||
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
|
||||
`Fallout4Together.dll`.
|
||||
- Pending manual validation: repeat the GUI fake-client disconnect test that
|
||||
previously caused held proxies to visually reappear.
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Runtime actors are still held instead of safely despawned or deleted.
|
||||
- The placed fallback remains single-player only by design.
|
||||
- In-game validation of the held-position correction is still pending.
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Re-test removing all fake clients and confirm held proxies stay out of view.
|
||||
- Re-test reuse after the grace period and Leave Cell / Return To Cell.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03 - Runtime Proxy Spawn Polish Patch
|
||||
|
||||
### Summary
|
||||
|
||||
Polished Stage 4 runtime proxy spawning so newly isolated runtime actors are
|
||||
placed at the remote player's current transform immediately instead of visually
|
||||
starting near the local player before snapping away.
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `plugin/src/F4TProxyActorController.cpp`
|
||||
- `docs/architecture.md`
|
||||
- `docs/dev-log.md`
|
||||
|
||||
### Details
|
||||
|
||||
- Added slot-local initial remote spawn transform tracking for runtime proxy
|
||||
slots.
|
||||
- Captures the remote player's current valid same-cell transform before starting
|
||||
the Stage 4 PlaceAtMe-backed spawn path.
|
||||
- Applies that transform as soon as the new PlaceAtMe-backed actor is isolated
|
||||
and again at promotion, setting both position and heading before normal remote
|
||||
movement resumes.
|
||||
- Reusable slot reassignment continues to snap directly to the newly assigned
|
||||
remote player's current transform and resets movement/sneak observation state.
|
||||
- Kept movement smoothing and snap behavior unchanged: normal movement still
|
||||
lerps after initialization, while `cell_change`, `worldspace_change`, and
|
||||
`teleport` still snap.
|
||||
- Networking, protocol, Python server code, fake-client tooling, placed fallback
|
||||
behavior, held/reusable slot behavior, same-cell gating, and data-only sneak
|
||||
observation were unchanged.
|
||||
|
||||
### Testing
|
||||
|
||||
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
|
||||
`Fallout4Together.dll`.
|
||||
- Pending manual validation: add a GUI fake client and confirm its proxy no
|
||||
longer briefly appears near the local player before moving to the fake client's
|
||||
remote transform.
|
||||
- Pending manual validation: re-test reusable slot reassignment, Walk To Player,
|
||||
Walk Circle, Leave Cell, Return To Cell, disconnect, and reconnect.
|
||||
|
||||
### Known Issues
|
||||
|
||||
- The Stage 4 spawn path still uses PlaceAtMe-backed candidate isolation, so the
|
||||
actor is moved to the remote transform immediately after isolation rather than
|
||||
replacing the underlying diagnostic spawn mechanism.
|
||||
- Runtime actors are still held instead of safely despawned or deleted.
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Manually verify new-client spawn visuals in `F4TTestCell01`.
|
||||
- If a visible local-player flash remains before candidate isolation, investigate
|
||||
replacing the PlaceAtMe-backed Stage 4 spawn backend in a separate milestone.
|
||||
|
||||
---
|
||||
|
||||
## Entry Template
|
||||
|
||||
Use this format for future updates:
|
||||
|
||||
@@ -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,33 +3937,127 @@ 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) {
|
||||
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();
|
||||
}
|
||||
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(
|
||||
"Runtime proxy slot created for remote player {}.",
|
||||
a_remotePlayerId));
|
||||
"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(
|
||||
ProxyActorSlot& a_slot,
|
||||
const RemotePlayer& a_remotePlayer,
|
||||
@@ -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