Repo Cleanup

This commit is contained in:
2026-07-10 17:30:10 +12:00
parent ca92123cd9
commit 5b031975d0
58 changed files with 112 additions and 2464 deletions
+35
View File
@@ -0,0 +1,35 @@
# Protocol
This folder documents the planned network protocol for Commonwealth Online.
The first protocol should be simple and focused on player transform sync.
## First Packet Types
- Welcome packet
- Transform packet
- World state packet (host time and weather) — see [`world-state.md`](world-state.md)
- Server world state packet (dev GUI weather/time) — see [`server-world-state.md`](server-world-state.md)
- Disconnect packet
## Future Packet Types
- Animation state packet
- Fire weapon packet
- Health packet
- Chat packet
- Cell change packet
## Design Goals
The protocol should be:
- Simple
- Easy to debug
- Versioned
- Safe to extend
- Clear enough to test outside Fallout 4
## Early Testing
The protocol should be testable using fake clients before being connected to Fallout 4.
+82
View File
@@ -0,0 +1,82 @@
# Message Types
This file tracks current and planned message type names for the Commonwealth Online local networking prototype.
## Implemented
These message types are currently implemented:
```text
welcome
transform
worldState
worldStateHost
serverWorldState
disconnect
```
Current behavior:
- `welcome` is sent by the server after assigning a `playerId`.
- `transform` is sent by the Fallout 4 plugin, processed by the server, and
broadcast to other connected clients.
- `worldState` is sent by the current world-state host, validated by the
server, and broadcast to other connected clients.
- `worldStateHost` is sent in `welcome` and when the host disconnects to
reassign host to the lowest remaining `playerId`.
- `serverWorldState` is sent by the server to all connected clients when the
dev server GUI sets weather or time.
- `disconnect` is sent by the server when a client disconnects.
The Fallout 4 plugin sends transform packets and receives `welcome`,
`transform`, `worldState`, `serverWorldState`, and `disconnect` packets. Received transforms are
stored as plain remote-player state. Non-host clients apply received world state
on the game thread.
## Current Receivers
The Fallout 4 plugin and `server/fake_client.py` currently receive:
```text
welcome
transform
worldState
worldStateHost
serverWorldState
disconnect
```
The plugin and fake client store broadcast transform packets in in-memory remote
player state tables and remove entries when disconnect packets arrive. The fake
client also tracks the latest host world-state snapshot.
## Planned Later
These message types are planned but not implemented yet:
```text
ping
pong
chat_message
animation_state
weapon_fire
health_update
cell_change
actor_spawn
actor_remove
```
`cell_change` is currently represented through the `movementType` field inside
`transform` packets. It is not its own packet type yet.
Remote actor spawning, combat, inventory, quest, settlement, and broader
gameplay sync are planned later and are outside the current local prototype.
## Naming Rules
Message types should be:
- Lowercase
- Snake case
- Short but clear
- Stable once used in test builds
+250
View File
@@ -0,0 +1,250 @@
# Packet Types
This document describes the currently implemented local prototype packet types
for Commonwealth Online.
The current protocol is newline-separated JSON over a local TCP connection to
`127.0.0.1:7777`.
Implemented packet types:
- `welcome`
- `transform`
- `disconnect`
The current system is still a local networking prototype. It is not playable
multiplayer yet.
## Implemented
### Welcome Packet
Sent from the server to a newly connected client after the server assigns a
`playerId`.
Example:
```json
{
"type": "welcome",
"playerId": 1,
"serverTime": 1780212098.457079
}
```
Fields:
```text
type Packet type. Always "welcome".
playerId Server-assigned player ID for this connection.
serverTime Server timestamp when the welcome packet was created.
```
The Fallout 4 plugin and `server/fake_client.py` receive this packet. The plugin
stores the assigned `playerId` for filtering its own future transform echoes.
### Transform Packet
Sent by the Fallout 4 plugin to the local server when the local player's
transform changes. The server adds `playerId` and `serverTime`, then broadcasts
the processed packet to other connected clients.
Example after server processing:
```json
{
"type": "transform",
"playerId": 2,
"x": -81781.26,
"y": 87962.61,
"z": 7696.05,
"angleZ": 3.20,
"movementType": "normal",
"cellId": "0000DD60",
"worldspaceId": "0000003C",
"clientTime": 1780212128.301,
"isMoving": true,
"isSprinting": false,
"isSneaking": false,
"isJumping": false,
"weaponDrawn": false,
"movementSpeed": 186.4,
"equippedItems": [
{ "slot": "body", "formId": "0001F66A" },
{ "slot": "headband", "formId": "" },
{ "slot": "rightHand", "formId": "0001F4A6" }
],
"appearance": {
"version": 3,
"raceFormId": "00013746",
"height": 1.0,
"morphWeight": { "thin": 0.0, "muscular": 0.0, "large": 0.0 },
"bodyTintColor": { "r": 255, "g": 220, "b": 190, "a": 255 },
"hairColorFormId": "",
"facialHairColorFormId": "",
"complexionFormId": "",
"headParts": [],
"morphs": [],
"morphRegions": [],
"facialBoneMorphs": [],
"tints": []
},
"serverTime": 1780212128.3011043
}
```
Fields:
```text
type Packet type. Always "transform".
playerId Server-assigned player ID for the sender.
x Local player X position.
y Local player Y position.
z Local player Z position.
angleZ Local player Z rotation angle.
movementType Movement classification for this transform update.
cellId Current cell form ID as a string.
worldspaceId Current worldspace form ID as a string.
clientTime Timestamp generated by the sending plugin.
isMoving Optional movement-state flag; defaults to false when missing.
isSprinting Optional movement-state flag; defaults to false when missing.
isSneaking Optional movement-state flag; defaults to false when missing.
isJumping Optional movement-state flag; defaults to false when missing.
weaponDrawn Optional weapon drawn state; defaults to false when missing.
movementSpeed Optional derived movement speed in game units per second; defaults to 0.0 when missing.
equippedItems Optional full snapshot of tracked visible apparel slots. Each entry has a string `slot` and hex-string `formId`; an empty `formId` means the slot is intentionally unequipped.
appearance Optional versioned player appearance snapshot for runtime proxy visuals.
serverTime Timestamp added by the server before broadcast.
```
Possible `movementType` values:
```text
normal
cell_change
worldspace_change
teleport
```
`cell_change` and `worldspace_change` are currently represented through
`movementType` inside transform packets. They are not separate packet types yet.
Movement state fields are data-only for now. Receivers must treat them as
optional and must not reject older transform packets when they are absent.
Equipment fields are optional and additive. When `equippedItems` is missing,
receivers keep their existing/default proxy appearance. When present, it is a
complete snapshot for the tracked visible apparel slots:
```text
hairTop
hairLong
body
underTorso
underLeftArm
underRightArm
underLeftLeg
underRightLeg
aboveTorso
aboveLeftArm
aboveRightArm
aboveLeftLeg
aboveRightLeg
headband
eyes
scalp
```
Tracked slots include 16 visible apparel BIPED slots (listed above) plus:
```text
rightHand Right-hand weapon slot (WEAP form ID); optional
```
The equipment-sync milestones send resolved `ARMO` and `WEAP` form IDs and
empty strings for unequipped tracked slots. Off-hand weapons, power armor,
weapon condition, legendary instance data, tint/material overrides, and forms
missing from the receiver load order are outside this packet extension.
When `rightHand` is missing, receivers keep their existing proxy weapon. When
present with an empty `formId`, the proxy is unequipped. When a `WEAP` form ID
is not found locally, receivers log a throttled warning and skip equipping
that weapon; the proxy continues with its current state and other sync intact.
Appearance fields are optional and additive. When `appearance` is missing,
receivers keep the proxy's existing/default body and face. When present, it is a
best-effort snapshot for male-proxy-compatible runtime visuals:
```text
version Appearance payload version. Current value is 3.
raceFormId Hex-string race form ID captured for diagnostics/future use; receivers do not switch proxy race yet.
height NPC height scalar.
morphWeight Body triangle weights: thin, muscular, and large.
bodyTintColor Body tint channels r, g, b, a as 0-255 integers.
hairColorFormId Hex-string `CLFM` form ID for hair color, or empty when unavailable.
facialHairColorFormId Hex-string `CLFM` form ID for facial hair color, or empty when unavailable.
complexionFormId Hex-string `TXST` form ID for skin complexion (hands, arms, body tone), or empty when unavailable.
headParts Hex-string `HDPT` form IDs for the sender's current head parts.
morphs Morph slider values as `{ "id": hex-string, "value": number }` entries.
morphRegions Region morph slider values as a flat array of numbers (added in version 2).
facialBoneMorphs Bone-based facial morphs as `{ "id": hex-string, "position": [x,y,z], "rotation": [x,y,z], "scale": [x,y,z] }` entries (added in version 2).
tints Character tint layers (skin tone, complexion, makeup, beard shade) as `{ "id": number, "type": number, "value": number, "color": hex-string, "swatch": number }` entries (added in version 3). `id` is the race tint-template uniqueID, `type` is the tint entry type (0 mask, 1 palette, 2 texture), and `value` is the 0-255 intensity. `color` (ARGB hex) and `swatch` are present only for palette entries.
```
`morphRegions` and `facialBoneMorphs` are optional and additive. Version 1
senders omit them; receivers must treat their absence as "no change" and keep
existing proxy morph data.
`tints` is optional and additive (added in version 3). Earlier senders omit it;
receivers must treat its absence as "no change". Receivers apply a tint by
matching `id` against an existing proxy tint entry (updating intensity/color in
place) and only reconstruct a missing entry when the proxy race exposes a
matching tint template, skipping unresolved tints rather than failing.
Receivers must treat missing or malformed appearance subfields as non-fatal and
apply only the parts they can resolve locally. Gender is intentionally not part
of this payload yet; female proxy support can add it later.
### Disconnect Packet
Sent by the server to remaining connected clients when a client disconnects.
Example:
```json
{
"type": "disconnect",
"playerId": 2,
"serverTime": 1780212134.0
}
```
Fields:
```text
type Packet type. Always "disconnect".
playerId Server-assigned player ID that disconnected.
serverTime Server timestamp when the disconnect packet was created.
```
The Fallout 4 plugin and `server/fake_client.py` use this packet to remove
remote players from their in-memory state tables.
## Receiver Behavior
The Fallout 4 plugin and fake client both receive:
```text
welcome
transform
disconnect
```
The plugin receive loop runs on a background networking thread. It stores plain
remote-player state only and does not spawn actors or touch Fallout 4 game
objects.
## Planned Later
- Remote actor spawning.
- Gameplay synchronization.
+180
View File
@@ -0,0 +1,180 @@
# Player Sync
Player synchronization is the first major technical goal for Commonwealth Online.
The current implementation is still a local networking prototype.
## Current Implemented Flow
```text
Fallout 4 Plugin ↔ Local Python Server ↔ Other Clients
↓
Plugin Remote Player State
```
The Fallout 4 plugin currently reads the local player's transform and sends it
to the local Python server. Normal movement sends are throttled separately from
local movement logs: while the player is moving, the plugin targets roughly
10 Hz and skips sends until position or rotation changes meaningfully. The
server assigns a `playerId`, adds `serverTime`, and broadcasts transform packets
to other connected clients.
When a new client connects, the server sends the last stored transform snapshot
for each already-connected player to that new client after `welcome`. These
snapshots include optional fields such as `equippedItems` and `appearance`, so a
late joiner can initialize proxy visuals before the existing players' next idle
heartbeat or movement packet.
The Fallout 4 plugin also receives server packets on a background networking
thread. It stores its assigned `playerId`, stores remote transform state by
remote `playerId`, and removes remote state when disconnect packets arrive.
`server/fake_client.py` remains a lightweight test receiver for the same packet
lifecycle.
The plugin's game-thread proxy actor controller reads a copied snapshot of this
remote-player state and dynamically spawns/moves runtime proxy actors in the
local player's current loaded cell. The networking thread still does not touch
Fallout 4 actors or references. If a remote player is not in the same
cell/worldspace, the controller holds that player's proxy at a hidden in-cell
position instead of leaving it at the last visible location.
`COVault109` is treated as a solo cell for the first-time character-creation
flow. On entry the plugin can send one location update so other clients hold any
stale proxy, then it suppresses outgoing transform packets while the local
player remains inside and the proxy controller skips remote representation. This
is a client-side visibility/isolation rule and does not change the transform
packet schema.
## Current Remote Player State Model
The plugin and fake client store remote players by `playerId`.
Each remote player state entry tracks:
```text
playerId
x
y
z
angleZ
movementType
cellId
worldspaceId
clientTime
serverTime
isMoving
isSprinting
isSneaking
isJumping
isCrouching
weaponDrawn
movementSpeed
animationGraphSpeed
equippedItems
appearance
lastReceivedLocalTime
```
Required transform packet fields are `type`, `playerId`, `x`, `y`, `z`,
`angleZ`, and `cellId`. `worldspaceId`, `movementType`, `clientTime`, and
`serverTime` are optional; interior test cells may omit `worldspaceId`.
Movement state fields are also optional and default to not moving, not
sprinting, not sneaking, not jumping, weapon holstered, and speed `0.0` when
missing.
`equippedItems` is also optional. New clients send it as a complete snapshot of
tracked visible apparel slots with `{ slot, formId }` entries; empty `formId`
values mean that tracked slot is intentionally unequipped. Older clients can
omit the field and receivers keep the proxy's existing/default appearance.
`appearance` is optional and versioned. It carries male-proxy-compatible body
and face data such as race form ID for diagnostics/future use, height, body
morph weight, body tint color, hair color, facial hair color, head-part form IDs,
and morph slider values. Receivers apply supported fields only after proxy 3D is
loaded. Gender switching is intentionally omitted until female proxy support is
available.
This model proves the data shape and lifecycle before spawning remote actors.
For the current visual milestone, the controller represents only one remote
player and chooses the lowest available `playerId` if more than one remote
player exists.
## Implemented
- The plugin sends local player transform packets.
- Normal transform sends are independent from movement logging and are
thresholded to avoid packet-per-frame traffic.
- The plugin receives `welcome` packets and stores its assigned `playerId`.
- The plugin receives broadcast transform packets.
- The plugin ignores transform packets for its own assigned `playerId`.
- The plugin stores remote player state by `playerId`.
- The plugin receives `disconnect` packets and removes remote player state.
- The server assigns incrementing `playerId` values.
- The server sends `welcome` packets.
- The server adds `playerId` and `serverTime` to transform packets.
- The server broadcasts transform packets to other connected clients.
- The fake client stores remote player state by `playerId`.
- The server broadcasts `disconnect` packets.
- The fake client removes disconnected remote players from its state table.
- The plugin moves the single placed test proxy actor from a thread-safe
remote-player snapshot on the game-thread update path.
- Normal proxy movement is smoothed toward the latest received target, while
`cell_change`, `worldspace_change`, and `teleport` transforms snap directly to
avoid slow movement across large discontinuities.
- The game-thread proxy controller holds the single proxy at a hidden in-cell
position when the represented player disconnects, disappears, or leaves the
local test cell, then snaps back to a valid same-cell remote player before
resuming smoothing.
- Transform packets include movement state data (`isMoving`, `movementSpeed`,
`isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`).
- Transform packets can include optional `equippedItems` snapshots for visible
clothing, armor, hats, eyewear, and right-hand weapon slots. The game-thread
proxy controller applies those snapshots after proxy 3D is loaded. Weapons are
synced as optional `WEAP` form IDs; missing weapons or unresolved form IDs do
not block other equipment sync.
- Transform packets can include optional `appearance` snapshots for best-effort
body and face proxy visuals. The game-thread proxy controller applies supported
fields after proxy 3D is loaded and defers race/gender switching.
- The plugin suppresses transform sends and proxy representation in the
`COVault109` solo cell.
- The game-thread proxy controller applies confirmed Havok animation graph
variables to runtime proxy actors from that remote state (locomotion, sneak,
jump). Weapon-drawn state is parsed but animation application is implemented
separately (Phase 2 milestone). Transform position/heading sync is unchanged.
## Transform Cadence
Normal local movement currently uses three separate rates:
- Network sends target roughly 10 Hz while the player is moving.
- Sends are skipped until position changes by about 3 game units or rotation
changes by about 0.02 radians.
- Local movement logs stay much slower, around once per second, so
`CommonwealthOnline.log` remains readable.
`cell_change`, `worldspace_change`, and `teleport` movement types bypass the
normal send interval and are sent immediately. The receiving plugin still stores
remote transform state on the networking thread and moves actors only from the
game-thread proxy controller.
Runtime proxy actors are dynamically spawned per remote `playerId` in the local
player's current loaded cell (see architecture). Remote players in a different
cell are held at a hidden in-cell position until the local player enters their
cell. Full animation graph replication is not implemented; only curated graph
variable writes drive proxy locomotion visuals.
## Planned Later
- Increase concurrent proxy cap beyond four players per client
- Cross-cell actor transfer without respawn (if respawn proves insufficient)
- Power armor and weapon model sync
## Not Required Yet
- Full animation graph sync
- Combat sync
- Full inventory sync beyond visible equipped apparel
- Quest sync
- Dialogue sync
- Settlement sync
- Accurate physics sync
- Public networking or matchmaking
+66
View File
@@ -0,0 +1,66 @@
# Server World State Packet
Server-authoritative weather and time control from the dev server GUI.
## Authority
- **Time** (`timeHHmm`): sent to **all** clients; each converts `HHmm` to Fallout game-hour float (for example `2200 -> 22.0`, `1830 -> 18.5`) before applying.
- **Weather** (`weatherConsoleArg`): sent to **all** clients, but only the current **world-state host** runs `fw <8-digit-id>`. The host then relays usual `worldState` updates to other clients.
## Packet Format
Time-only (all clients):
```json
{
"type": "serverWorldState",
"timeHHmm": "1900",
"serverTime": 1780212128.301
}
```
Weather (all clients receive; only world-state host acts):
```json
{
"type": "serverWorldState",
"weatherConsoleArg": "001ca7e4",
"weatherFormId": "001CA7E4",
"serverTime": 1780212128.301
}
```
## Field Definitions
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | Must be `"serverWorldState"` |
| `timeHHmm` | string | no | 24-hour clock value (HHmm) converted client-side to game-hour float for `set gamehour to <float>` |
| `weatherConsoleArg` | string | no | Exact argument for `fw` on the current world-state host (8-digit lowercase hex) |
| `weatherFormId` | string | no | Relay form ID used in host `worldState` broadcast |
| `serverTime` | number (double) | no | Relay timestamp |
## Client Apply Rules
- **Time:** all clients convert `timeHHmm` to game-hour float and apply `set gamehour to <float>` on the game thread.
- **Weather:** only the current world-state host runs `fw <weatherConsoleArg>`.
- Weather commands are scheduled on the F4SE game thread from the network receive path.
- After the host runs `fw`, it immediately relays `worldState` with `weatherFormId`.
Edit `server/world_state_presets.py` so each preset uses the exact `fw` ID that works in your console (for example `0000015e`).
## Preset Weather Console Args
| Label | Default `fw` arg |
|-------|------------------|
| Clear | `0002b52a` |
| Cloudy | `001cc186` |
| Overcast | `001c8556` |
| Fog | `001c3473` |
| Rain | `001ca7e4` |
| Radstorm | `001c3d5e` |
| Glowing Sea | `000f1033` |
## Backward Compatibility
Older clients that do not handle `serverWorldState` ignore the packet safely.
+82
View File
@@ -0,0 +1,82 @@
# World State Packet
Host-authoritative time and weather sync for the Commonwealth Online prototype.
## Authority
- The server tracks a `worldStateHostPlayerId` for the session.
- The first connected client becomes host. When the host disconnects, the server
reassigns host to the lowest remaining `playerId` and broadcasts `worldStateHost`.
- Only the current host sends `worldState` packets.
- The Python relay validates the sender and broadcasts to all other clients.
Promoted hosts keep their already-synced local time and weather; reassignment should
not change world state for remaining clients.
## worldStateHost Packet
Sent in `welcome` (initial host) and broadcast when the host disconnects:
```json
{
"type": "worldStateHost",
"worldStateHostPlayerId": 2,
"serverTime": 1780212128.301
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | Must be `"worldStateHost"` |
| `worldStateHostPlayerId` | integer | yes | Connected player ID that is now world-state host |
| `serverTime` | number (double) | no | Relay timestamp |
The `welcome` packet also includes optional `worldStateHostPlayerId` so new joiners
know the current host immediately.
## Packet Format
```json
{
"type": "worldState",
"playerId": 1,
"gameHour": 14.25,
"gameDaysPassed": 12.5,
"weatherFormId": "00123456",
"timeSync": true,
"clientTime": 1780212128.301,
"serverTime": 1780212128.301
}
```
## Field Definitions
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | Must be `"worldState"` |
| `playerId` | integer | yes (server) | Injected by server; identifies the sending host |
| `gameHour` | number (float) | yes | In-game hour of day, `0.0`–`24.0` |
| `gameDaysPassed` | number (float) | yes | In-game days passed global |
| `weatherFormId` | string | no | 8-character hex `TESWeather` form ID; omitted or `""` when host is indoors |
| `timeSync` | boolean | no | When `true`, non-host clients should apply `gameHour` and `gameDaysPassed`. Omitted or `false` on weather-only heartbeats. |
| `clientTime` | number (double) | no | Sender Unix epoch seconds |
| `serverTime` | number (double) | no | Relay timestamp added by server |
## Send Cadence (Host)
- About 1 Hz heartbeat for **weather** while connected (`timeSync` omitted or `false`)
- Immediate send with `timeSync: true` when:
- First connected or promoted to host
- Game time resumes after a freeze (menus, pip-boy, dialogue, loading screens, save/load, wait/sleep)
- `gameHour` or `gameDaysPassed` jumps beyond a small threshold (wait, sleep, console time changes)
- Immediate send when `weatherFormId` changes (includes current time fields; sets `timeSync` only when time also changed or on resume)
## Client Apply Rules
- **Time:** non-host clients track the latest host time from every packet but only snap local calendar globals when `timeSync` is `true` and gameplay time is not frozen, or immediately when gameplay time resumes after a freeze (using the latest stored host values).
- **Weather:** non-host clients apply forced weather only when the local player is in an exterior cell.
- Missing optional fields use safe defaults on receive.
## Backward Compatibility
Older clients that do not send or parse `worldState` continue to work. Transform sync is unchanged.
+50
View File
@@ -0,0 +1,50 @@
# World Sync
Fallout 4 assumes a single player owns the world state. Multiplayer requires deciding which client or server controls each piece of world state.
## Implemented (MVP)
Host-authoritative time and weather sync is implemented via the `worldState`
packet. See [`world-state.md`](world-state.md) for the field spec.
- Host: tracked by `worldStateHostPlayerId` (first connected client; reassigned to lowest remaining `playerId` on host disconnect)
- Synced fields: `gameHour`, `gameDaysPassed`, optional `weatherFormId`
- Weather apply: exterior cells only on non-host clients
- Server: pass-through relay with host-only send guard
## World State Examples
World state can include:
- Doors
- Containers
- Pickups
- NPC deaths
- Physics objects
- Quest stages
- Workshop objects
- Terminal states
- Dialogue state
## Early Approach
The first prototype should avoid world sync as much as possible.
Recommended early restrictions:
- Use a custom test cell
- Avoid lootable containers
- Avoid quests
- Avoid doors
- Avoid terminals
- Avoid persistent world changes
## Later Ideas
Possible future approaches:
- Host-controlled world state
- Server-authoritative world state
- Cell-specific state tracking
- Limited interaction whitelist
- Resettable test environments