Add Stage 4 runtime proxy slots (max 4)

Introduce a controller-local runtime proxy slot manager (ProxyActorSlot) and g_proxySlots map capped at kMaxRuntimeProxyActors = 4. Transition the proxy flow from a single active runtime proxy to per-remote-player slots: sorted remote snapshots, per-slot pre-spawn candidate snapshots, PlaceAtMe-backed spawn attempts (one in-progress spawn globally), isolation/settle/promotion of placed refs into slot.proxyHandle, per-slot movement smoothing/hold logic, and validation/restore of handles. Preserve the single placed fallback reference for one selected player and keep slots session-sticky (no reuse/despawn yet). Update docs (architecture.md, dev-log.md) and header/source (F4TProxyActorController.{h,cpp}) to reflect Stage 4 behavior and diagnostics.
This commit is contained in:
2026-06-02 22:49:50 +12:00
parent 37ef52549c
commit b4ec928c67
4 changed files with 976 additions and 175 deletions
+56 -58
View File
@@ -25,77 +25,79 @@ both send and receive paths:
9. The server broadcasts `disconnect` packets when clients disconnect. 9. The server broadcasts `disconnect` packets when clients disconnect.
10. The plugin and fake client remove disconnected players from their remote 10. The plugin and fake client remove disconnected players from their remote
player tables. player tables.
11. On the game-thread update path, the proxy controller performs a one-shot 11. On the game-thread update path, the proxy controller reads a copied
runtime spawn in `F4TTestCell01` from the selected runtime actor base. Stage remote-player snapshot before attempting any actor work. The snapshot is
3.7 makes the preferred runtime spawn strategy PlaceAtMe-backed spawning: sorted by `playerId`, and only remote players with valid transform data in
the plugin snapshots existing matching candidates, executes a single the same cell are eligible for runtime proxy representation.
`player.placeatme <baseFormId> 1` command, isolates the newly spawned actor 12. For each eligible remote `playerId`, the controller maintains a
by comparing pre/post candidate FormIDs, and lets that actor settle before `remotePlayerId -> runtime proxy slot` mapping. Each slot stores its
remote movement begins. The custom `F4T_RemotePlayerProxy` base is attempted `ObjectRefHandle`, lifecycle state, PlaceAtMe-backed spawn/candidate state,
first, with Codsworth retained only as a diagnostic fallback. movement smoothing state, and sneak observation state. Stage 4 caps runtime
12. On the same game-thread update path, the plugin reads a copied remote-player proxy slots at four actors for testing.
snapshot and resolves one active proxy actor. The runtime-spawned actor is 13. Runtime proxies are demand-driven. A slot executes the Stage 3.7
preferred when its handle resolves and validates; otherwise the placed PlaceAtMe-backed spawn flow only when that remote player needs a same-cell
fallback reference `F4TProxyRemotePlayer01REF` is used. Proxy visual updates representation, and only one PlaceAtMe spawn attempt may run at a time so
are independent from local movement log throttling: normal movement is candidate isolation remains unambiguous.
smoothed toward the latest target, while cell changes, worldspace changes, 14. Proxy visual updates are independent per slot. Normal movement is smoothed
and teleports snap directly. toward that players latest target, while `cell_change`, `worldspace_change`,
13. When the selected remote player is missing, disconnected, or not in the same and `teleport` snap directly. Sneak state is still observed and logged only;
test cell, the game-thread proxy controller moves the active proxy to a no visual crouch/sneak application is attempted.
hidden holding position inside `F4TTestCell01` until a valid same-cell 15. When a remote player leaves the cell or disconnects, only that players
remote player is available again. runtime proxy is moved to the hidden holding position inside
14. As part of the staged runtime proxy actor manager work, the same game-thread `F4TTestCell01`. Runtime actors are not disabled, deleted, or reassigned to a
different `playerId` during Stage 4.
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
is not used for multiple remote players.
17. As part of the staged runtime proxy actor manager work, the same game-thread
proxy controller also logs whether `Fallout4Together_Test.esp` is loaded and proxy controller also logs whether `Fallout4Together_Test.esp` is loaded and
whether the actor base `F4T_RemotePlayerProxy` resolves by editor ID. whether the actor base `F4T_RemotePlayerProxy` resolves by editor ID.
15. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy: 18. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy:
post-spawn visibility data, loaded-3D pointer checks where safe, console post-spawn visibility data, loaded-3D pointer checks where safe, console
helper logs, active source logs, and throttled post-movement diagnostics. helper logs, active source logs, and throttled post-movement diagnostics.
The diagnostic mode can force the runtime proxy near the local player to The diagnostic mode can force the runtime proxy near the local player to
prove whether the created actor is visibly renderable. prove whether the created actor is visibly renderable.
16. Stage 3.2 adds a temporary near-player visibility hold for that same single 19. Stage 3.2 adds a temporary near-player visibility hold for that same single
runtime proxy. During the hold, remote state is still selected and validated, runtime proxy. During the hold, remote state is still selected and validated,
but runtime proxy restore and remote movement are deferred long enough to but runtime proxy restore and remote movement are deferred long enough to
test whether a runtime-created actor ref can load visible 3D while kept near test whether a runtime-created actor ref can load visible 3D while kept near
the local player. the local player.
17. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts 20. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
to resolve Codsworth as a visible vanilla `TESNPC` candidate, then passes to resolve Codsworth as a visible vanilla `TESNPC` candidate, then passes
that actor base into the same runtime spawn, handle validation, near-player that actor base into the same runtime spawn, handle validation, near-player
hold, and visibility diagnostic path. This isolates custom actor-base issues hold, and visibility diagnostic path. This isolates custom actor-base issues
from runtime-spawn/init issues without changing the final architecture. from runtime-spawn/init issues without changing the final architecture.
18. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic 21. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic
layer. It compares Codsworth base-form runtime spawning against moving the layer. It compares Codsworth base-form runtime spawning against moving the
existing placed Codsworth reference near the local player as a visibility existing placed Codsworth reference near the local player as a visibility
control. This separates runtime actor creation/init problems from vanilla control. This separates runtime actor creation/init problems from vanilla
actor-base or placed-reference movement problems and does not change the actor-base or placed-reference movement problems and does not change the
final architecture. final architecture.
19. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual 22. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
console validation proved `player.placeatme 000179FF 1` can spawn visible console validation proved `player.placeatme 000179FF 1` can spawn visible
Codsworth in `F4TTestCell01`. The plugin executes that console placement path Codsworth in `F4TTestCell01`. The plugin executes that console placement path
from the local player, resolves the newly created Codsworth actor near the from the local player, resolves the newly created Codsworth actor near the
player, and holds/logs it separately from the `CreateReferenceAtLocation` player, and holds/logs it separately from the `CreateReferenceAtLocation`
runtime proxy path. runtime proxy path.
20. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots 23. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots
known Codsworth refs before executing `player.placeatme 000179FF 1`, accepts known Codsworth refs before executing `player.placeatme 000179FF 1`, accepts
only a newly isolated Codsworth ref for the primary diagnostic, and preserves only a newly isolated Codsworth ref for the primary diagnostic, and preserves
that actor at the original console-spawned location during a short settle that actor at the original console-spawned location during a short settle
window. This tests whether the actor becomes visible naturally or requires a window. This tests whether the actor becomes visible naturally or requires a
render/process refresh such as the manually observed pause-menu or alt-tab render/process refresh such as the manually observed pause-menu or alt-tab
behavior. Stage 3.6 does not change the final architecture. behavior. Stage 3.6 does not change the final architecture.
21. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the 24. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the
preferred single runtime proxy spawn path. The proxy controller uses the preferred single runtime proxy spawn path. The proxy controller uses the
resolved runtime FormID for `F4T_RemotePlayerProxy` when available, falls resolved runtime FormID for `F4T_RemotePlayerProxy` when available, falls
back to Codsworth only for diagnostics, defers active proxy selection during back to Codsworth only for diagnostics, defers active proxy selection during
candidate isolation and settle, then promotes the settled actor into the candidate isolation and settle, then promotes the settled actor into the
existing single-proxy movement pipeline. existing single-proxy movement pipeline.
The Fallout 4 plugin now dynamically spawns one runtime proxy actor for remote The Fallout 4 plugin now dynamically spawns one runtime proxy actor per
player representation in the test cell. The placed proxy reference remains as a represented remote `playerId` in the test cell, up to a small Stage 4 test cap.
fallback path if runtime spawn, handle validation, actor validation, or lookup The placed proxy reference remains as a single fallback path if runtime spawning
fails. fails for one selected remote player.
Stage 4 is still reserved for multiple runtime proxies and a
`remotePlayerId -> proxy actor handle` mapping. Stage 3.7 does not add that
mapping.
## Main Components ## Main Components
@@ -110,11 +112,11 @@ The native plugin is responsible for:
- Throttling normal transform sends separately from readable local movement logs - Throttling normal transform sends separately from readable local movement logs
- Receiving server packets on a background thread - Receiving server packets on a background thread
- Storing assigned and remote `playerId` state internally - Storing assigned and remote `playerId` state internally
- Moving the single active proxy actor from remote-player state on the game - Moving runtime proxy actors from per-`playerId` remote-player state on the
thread, preferring the runtime-spawned actor and falling back to the placed game thread, preferring runtime slots and falling back to the placed reference
reference for one selected player only if runtime spawning fails
- Holding that active proxy actor at a hidden in-cell position when no valid - Holding only the affected proxy actor at a hidden in-cell position when its
same-cell remote player should be represented remote player disconnects or leaves the same cell
- 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
@@ -128,7 +130,7 @@ The native plugin is responsible for:
can load visible 3D before remote movement resumes can load visible 3D before remote movement resumes
- Temporarily comparing a vanilla visible NPC actor base against the custom - Temporarily comparing a vanilla visible NPC actor base against the custom
proxy actor base during Stage 3.3, while preserving the same runtime spawn proxy actor base during Stage 3.3, while preserving the same runtime spawn
path and keeping Stage 4 reserved for later mapping work path before the Stage 4 mapping work
- Temporarily resolving Codsworth base and placed-reference absolute FormIDs - Temporarily resolving Codsworth base and placed-reference absolute FormIDs
during Stage 3.4, then moving the existing placed reference near the player as during Stage 3.4, then moving the existing placed reference near the player as
a visibility control separate from runtime spawning a visibility control separate from runtime spawning
@@ -138,13 +140,10 @@ The native plugin is responsible for:
- Temporarily isolating the newly spawned PlaceAtMe ref during Stage 3.6 and - Temporarily isolating the newly spawned PlaceAtMe ref during Stage 3.6 and
testing render/process settle behavior without changing active proxy mapping testing render/process settle behavior without changing active proxy mapping
- Using Stage 3.7 to isolate a PlaceAtMe-backed runtime proxy, preserve it - Using Stage 3.7 to isolate a PlaceAtMe-backed runtime proxy, preserve it
during a short settle window, and then promote it into the existing during a short settle window, and then promote it into proxy movement
single-proxy movement path - Using Stage 4 to manage one runtime proxy slot per represented remote
`playerId`, with a small max proxy cap and a single PlaceAtMe spawn attempt in
Planned later: progress at a time
- Supporting one proxy actor per represented remote `playerId`
- Mapping each represented remote `playerId` to its own proxy actor handle
### External Server ### External Server
@@ -196,14 +195,15 @@ Networking thread receives packets
Networking thread stores remote player state Networking thread stores remote player state
Game update reads a copied remote player snapshot Game update reads a copied remote player snapshot
Game update resolves/logs proxy actor base diagnostics Game update resolves/logs proxy actor base diagnostics
Game update selects the Stage 3 runtime actor base Game update sorts remote players by playerId
Game update attempts one PlaceAtMe-backed runtime actor spawn in F4TTestCell01 Game update validates transform data and same-cell compatibility
Game update snapshots matching candidates, isolates the new actor, and waits for the Stage 3.7 settle window Game update creates or reuses a runtime proxy slot per eligible remote playerId
Older standalone Stage 3.4/3.6 diagnostics stay disabled while Stage 3.7 is active Game update attempts at most one PlaceAtMe-backed runtime actor spawn at a time
Game update logs temporary Stage 3.1 runtime visibility diagnostics Game update snapshots matching candidates, isolates the new actor, and waits for the settle window
Game update can temporarily hold the Stage 3.2/3.3 runtime proxy near the local player Game update stores promoted runtime actors as ObjectRefHandle values in slots
Game update prefers the validated runtime proxy, or falls back to F4TProxyRemotePlayer01REF Game update moves each valid proxy from its assigned remote player state
Game update moves the selected active proxy actor and logs throttled movement diagnostics Game update holds only proxies whose assigned remote player left the cell or disappeared
Game update may use F4TProxyRemotePlayer01REF as one placed fallback if runtime spawning fails
``` ```
The networking receive thread does not directly modify Fallout 4 actors or game The networking receive thread does not directly modify Fallout 4 actors or game
@@ -213,8 +213,6 @@ update path.
## Out Of Scope For Current Prototype ## Out Of Scope For Current Prototype
- Multiple proxy actors
- `remotePlayerId -> proxy actor handle` mapping before Stage 4
- Animation graph sync or animation application - Animation graph sync or animation application
- Combat sync - Combat sync
- Quest sync - Quest sync
+69
View File
@@ -1834,6 +1834,75 @@ movement.
--- ---
## 2026-06-02 - Runtime Proxy Spawning Stage 4
### Summary
Added the first runtime proxy actor manager: the plugin now keeps a
controller-local runtime proxy slot map keyed by remote `playerId`, spawning
proxy actors only when valid same-cell remote state needs representation.
### Files Changed
- `plugin/include/F4TProxyActorController.h`
- `plugin/src/F4TProxyActorController.cpp`
- `docs/architecture.md`
- `docs/dev-log.md`
### Details
- Added `ProxyActorSlot` and `g_proxySlots`, capped by
`kMaxRuntimeProxyActors = 4` for this test milestone.
- Moved runtime spawning behind remote-player selection: the controller now
reads a remote snapshot, sorts by `playerId`, validates transform data, checks
same-cell compatibility, then creates or updates a slot for that player.
- Converted the PlaceAtMe-backed spawn/candidate/settle flow into slot-local
state while keeping a controller-level guard so only one PlaceAtMe spawn
attempt runs at a time.
- Stores promoted runtime proxies as `RE::ObjectRefHandle` values on the slot
and revalidates the handle before movement.
- Preserves per-proxy smoothing and snap behavior for `cell_change`,
`worldspace_change`, and `teleport`.
- Preserves sneak observation as per-slot logging only; no visual sneak/crouch
application was added.
- Holds only the affected players proxy when that player leaves the cell or
disappears. Slots stay session-sticky and are not reassigned to other
`playerId` values yet.
- Keeps `F4TProxyRemotePlayer01REF` as a single placed fallback for one selected
remote player if runtime spawning fails.
- Networking, protocol, Python server, fake-client tooling, gameplay sync,
animation sync, combat, inventory, quest, settlement, weapon/projectile, and
interaction sync were intentionally left unchanged.
### Testing
- Ran `xmake build` from `plugin`; it succeeded.
- Pending manual validation: launch Fallout 4 through F4SE, load
`Fallout4Together_Test.esp`, `coc F4TTestCell01`, start the Dev Server GUI,
add multiple fake clients, and confirm one independent runtime proxy per
`playerId` up to the max.
- Pending manual validation: confirm Leave Cell, Return To Cell, and disconnect
hold/restore only the affected players proxy while other proxies remain
active.
- Pending manual validation: connect a second real Fallout 4 client and confirm
it follows the same `RemotePlayerState[playerId]` path as GUI fake clients.
### Known Issues
- Runtime slots are session-sticky and are not reassigned after disconnect yet,
so long sessions with many unique player IDs can exhaust the small Stage 4
test cap.
- Runtime actors are still held instead of safely despawned or deleted.
- The placed fallback remains single-player only by design.
### Next Steps
- Manually validate Stage 4 with two or more fake clients in `F4TTestCell01`.
- Add a later cleanup/reuse milestone once a safe runtime actor cleanup path is
confirmed.
---
## Entry Template ## Entry Template
Use this format for future updates: Use this format for future updates:
+1 -1
View File
@@ -2,7 +2,7 @@
namespace F4T::ProxyActorController namespace F4T::ProxyActorController
{ {
// Game-thread-only actor controller for the single active runtime proxy and placed fallback. // Game-thread-only actor controller for runtime proxy slots and the single placed fallback.
// Networking threads must only update plain remote-player state. // Networking threads must only update plain remote-player state.
void Update(); void Update();
File diff suppressed because it is too large Load Diff