Files
Commonwealth-Online-Public/docs/architecture.md
T
andrew 4c17e70620 Add serverWorldState packet and GUI controls
Introduce a server-authoritative world-state packet to control weather and time from the dev server GUI. Adds protocol docs and message type (serverWorldState), a new Weather/Time tab in dev_server_app.py, server-side broadcasting in server_core.py, and preset utilities in server/world_state_presets.py. Plugin changes (F4TNetworking.cpp, F4TWorldStateSync.*) queue and apply incoming serverWorldState on the game thread, run `set gamehour` for all clients, and run `fw <8-digit-id>` on the assigned player 1 then force the host to relay a worldState snapshot. Also includes parsing/validation of hex form IDs, logging improvements, and minimal fake client handling. This ensures consistent server-driven time/weather and documents usage and presets.
2026-06-23 23:03:34 +12:00

16 KiB
Raw Blame History

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:

  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 Python relay server. The default dev setup connects to 127.0.0.1:7777 on the same PC; the server itself listens on 0.0.0.0:7777 so other machines can connect using the host PC's LAN IP and port. 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. The server tracks a world-state host (worldStateHostPlayerId). The first connected client becomes host; when the host disconnects, the server reassigns host to the lowest remaining playerId and broadcasts worldStateHost. The host sends worldState packets with gameHour, gameDaysPassed, optional weatherFormId, and optional timeSync. Weather heartbeats run at ~1 Hz; time is pushed with timeSync: true on connect, after time freezes (menus, loads, saves), and on large time jumps. The dev server GUI can also broadcast serverWorldState to all clients for admin weather (fw) and time (set gamehour to HHmm) commands. Promoted hosts continue with their already-synced local time and weather so remaining clients should not see a jump.
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. Proxy visual updates are independent per slot. Normal movement is smoothed toward that players 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.
  17. When a remote player leaves the cell or disconnects, only that players 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.
  18. 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.
  19. 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.
  20. 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.
  21. 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.
  22. 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.
  23. 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.
  24. 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.
  25. 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.
  26. 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:

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:

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