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
|
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
|
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
|
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
|
14. Proxy visual updates are independent per slot. Normal movement is smoothed
|
||||||
toward that player’s latest target, while `cell_change`, `worldspace_change`,
|
toward that player’s latest target, while `cell_change`, `worldspace_change`,
|
||||||
and `teleport` snap directly. Sneak state is still observed and logged only;
|
and `teleport` snap directly. Sneak state is still observed and logged only;
|
||||||
no visual crouch/sneak application is attempted.
|
no visual crouch/sneak application is attempted.
|
||||||
15. When a remote player leaves the cell or disconnects, only that player’s
|
15. When a remote player leaves the cell or disconnects, only that player’s
|
||||||
runtime proxy is moved to the hidden holding position inside
|
runtime proxy is moved to the hidden holding position inside
|
||||||
`F4TTestCell01`. Runtime actors are not disabled, deleted, or reassigned to a
|
`F4TTestCell01`. Runtime actors are not disabled or deleted. Connected
|
||||||
different `playerId` during Stage 4.
|
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
|
16. The placed fallback reference `F4TProxyRemotePlayer01REF` remains a single
|
||||||
backup representation. If runtime spawning fails for one deterministic
|
backup representation. If runtime spawning fails for one deterministic
|
||||||
selected remote player, the fallback can represent that one player only; it
|
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
|
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.
|
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
|
The placed proxy reference remains as a single fallback path if runtime spawning
|
||||||
fails for one selected remote player.
|
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
|
for one selected player only if runtime spawning fails
|
||||||
- Holding only the affected proxy actor at a hidden in-cell position when its
|
- Holding only the affected proxy actor at a hidden in-cell position when its
|
||||||
remote player disconnects or leaves the same cell
|
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
|
- Logging staged runtime proxy actor base lookup diagnostics on the same
|
||||||
game-thread controller path
|
game-thread controller path
|
||||||
- Attempting one game-thread-only PlaceAtMe-backed runtime spawn from the
|
- Attempting one game-thread-only PlaceAtMe-backed runtime spawn from the
|
||||||
selected Stage 3 actor base in `F4TTestCell01`, normally
|
selected Stage 3 actor base in `F4TTestCell01`, normally
|
||||||
`F4T_RemotePlayerProxy`
|
`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
|
- Logging temporary Stage 3.1 visibility and movement diagnostics for the single
|
||||||
runtime proxy, including active proxy source, runtime FormID, local distance,
|
runtime proxy, including active proxy source, runtime FormID, local distance,
|
||||||
and safe loaded-3D checks
|
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 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 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 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 stores promoted runtime actors as ObjectRefHandle values in slots
|
||||||
Game update moves each valid proxy from its assigned remote player state
|
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 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
|
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
|
## Entry Template
|
||||||
|
|
||||||
Use this format for future updates:
|
Use this format for future updates:
|
||||||
|
|||||||
@@ -56,9 +56,12 @@ namespace
|
|||||||
constexpr auto kDebugRuntimeProxyScale = 1.50F;
|
constexpr auto kDebugRuntimeProxyScale = 1.50F;
|
||||||
constexpr float kProxyPositionLerpAlpha = 0.15F;
|
constexpr float kProxyPositionLerpAlpha = 0.15F;
|
||||||
constexpr std::size_t kMaxRuntimeProxyActors = 4;
|
constexpr std::size_t kMaxRuntimeProxyActors = 4;
|
||||||
|
constexpr float kDisconnectedProxyReuseDelaySeconds = 30.0F;
|
||||||
// Keep inactive proxies inside the loaded test cell instead of disabling actor
|
// Keep inactive proxies inside the loaded test cell instead of disabling actor
|
||||||
// refs before a safe lifecycle cleanup path is validated in-game.
|
// 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 kSafetyOffsetMovementInterval = 200ms;
|
||||||
constexpr auto kWarningLogInterval = 5s;
|
constexpr auto kWarningLogInterval = 5s;
|
||||||
constexpr auto kProxyMovementDiagnosticLogInterval = 2s;
|
constexpr auto kProxyMovementDiagnosticLogInterval = 2s;
|
||||||
@@ -110,11 +113,12 @@ namespace
|
|||||||
|
|
||||||
enum class ProxyLifecycleState
|
enum class ProxyLifecycleState
|
||||||
{
|
{
|
||||||
kNoRemotePlayer,
|
kHeldNoRemote,
|
||||||
kRepresentingRemotePlayer,
|
kActive,
|
||||||
kRemotePlayerDisconnected,
|
kHeldDisconnected,
|
||||||
kRemotePlayerLeftCell,
|
kHeldLeftCell,
|
||||||
kProxyHiddenOrIdle
|
kReusable,
|
||||||
|
kSpawnFailed
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class ProxyMovementDiagnosticMode
|
enum class ProxyMovementDiagnosticMode
|
||||||
@@ -177,16 +181,21 @@ namespace
|
|||||||
struct ProxyActorSlot
|
struct ProxyActorSlot
|
||||||
{
|
{
|
||||||
std::uint32_t remotePlayerId{};
|
std::uint32_t remotePlayerId{};
|
||||||
|
std::uint32_t holdingPositionIndex{};
|
||||||
RE::ObjectRefHandle proxyHandle;
|
RE::ObjectRefHandle proxyHandle;
|
||||||
RE::ObjectRefHandle pendingSpawnHandle;
|
RE::ObjectRefHandle pendingSpawnHandle;
|
||||||
ProxyLifecycleState lifecycleState{ ProxyLifecycleState::kNoRemotePlayer };
|
ProxyLifecycleState lifecycleState{ ProxyLifecycleState::kHeldNoRemote };
|
||||||
std::optional<ProxyLifecycleState> holdReason;
|
std::optional<ProxyLifecycleState> holdReason;
|
||||||
|
std::optional<std::uint32_t> previousRemotePlayerId;
|
||||||
PlaceAtMeBackedProxySpawnState spawnState{ PlaceAtMeBackedProxySpawnState::kNotStarted };
|
PlaceAtMeBackedProxySpawnState spawnState{ PlaceAtMeBackedProxySpawnState::kNotStarted };
|
||||||
RuntimeActorBaseSource spawnSource{ RuntimeActorBaseSource::kCustomProxy };
|
RuntimeActorBaseSource spawnSource{ RuntimeActorBaseSource::kCustomProxy };
|
||||||
std::uint32_t spawnBaseFormId{ 0 };
|
std::uint32_t spawnBaseFormId{ 0 };
|
||||||
std::unordered_set<std::uint32_t> preexistingCandidateRefs;
|
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 candidateSearchStartTime{};
|
||||||
std::chrono::steady_clock::time_point settleStartTime{};
|
std::chrono::steady_clock::time_point settleStartTime{};
|
||||||
|
std::chrono::steady_clock::time_point heldSinceTime{};
|
||||||
|
float initialRemoteSpawnHeading{ 0.0F };
|
||||||
bool spawnAttempted{ false };
|
bool spawnAttempted{ false };
|
||||||
bool spawnAttemptInProgress{ false };
|
bool spawnAttemptInProgress{ false };
|
||||||
bool spawnSucceeded{ false };
|
bool spawnSucceeded{ false };
|
||||||
@@ -194,6 +203,8 @@ namespace
|
|||||||
bool restoredForCurrentState{ false };
|
bool restoredForCurrentState{ false };
|
||||||
bool firstSmoothedMovementLogged{ false };
|
bool firstSmoothedMovementLogged{ false };
|
||||||
bool remoteStateMovedLogged{ false };
|
bool remoteStateMovedLogged{ false };
|
||||||
|
bool reusableLogged{ false };
|
||||||
|
bool directRemoteSpawnLogged{ false };
|
||||||
std::optional<bool> lastObservedSneakState;
|
std::optional<bool> lastObservedSneakState;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,7 +227,7 @@ namespace
|
|||||||
bool g_proxySmoothingEnabledLogged = false;
|
bool g_proxySmoothingEnabledLogged = false;
|
||||||
bool g_firstSmoothedMovementLogged = false;
|
bool g_firstSmoothedMovementLogged = false;
|
||||||
auto g_lastSafetyOffsetMovementTime = std::chrono::steady_clock::time_point{};
|
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<ProxyLifecycleState> g_proxyHoldReason;
|
||||||
std::optional<std::uint32_t> g_proxyHoldPlayerId;
|
std::optional<std::uint32_t> g_proxyHoldPlayerId;
|
||||||
std::optional<ActiveProxyKind> g_proxyHoldKind;
|
std::optional<ActiveProxyKind> g_proxyHoldKind;
|
||||||
@@ -282,6 +293,7 @@ namespace
|
|||||||
RuntimeActorBaseSource g_runtimeProxyActorBaseSource = RuntimeActorBaseSource::kCustomProxy;
|
RuntimeActorBaseSource g_runtimeProxyActorBaseSource = RuntimeActorBaseSource::kCustomProxy;
|
||||||
std::unordered_map<std::uint32_t, ProxyActorSlot> g_proxySlots;
|
std::unordered_map<std::uint32_t, ProxyActorSlot> g_proxySlots;
|
||||||
std::optional<std::uint32_t> g_runtimeProxySpawnAttemptPlayerId;
|
std::optional<std::uint32_t> g_runtimeProxySpawnAttemptPlayerId;
|
||||||
|
std::uint32_t g_nextProxyHoldingPositionIndex = 0;
|
||||||
bool g_stage4StartedLogged = false;
|
bool g_stage4StartedLogged = false;
|
||||||
bool g_fallbackRuntimeUnavailableLogged = false;
|
bool g_fallbackRuntimeUnavailableLogged = false;
|
||||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_lastWarningLogTimes;
|
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,
|
std::optional<std::uint32_t> a_playerId,
|
||||||
ActiveProxyKind a_proxyKind)
|
ActiveProxyKind a_proxyKind)
|
||||||
{
|
{
|
||||||
return g_proxyLifecycleState == ProxyLifecycleState::kProxyHiddenOrIdle &&
|
return g_proxyLifecycleState == a_reason &&
|
||||||
g_proxyHoldReason == a_reason &&
|
g_proxyHoldReason == a_reason &&
|
||||||
g_proxyHoldPlayerId == a_playerId &&
|
g_proxyHoldPlayerId == a_playerId &&
|
||||||
g_proxyHoldKind == a_proxyKind;
|
g_proxyHoldKind == a_proxyKind;
|
||||||
@@ -451,12 +463,12 @@ namespace
|
|||||||
void LogProxyStillHeld(ProxyLifecycleState a_reason, std::optional<std::uint32_t> a_playerId)
|
void LogProxyStillHeld(ProxyLifecycleState a_reason, std::optional<std::uint32_t> a_playerId)
|
||||||
{
|
{
|
||||||
switch (a_reason) {
|
switch (a_reason) {
|
||||||
case ProxyLifecycleState::kNoRemotePlayer:
|
case ProxyLifecycleState::kHeldNoRemote:
|
||||||
LogThrottledInfo(
|
LogThrottledInfo(
|
||||||
"proxy_held_no_remote_player",
|
"proxy_held_no_remote_player",
|
||||||
"No suitable remote player is available; proxy actor is already at the holding position.");
|
"No suitable remote player is available; proxy actor is already at the holding position.");
|
||||||
break;
|
break;
|
||||||
case ProxyLifecycleState::kRemotePlayerDisconnected:
|
case ProxyLifecycleState::kHeldDisconnected:
|
||||||
if (a_playerId) {
|
if (a_playerId) {
|
||||||
LogThrottledInfo(
|
LogThrottledInfo(
|
||||||
"proxy_held_disconnected_" + std::to_string(*a_playerId),
|
"proxy_held_disconnected_" + std::to_string(*a_playerId),
|
||||||
@@ -465,7 +477,7 @@ namespace
|
|||||||
*a_playerId));
|
*a_playerId));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ProxyLifecycleState::kRemotePlayerLeftCell:
|
case ProxyLifecycleState::kHeldLeftCell:
|
||||||
if (a_playerId) {
|
if (a_playerId) {
|
||||||
LogThrottledInfo(
|
LogThrottledInfo(
|
||||||
"proxy_held_left_cell_" + std::to_string(*a_playerId),
|
"proxy_held_left_cell_" + std::to_string(*a_playerId),
|
||||||
@@ -491,7 +503,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
a_proxy.SetPosition(kProxyHiddenHoldingPosition, true);
|
a_proxy.SetPosition(kProxyHiddenHoldingPosition, true);
|
||||||
g_proxyLifecycleState = ProxyLifecycleState::kProxyHiddenOrIdle;
|
g_proxyLifecycleState = a_reason;
|
||||||
g_proxyHoldReason = a_reason;
|
g_proxyHoldReason = a_reason;
|
||||||
g_proxyHoldPlayerId = a_playerId;
|
g_proxyHoldPlayerId = a_playerId;
|
||||||
g_proxyHoldKind = a_proxyKind;
|
g_proxyHoldKind = a_proxyKind;
|
||||||
@@ -500,12 +512,12 @@ namespace
|
|||||||
ResetRemoteMovementStateTracking();
|
ResetRemoteMovementStateTracking();
|
||||||
|
|
||||||
switch (a_reason) {
|
switch (a_reason) {
|
||||||
case ProxyLifecycleState::kNoRemotePlayer:
|
case ProxyLifecycleState::kHeldNoRemote:
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"No suitable remote player is available; moving {} to holding position.",
|
"No suitable remote player is available; moving {} to holding position.",
|
||||||
GetActiveProxyLogName(a_proxyKind)));
|
GetActiveProxyLogName(a_proxyKind)));
|
||||||
break;
|
break;
|
||||||
case ProxyLifecycleState::kRemotePlayerDisconnected:
|
case ProxyLifecycleState::kHeldDisconnected:
|
||||||
if (a_playerId) {
|
if (a_playerId) {
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Remote player {} disappeared; moving {} to holding position.",
|
"Remote player {} disappeared; moving {} to holding position.",
|
||||||
@@ -513,7 +525,7 @@ namespace
|
|||||||
GetActiveProxyLogName(a_proxyKind)));
|
GetActiveProxyLogName(a_proxyKind)));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ProxyLifecycleState::kRemotePlayerLeftCell:
|
case ProxyLifecycleState::kHeldLeftCell:
|
||||||
if (a_playerId) {
|
if (a_playerId) {
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Remote player {} is not in the same cell; moving {} to holding position.",
|
"Remote player {} is not in the same cell; moving {} to holding position.",
|
||||||
@@ -2517,6 +2529,48 @@ namespace
|
|||||||
return candidates;
|
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(
|
bool IsSlotSpawnCandidateActor(
|
||||||
const ProxyActorSlot& a_slot,
|
const ProxyActorSlot& a_slot,
|
||||||
const RE::Actor& a_actor,
|
const RE::Actor& a_actor,
|
||||||
@@ -2723,6 +2777,7 @@ namespace
|
|||||||
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kFailed;
|
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kFailed;
|
||||||
a_slot.spawnAttemptInProgress = false;
|
a_slot.spawnAttemptInProgress = false;
|
||||||
a_slot.spawnFailed = true;
|
a_slot.spawnFailed = true;
|
||||||
|
a_slot.lifecycleState = ProxyLifecycleState::kSpawnFailed;
|
||||||
a_slot.pendingSpawnHandle.reset();
|
a_slot.pendingSpawnHandle.reset();
|
||||||
if (g_runtimeProxySpawnAttemptPlayerId == a_slot.remotePlayerId) {
|
if (g_runtimeProxySpawnAttemptPlayerId == a_slot.remotePlayerId) {
|
||||||
g_runtimeProxySpawnAttemptPlayerId.reset();
|
g_runtimeProxySpawnAttemptPlayerId.reset();
|
||||||
@@ -2731,6 +2786,7 @@ namespace
|
|||||||
|
|
||||||
void StartSlotPlaceAtMeSettle(ProxyActorSlot& a_slot, RE::Actor& a_actor)
|
void StartSlotPlaceAtMeSettle(ProxyActorSlot& a_slot, RE::Actor& a_actor)
|
||||||
{
|
{
|
||||||
|
ApplySlotInitialRemoteSpawnTransform(a_slot, a_actor);
|
||||||
a_slot.pendingSpawnHandle = a_actor.GetHandle();
|
a_slot.pendingSpawnHandle = a_actor.GetHandle();
|
||||||
a_slot.settleStartTime = std::chrono::steady_clock::now();
|
a_slot.settleStartTime = std::chrono::steady_clock::now();
|
||||||
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kSettling;
|
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kSettling;
|
||||||
@@ -2755,14 +2811,17 @@ namespace
|
|||||||
isVisible));
|
isVisible));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplySlotInitialRemoteSpawnTransform(a_slot, a_actor);
|
||||||
a_slot.proxyHandle = a_actor.GetHandle();
|
a_slot.proxyHandle = a_actor.GetHandle();
|
||||||
a_slot.pendingSpawnHandle.reset();
|
a_slot.pendingSpawnHandle.reset();
|
||||||
a_slot.spawnSucceeded = true;
|
a_slot.spawnSucceeded = true;
|
||||||
a_slot.spawnAttemptInProgress = false;
|
a_slot.spawnAttemptInProgress = false;
|
||||||
a_slot.spawnFailed = false;
|
a_slot.spawnFailed = false;
|
||||||
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kPromoted;
|
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kPromoted;
|
||||||
a_slot.lifecycleState = ProxyLifecycleState::kProxyHiddenOrIdle;
|
a_slot.lifecycleState = ProxyLifecycleState::kHeldNoRemote;
|
||||||
a_slot.holdReason = ProxyLifecycleState::kProxyHiddenOrIdle;
|
a_slot.holdReason = ProxyLifecycleState::kHeldNoRemote;
|
||||||
|
a_slot.heldSinceTime = {};
|
||||||
|
a_slot.reusableLogged = false;
|
||||||
a_slot.restoredForCurrentState = false;
|
a_slot.restoredForCurrentState = false;
|
||||||
if (g_runtimeProxySpawnAttemptPlayerId == a_slot.remotePlayerId) {
|
if (g_runtimeProxySpawnAttemptPlayerId == a_slot.remotePlayerId) {
|
||||||
g_runtimeProxySpawnAttemptPlayerId.reset();
|
g_runtimeProxySpawnAttemptPlayerId.reset();
|
||||||
@@ -3490,7 +3549,7 @@ namespace
|
|||||||
|
|
||||||
a_proxy.SetPosition(targetPosition, true);
|
a_proxy.SetPosition(targetPosition, true);
|
||||||
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
||||||
g_proxyLifecycleState = ProxyLifecycleState::kRepresentingRemotePlayer;
|
g_proxyLifecycleState = ProxyLifecycleState::kActive;
|
||||||
g_proxyHoldReason.reset();
|
g_proxyHoldReason.reset();
|
||||||
g_proxyHoldPlayerId.reset();
|
g_proxyHoldPlayerId.reset();
|
||||||
g_proxyHoldKind.reset();
|
g_proxyHoldKind.reset();
|
||||||
@@ -3602,38 +3661,103 @@ namespace
|
|||||||
a_slot.spawnAttemptInProgress = false;
|
a_slot.spawnAttemptInProgress = false;
|
||||||
a_slot.spawnSucceeded = false;
|
a_slot.spawnSucceeded = false;
|
||||||
a_slot.restoredForCurrentState = false;
|
a_slot.restoredForCurrentState = false;
|
||||||
|
if (a_slot.lifecycleState != ProxyLifecycleState::kHeldDisconnected) {
|
||||||
|
a_slot.heldSinceTime = {};
|
||||||
|
a_slot.reusableLogged = false;
|
||||||
|
}
|
||||||
if (g_runtimeProxySpawnAttemptPlayerId == a_slot.remotePlayerId) {
|
if (g_runtimeProxySpawnAttemptPlayerId == a_slot.remotePlayerId) {
|
||||||
g_runtimeProxySpawnAttemptPlayerId.reset();
|
g_runtimeProxySpawnAttemptPlayerId.reset();
|
||||||
}
|
}
|
||||||
return nullptr;
|
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,
|
ProxyActorSlot& a_slot,
|
||||||
RE::Actor& a_proxy,
|
RE::Actor& a_proxy,
|
||||||
ProxyLifecycleState a_reason)
|
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);
|
LogProxyStillHeld(a_reason, a_slot.remotePlayerId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
a_proxy.SetPosition(kProxyHiddenHoldingPosition, true);
|
a_proxy.SetPosition(GetSlotHoldingPosition(a_slot), true);
|
||||||
a_slot.lifecycleState = ProxyLifecycleState::kProxyHiddenOrIdle;
|
a_slot.lifecycleState = a_reason;
|
||||||
a_slot.holdReason = a_reason;
|
a_slot.holdReason = a_reason;
|
||||||
a_slot.restoredForCurrentState = false;
|
ResetSlotMovementState(a_slot);
|
||||||
a_slot.firstSmoothedMovementLogged = false;
|
ResetSlotObservationState(a_slot);
|
||||||
a_slot.remoteStateMovedLogged = false;
|
if (a_reason == ProxyLifecycleState::kHeldDisconnected) {
|
||||||
a_slot.lastObservedSneakState.reset();
|
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) {
|
switch (a_reason) {
|
||||||
case ProxyLifecycleState::kRemotePlayerDisconnected:
|
case ProxyLifecycleState::kHeldDisconnected:
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Remote player {} disappeared; moving runtime proxy actor {:08X} to holding position.",
|
"Remote player {} disappeared; moving runtime proxy actor {:08X} to holding position.",
|
||||||
a_slot.remotePlayerId,
|
a_slot.remotePlayerId,
|
||||||
a_proxy.GetFormID()));
|
a_proxy.GetFormID()));
|
||||||
break;
|
break;
|
||||||
case ProxyLifecycleState::kRemotePlayerLeftCell:
|
case ProxyLifecycleState::kHeldLeftCell:
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Remote player {} is not in the same cell; moving runtime proxy actor {:08X} to holding position.",
|
"Remote player {} is not in the same cell; moving runtime proxy actor {:08X} to holding position.",
|
||||||
a_slot.remotePlayerId,
|
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(
|
void RestoreSlotProxyForRemotePlayer(
|
||||||
ProxyActorSlot& a_slot,
|
ProxyActorSlot& a_slot,
|
||||||
RE::Actor& a_proxy,
|
RE::Actor& a_proxy,
|
||||||
@@ -3662,8 +3825,10 @@ namespace
|
|||||||
|
|
||||||
a_proxy.SetPosition(targetPosition, true);
|
a_proxy.SetPosition(targetPosition, true);
|
||||||
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
||||||
a_slot.lifecycleState = ProxyLifecycleState::kRepresentingRemotePlayer;
|
a_slot.lifecycleState = ProxyLifecycleState::kActive;
|
||||||
a_slot.holdReason.reset();
|
a_slot.holdReason.reset();
|
||||||
|
a_slot.heldSinceTime = {};
|
||||||
|
a_slot.reusableLogged = false;
|
||||||
a_slot.restoredForCurrentState = true;
|
a_slot.restoredForCurrentState = true;
|
||||||
a_slot.firstSmoothedMovementLogged = false;
|
a_slot.firstSmoothedMovementLogged = false;
|
||||||
a_slot.remoteStateMovedLogged = false;
|
a_slot.remoteStateMovedLogged = false;
|
||||||
@@ -3720,8 +3885,10 @@ namespace
|
|||||||
|
|
||||||
a_proxy.SetPosition(nextPosition, true);
|
a_proxy.SetPosition(nextPosition, true);
|
||||||
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
||||||
a_slot.lifecycleState = ProxyLifecycleState::kRepresentingRemotePlayer;
|
a_slot.lifecycleState = ProxyLifecycleState::kActive;
|
||||||
a_slot.holdReason.reset();
|
a_slot.holdReason.reset();
|
||||||
|
a_slot.heldSinceTime = {};
|
||||||
|
a_slot.reusableLogged = false;
|
||||||
|
|
||||||
if (shouldSnap) {
|
if (shouldSnap) {
|
||||||
LogThrottledInfo(
|
LogThrottledInfo(
|
||||||
@@ -3770,33 +3937,127 @@ namespace
|
|||||||
return slot != g_proxySlots.end() ? std::addressof(slot->second) : nullptr;
|
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 static_cast<std::size_t>(std::ranges::count_if(
|
||||||
return existingSlot;
|
g_proxySlots,
|
||||||
|
[a_state](const auto& a_entry) {
|
||||||
|
return a_entry.second.lifecycleState == a_state;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_proxySlots.size() >= kMaxRuntimeProxyActors) {
|
std::optional<std::uint32_t> FindReusableSlot()
|
||||||
LogThrottledWarning(
|
{
|
||||||
"stage4_max_runtime_proxy_limit",
|
std::optional<std::uint32_t> reusablePlayerId;
|
||||||
std::format(
|
for (const auto& [playerId, slot] : g_proxySlots) {
|
||||||
"Runtime proxy max limit reached: max={}, activeSlots={}. Remote player {} will not receive a visual proxy yet.",
|
if (slot.lifecycleState != ProxyLifecycleState::kReusable) {
|
||||||
kMaxRuntimeProxyActors,
|
continue;
|
||||||
g_proxySlots.size(),
|
}
|
||||||
a_remotePlayerId));
|
|
||||||
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto [slot, inserted] = g_proxySlots.emplace(a_remotePlayerId, ProxyActorSlot{});
|
auto& slotValue = node.mapped();
|
||||||
slot->second.remotePlayerId = a_remotePlayerId;
|
const auto previousRemotePlayerId = slotValue.remotePlayerId;
|
||||||
if (inserted) {
|
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(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Runtime proxy slot created for remote player {}.",
|
"Reusable runtime proxy slot reassigned from disconnected remote player {} to remote player {}.",
|
||||||
a_remotePlayerId));
|
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);
|
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(
|
bool UpdateRuntimeSlotForRemotePlayer(
|
||||||
ProxyActorSlot& a_slot,
|
ProxyActorSlot& a_slot,
|
||||||
const RemotePlayer& a_remotePlayer,
|
const RemotePlayer& a_remotePlayer,
|
||||||
@@ -3804,6 +4065,7 @@ namespace
|
|||||||
const RE::PlayerCharacter& a_player,
|
const RE::PlayerCharacter& a_player,
|
||||||
bool a_sameCellPassed)
|
bool a_sameCellPassed)
|
||||||
{
|
{
|
||||||
|
CaptureSlotInitialRemoteSpawnTransform(a_slot, a_remotePlayer);
|
||||||
auto* proxy = TryResolveSlotProxy(a_slot, a_parentCell, a_player);
|
auto* proxy = TryResolveSlotProxy(a_slot, a_parentCell, a_player);
|
||||||
if (!proxy) {
|
if (!proxy) {
|
||||||
MaintainSlotPlaceAtMeSpawn(a_slot, a_parentCell, a_player);
|
MaintainSlotPlaceAtMeSpawn(a_slot, a_parentCell, a_player);
|
||||||
@@ -3814,7 +4076,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!a_slot.restoredForCurrentState ||
|
if (!a_slot.restoredForCurrentState ||
|
||||||
a_slot.lifecycleState != ProxyLifecycleState::kRepresentingRemotePlayer ||
|
a_slot.lifecycleState != ProxyLifecycleState::kActive ||
|
||||||
ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {
|
ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {
|
||||||
RestoreSlotProxyForRemotePlayer(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
|
RestoreSlotProxyForRemotePlayer(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
|
||||||
return true;
|
return true;
|
||||||
@@ -3834,13 +4096,31 @@ namespace
|
|||||||
if (a_seenRemotePlayerIds.contains(playerId)) {
|
if (a_seenRemotePlayerIds.contains(playerId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (slot.lifecycleState == ProxyLifecycleState::kReusable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (slot.spawnAttemptInProgress || slot.spawnState == PlaceAtMeBackedProxySpawnState::kSettling) {
|
if (slot.spawnAttemptInProgress || slot.spawnState == PlaceAtMeBackedProxySpawnState::kSettling) {
|
||||||
MaintainSlotPlaceAtMeSpawn(slot, a_parentCell, a_player);
|
MaintainSlotPlaceAtMeSpawn(slot, a_parentCell, a_player);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto* proxy = TryResolveSlotProxy(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)) {
|
if (auto* fallbackProxy = ResolveProxy(a_parentCell, a_player)) {
|
||||||
MoveProxyToHoldingPosition(
|
MoveProxyToHoldingPosition(
|
||||||
*fallbackProxy,
|
*fallbackProxy,
|
||||||
ProxyLifecycleState::kRemotePlayerDisconnected,
|
ProxyLifecycleState::kHeldDisconnected,
|
||||||
g_representedPlayerId,
|
g_representedPlayerId,
|
||||||
ActiveProxyKind::kPlacedFallback);
|
ActiveProxyKind::kPlacedFallback);
|
||||||
}
|
}
|
||||||
@@ -3885,7 +4165,7 @@ namespace
|
|||||||
const auto representedPlayerChanged = g_representedPlayerId != a_remotePlayer.playerId;
|
const auto representedPlayerChanged = g_representedPlayerId != a_remotePlayer.playerId;
|
||||||
g_representedPlayerId = a_remotePlayer.playerId;
|
g_representedPlayerId = a_remotePlayer.playerId;
|
||||||
if (representedPlayerChanged ||
|
if (representedPlayerChanged ||
|
||||||
g_proxyLifecycleState != ProxyLifecycleState::kRepresentingRemotePlayer ||
|
g_proxyLifecycleState != ProxyLifecycleState::kActive ||
|
||||||
ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {
|
ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {
|
||||||
RestoreProxyForRemotePlayer(
|
RestoreProxyForRemotePlayer(
|
||||||
*fallbackProxy,
|
*fallbackProxy,
|
||||||
@@ -3949,6 +4229,7 @@ namespace F4T::ProxyActorController
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto remotePlayers = GetSortedRemotePlayerSnapshot();
|
const auto remotePlayers = GetSortedRemotePlayerSnapshot();
|
||||||
|
MarkReusableSlots();
|
||||||
std::unordered_set<std::uint32_t> seenRemotePlayerIds;
|
std::unordered_set<std::uint32_t> seenRemotePlayerIds;
|
||||||
seenRemotePlayerIds.reserve(remotePlayers.size());
|
seenRemotePlayerIds.reserve(remotePlayers.size());
|
||||||
std::optional<RemotePlayer> fallbackCandidate;
|
std::optional<RemotePlayer> fallbackCandidate;
|
||||||
@@ -3960,7 +4241,7 @@ namespace F4T::ProxyActorController
|
|||||||
if (!HasValidRemoteTransform(remotePlayer)) {
|
if (!HasValidRemoteTransform(remotePlayer)) {
|
||||||
if (auto* slot = FindSlot(remotePlayer.playerId)) {
|
if (auto* slot = FindSlot(remotePlayer.playerId)) {
|
||||||
if (auto* proxy = TryResolveSlotProxy(*slot, *parentCell, *player)) {
|
if (auto* proxy = TryResolveSlotProxy(*slot, *parentCell, *player)) {
|
||||||
MoveSlotProxyToHoldingPosition(*slot, *proxy, ProxyLifecycleState::kNoRemotePlayer);
|
MarkSlotHeld(*slot, *proxy, ProxyLifecycleState::kHeldNoRemote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -3970,13 +4251,13 @@ namespace F4T::ProxyActorController
|
|||||||
if (!sameCellPassed) {
|
if (!sameCellPassed) {
|
||||||
if (auto* slot = FindSlot(remotePlayer.playerId)) {
|
if (auto* slot = FindSlot(remotePlayer.playerId)) {
|
||||||
if (auto* proxy = TryResolveSlotProxy(*slot, *parentCell, *player)) {
|
if (auto* proxy = TryResolveSlotProxy(*slot, *parentCell, *player)) {
|
||||||
MoveSlotProxyToHoldingPosition(*slot, *proxy, ProxyLifecycleState::kRemotePlayerLeftCell);
|
MarkSlotHeld(*slot, *proxy, ProxyLifecycleState::kHeldLeftCell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* slot = GetOrCreateSlot(remotePlayer.playerId);
|
auto* slot = GetOrCreateSlot(remotePlayer, *parentCell, *player, sameCellPassed);
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -3991,7 +4272,7 @@ namespace F4T::ProxyActorController
|
|||||||
if (auto* fallbackProxy = ResolveProxy(*parentCell, *player)) {
|
if (auto* fallbackProxy = ResolveProxy(*parentCell, *player)) {
|
||||||
MoveProxyToHoldingPosition(
|
MoveProxyToHoldingPosition(
|
||||||
*fallbackProxy,
|
*fallbackProxy,
|
||||||
ProxyLifecycleState::kNoRemotePlayer,
|
ProxyLifecycleState::kHeldNoRemote,
|
||||||
g_representedPlayerId,
|
g_representedPlayerId,
|
||||||
ActiveProxyKind::kPlacedFallback);
|
ActiveProxyKind::kPlacedFallback);
|
||||||
}
|
}
|
||||||
@@ -4005,6 +4286,8 @@ namespace F4T::ProxyActorController
|
|||||||
}
|
}
|
||||||
|
|
||||||
HoldDisconnectedSlots(seenRemotePlayerIds, *parentCell, *player);
|
HoldDisconnectedSlots(seenRemotePlayerIds, *parentCell, *player);
|
||||||
|
MarkReusableSlots();
|
||||||
|
CorrectHeldSlotPositions(*parentCell, *player);
|
||||||
HoldFallbackIfRepresentedPlayerDisconnected(seenRemotePlayerIds, *parentCell, *player);
|
HoldFallbackIfRepresentedPlayerDisconnected(seenRemotePlayerIds, *parentCell, *player);
|
||||||
|
|
||||||
if (fallbackCandidate) {
|
if (fallbackCandidate) {
|
||||||
|
|||||||
Reference in New Issue
Block a user