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.
15 KiB
Architecture
Current Architecture
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:
- The Fallout 4 plugin reads local player transform data.
- The plugin derives basic movement state on the game-thread polling path and sends it as optional data on transform packets.
- 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. - The server assigns
playerIdvalues and sendswelcomepackets. - The plugin receives its
welcomepacket and stores its assignedplayerId. - The server adds
playerIdandserverTimeto transform packets. - The server broadcasts transform packets to other connected clients.
- The plugin and
server/fake_client.pystore remote player state byplayerId. - The server broadcasts
disconnectpackets when clients disconnect. - The plugin and fake client remove disconnected players from their remote player tables.
- 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. - For each eligible remote
playerId, the controller maintains aremotePlayerId -> runtime proxy slotmapping. Each slot stores itsObjectRefHandle, 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. - 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.
- 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.
- Proxy visual updates are independent per slot. Normal movement is smoothed
toward that player’s latest target, while
cell_change,worldspace_change, andteleportsnap 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. - 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. - The placed fallback reference
F4TProxyRemotePlayer01REFremains 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. - As part of the staged runtime proxy actor manager work, the same game-thread
proxy controller also logs whether
Fallout4Together_Test.espis loaded and whether the actor baseF4T_RemotePlayerProxyresolves by editor ID. - 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.
- 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.
- Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
to resolve Codsworth as a visible vanilla
TESNPCcandidate, 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. - 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.
- Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
console validation proved
player.placeatme 000179FF 1can spawn visible Codsworth inF4TTestCell01. 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 theCreateReferenceAtLocationruntime proxy path. - 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. - 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_RemotePlayerProxywhen 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:
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
playerIdstate internally - Moving runtime proxy actors from per-
playerIdremote-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, normallyF4T_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
F4TProxyAnimationSyncfrom 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 existingCreateReferenceAtLocationruntime 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:
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