Add new Papyrus scripts (CoSync.psc, CoSyncPlayer.psc, CoSyncQuest.psc) providing native bindings and quest/player proxy logic for networking: connection/session APIs, entity management, world/weather/workshop/companion sync, consumable/pipboy/door/power-armor events, and proxy animation/initialization handlers. Refactor the proxy actor controller (F4TProxyActorController.cpp) to support dynamic/runtime proxy spawning and improved lifecycle/visibility handling: change proxy base/form IDs to COPlayerProxy/CommonwealthOnline, add process-list actor lookup, spawn helpers, play-space/cell checks, 3D visibility refresh, ActorHandle usage, and spawn mutexes and globals for safer spawning. Update docs and setup (architecture.md, limitations.md, plugin/setup.md) to describe global dynamic spawn behavior, max runtime proxies (kMaxRuntimeProxyActors = 4), and validation checklist; update launch-two-fallout4.bat to show instance log locations. Misc: small tweaks to logging and debug flags to gate diagnostics.
286 lines
15 KiB
Markdown
286 lines
15 KiB
Markdown
# Architecture
|
||
|
||
## Current Architecture
|
||
|
||
```text
|
||
Fallout 4 Plugin ↔ Local Python Server ↔ Other Clients
|
||
↓
|
||
Plugin Remote Player State
|
||
```
|
||
|
||
The current system is still a local prototype, but the Fallout 4 plugin now has
|
||
both send and receive paths:
|
||
|
||
1. The Fallout 4 plugin reads local player transform data.
|
||
2. The plugin derives basic movement state on the game-thread polling path and
|
||
sends it as optional data on transform packets.
|
||
3. The plugin sends transform packets to the local Python server on
|
||
`127.0.0.1:7777`. Normal movement sends are throttled separately from
|
||
movement logs, targeting roughly 10 Hz while the player is moving.
|
||
4. The server assigns `playerId` values and sends `welcome` packets.
|
||
5. The plugin receives its `welcome` packet and stores its assigned `playerId`.
|
||
6. The server adds `playerId` and `serverTime` to transform packets.
|
||
7. The server broadcasts transform packets to other connected clients.
|
||
8. The plugin and `server/fake_client.py` store remote player state by `playerId`.
|
||
9. The server broadcasts `disconnect` packets when clients disconnect.
|
||
10. The plugin and fake client remove disconnected players from their remote
|
||
player tables.
|
||
11. On the game-thread update path, the proxy controller reads a copied
|
||
remote-player snapshot before attempting any actor work. The snapshot is
|
||
sorted by `playerId`, and only remote players with valid transform data in
|
||
the same cell are eligible for runtime proxy representation.
|
||
12. For each eligible remote `playerId`, the controller maintains a
|
||
`remotePlayerId -> runtime proxy slot` mapping. Each slot stores its
|
||
`ObjectRefHandle`, lifecycle state, PlaceAtMe-backed spawn/candidate state,
|
||
movement smoothing state, and data-only animation debug state. Stage 4 caps runtime
|
||
proxy slots at four actors for testing.
|
||
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. 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. Runtime proxies receive a small game-thread neutralization pass when a
|
||
PlaceAtMe-backed actor is promoted, when a reusable slot is reassigned, while
|
||
active on a throttle, and when moved or kept in holding. The pass only uses
|
||
confirmed CommonLibF4 actor APIs/fields to stop combat, clear combat target
|
||
handles, reset safe hostile/attack state, suppress combat-oriented process
|
||
flags, clear exposed AI process movement targets, and attempt safe
|
||
package/pathing interruption with a do-nothing package when pathing, flee,
|
||
alarm, bump, or package movement intent is observed. It does not disable,
|
||
hide, delete, despawn, alpha-fade, or force animation graph events on the
|
||
actor.
|
||
15. 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. Movement and animation-related remote state is
|
||
still observed and logged only; no visual crouch/sneak, jump, weapon drawn,
|
||
locomotion, animation graph, or actor-state application is attempted.
|
||
16. 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 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.
|
||
17. 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.
|
||
18. 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
|
||
whether the actor base `F4T_RemotePlayerProxy` resolves by editor ID.
|
||
19. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy:
|
||
post-spawn visibility data, loaded-3D pointer checks where safe, console
|
||
helper logs, active source logs, and throttled post-movement diagnostics.
|
||
The diagnostic mode can force the runtime proxy near the local player to
|
||
prove whether the created actor is visibly renderable.
|
||
20. 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,
|
||
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
|
||
the local player.
|
||
21. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
|
||
to resolve Codsworth as a visible vanilla `TESNPC` candidate, then passes
|
||
that actor base into the same runtime spawn, handle validation, near-player
|
||
hold, and visibility diagnostic path. This isolates custom actor-base issues
|
||
from runtime-spawn/init issues without changing the final architecture.
|
||
22. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic
|
||
layer. It compares Codsworth base-form runtime spawning against moving the
|
||
existing placed Codsworth reference near the local player as a visibility
|
||
control. This separates runtime actor creation/init problems from vanilla
|
||
actor-base or placed-reference movement problems and does not change the
|
||
final architecture.
|
||
23. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
|
||
console validation proved `player.placeatme 000179FF 1` can spawn visible
|
||
Codsworth in `F4TTestCell01`. The plugin executes that console placement path
|
||
from the local player, resolves the newly created Codsworth actor near the
|
||
player, and holds/logs it separately from the `CreateReferenceAtLocation`
|
||
runtime proxy path.
|
||
24. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots
|
||
known Codsworth refs before executing `player.placeatme 000179FF 1`, accepts
|
||
only a newly isolated Codsworth ref for the primary diagnostic, and preserves
|
||
that actor at the original console-spawned location during a short settle
|
||
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
|
||
behavior. Stage 3.6 does not change the final architecture.
|
||
25. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the
|
||
preferred single runtime proxy spawn path. The proxy controller uses the
|
||
resolved runtime FormID for `F4T_RemotePlayerProxy` when available, falls
|
||
back to Codsworth only for diagnostics, defers active proxy selection during
|
||
candidate isolation and settle, then promotes the settled actor into the
|
||
existing single-proxy movement pipeline.
|
||
|
||
The Fallout 4 plugin dynamically spawns one runtime proxy actor per represented
|
||
remote `playerId` in the local player's current loaded cell, up to a small cap
|
||
(`kMaxRuntimeProxyActors = 4`). The proxy controller runs in any cell when global
|
||
mode is enabled; dynamic spawn is the default path. Pre-placed Creation Kit
|
||
proxy refs and the single placed fallback remain available behind compile-time
|
||
flags for regression testing in `F4TTestCell01`.
|
||
|
||
When the local player changes cells, stale proxy actors in the prior cell are
|
||
released and fresh proxies are spawned for co-located remotes in the new cell.
|
||
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.
|
||
|
||
Proxy animation sync applies curated graph variables from remote movement state:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
RemotePlayerState[RemotePlayerState] --> ProxyActorSlot[ProxyActorSlot]
|
||
ProxyActorSlot --> AnimDebug[Animation debug logs]
|
||
ProxyActorSlot --> AnimSync[F4TProxyAnimationSync]
|
||
AnimSync --> GraphVars[SetGraphVariable on proxy Actor]
|
||
```
|
||
|
||
## Main Components
|
||
|
||
### F4SE Plugin
|
||
|
||
The native plugin is responsible for:
|
||
|
||
- Loading into Fallout 4
|
||
- Reading local player state
|
||
- Sending local player data to the server
|
||
- Adding basic data-only movement state to transform packets
|
||
- Throttling normal transform sends separately from readable local movement logs
|
||
- Receiving server packets on a background thread
|
||
- Storing assigned and remote `playerId` state internally
|
||
- Moving runtime proxy actors from per-`playerId` remote-player state on the
|
||
game thread via dynamic spawn in the local player's current cell (pre-placed
|
||
CK pool and placed fallback available behind compile-time regression flags)
|
||
- 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
|
||
- Neutralizing only resolved runtime proxy actors on the game-thread controller
|
||
path after spawn promotion, during reusable-slot reassignment, periodically
|
||
while active, and while held so they remain passive visual puppets rather than
|
||
independent combat actors
|
||
- Logging initial and transition-only data/debug animation state per resolved
|
||
runtime proxy slot from `RemotePlayerState`, including moving, speed bucket,
|
||
sprinting, sneaking, jumping, weapon drawn, and movement type
|
||
- Applying curated Havok animation graph variables to runtime proxies through
|
||
`F4TProxyAnimationSync` from the same remote movement state (writes on
|
||
transition and while moving; clears on hold, disconnect, reuse, and reassignment)
|
||
- Suppressing exposed AI movement intent for runtime proxies by clearing safe
|
||
process target handles and using validated package interruption/do-nothing
|
||
calls when combat, pathing, flee/alarm, bump, or package movement state is
|
||
observed
|
||
- 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
|
||
- Holding the single runtime proxy near the local player during temporary Stage
|
||
3.2 diagnostics so the plugin can determine whether runtime-created actor refs
|
||
can load visible 3D before remote movement resumes
|
||
- Temporarily comparing a vanilla visible NPC actor base against the custom
|
||
proxy actor base during Stage 3.3, while preserving the same runtime spawn
|
||
path before the Stage 4 mapping work
|
||
- Temporarily resolving Codsworth base and placed-reference absolute FormIDs
|
||
during Stage 3.4, then moving the existing placed reference near the player as
|
||
a visibility control separate from runtime spawning
|
||
- Temporarily testing a local-player `player.placeatme`-equivalent console path
|
||
during Stage 3.5, then comparing the resulting visible actor behavior against
|
||
the existing `CreateReferenceAtLocation` runtime actor path
|
||
- Temporarily isolating the newly spawned PlaceAtMe ref during Stage 3.6 and
|
||
testing render/process settle behavior without changing active proxy mapping
|
||
- Using Stage 3.7 to isolate a PlaceAtMe-backed runtime proxy, preserve it
|
||
during a short settle window, and then promote it into proxy movement
|
||
- 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
|
||
progress at a time
|
||
|
||
### External Server
|
||
|
||
The server is responsible for:
|
||
|
||
- Accepting client connections
|
||
- Assigning player IDs
|
||
- Sending welcome packets
|
||
- Receiving player state
|
||
- Broadcasting player state
|
||
- Tracking disconnects
|
||
- Broadcasting disconnect packets
|
||
|
||
### Fake Client
|
||
|
||
The fake client is responsible for:
|
||
|
||
- Connecting to the local server
|
||
- Receiving welcome packets
|
||
- Receiving broadcast transform packets
|
||
- Storing remote player state by `playerId`
|
||
- Removing remote players when disconnect packets arrive
|
||
|
||
The fake client is a temporary receiver for testing the networking lifecycle. It
|
||
does not spawn remote actors or represent playable multiplayer.
|
||
|
||
### Creation Kit Plugin
|
||
|
||
The Creation Kit side is planned later and may be responsible for:
|
||
|
||
- Test cells
|
||
- Placeholder actors
|
||
- Optional Papyrus helper scripts
|
||
- Debug objects
|
||
- Controlled test environments
|
||
|
||
## Threading Model
|
||
|
||
Networking should run separately from game update logic.
|
||
|
||
Current approach:
|
||
|
||
```text
|
||
Game update reads local player transform
|
||
Game update derives basic movement state
|
||
Game update sends thresholded transform packets around 10 Hz while moving
|
||
Game update logs local movement around 1 Hz
|
||
Networking thread receives packets
|
||
Networking thread stores remote player state
|
||
Game update reads a copied remote player snapshot
|
||
Game update resolves/logs proxy actor base diagnostics
|
||
Game update sorts remote players by playerId
|
||
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 logs data-only animation debug state for resolved runtime proxy slots
|
||
Game update neutralizes resolved runtime proxy actors after spawn promotion, reuse, active throttled checks, and holding
|
||
Game update suppresses exposed AI movement/pathing intent without forcing animation graph events
|
||
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
|
||
```
|
||
|
||
The networking receive thread does not directly modify Fallout 4 actors or game
|
||
objects. It parses newline-separated JSON, updates plain C++ remote-player
|
||
state, and leaves actor access to `F4TProxyActorController` on the game-thread
|
||
update path.
|
||
|
||
## Out Of Scope For Current Prototype
|
||
|
||
- Animation graph sync or animation application
|
||
- Combat sync
|
||
- Quest sync
|
||
- Inventory sync
|
||
- Settlement sync
|
||
- Public servers or matchmaking
|