Repo Cleanup
This commit is contained in:
@@ -1,319 +0,0 @@
|
||||
# FFDec guide — add Multiplayer button to MainMenu.swf
|
||||
|
||||
> **Deprecated:** Use C++ runtime injection instead. See [`MainMenu_Injection.md`](MainMenu_Injection.md). Do not deploy a patched `MainMenu.swf` for Commonwealth Online.
|
||||
|
||||
Step-by-step instructions for adding the **Multiplayer** main-menu item using [JPEXS FFDec](https://github.com/jindrapetrik/jpexs-decompiler/releases), without breaking **Settings → Gameplay / Display / Audio**.
|
||||
|
||||
This guide is kept for historical reference only.
|
||||
|
||||
---
|
||||
|
||||
## Before you start
|
||||
|
||||
### Restore vanilla SWF
|
||||
|
||||
Always patch from the vanilla file, not an old broken copy:
|
||||
|
||||
```powershell
|
||||
.\tools\restore-vanilla-mainmenu.ps1
|
||||
```
|
||||
|
||||
Or manually copy `Interface/MainMenu.swf.vanilla.bak` → `Interface/MainMenu.swf`.
|
||||
|
||||
Vanilla size is **61,959 bytes**. If your file is **64 KB+**, it is probably a bad re-import — restore and start again.
|
||||
|
||||
### Requirements
|
||||
|
||||
- **FFDec** installed
|
||||
- Game launched via **F4SE** with `CommonwealthOnline.dll` in `Data/F4SE/Plugins/`
|
||||
- Loose Interface overrides enabled in `%USERPROFILE%/Documents/My Games/Fallout4/Fallout4Custom.ini`:
|
||||
|
||||
```ini
|
||||
[Archive]
|
||||
bInvalidateOlderFiles=1
|
||||
```
|
||||
|
||||
### Critical rule
|
||||
|
||||
| Do | Don't |
|
||||
|----|--------|
|
||||
| Edit **individual methods** (double-click method in traits list) | Paste the full `MainMenu.as` export into the class editor |
|
||||
| Save after each small edit | Use **Import script** on the whole `MainMenu` class |
|
||||
| Keep SWF ~62–63 KB | Re-export the entire decompiled script |
|
||||
|
||||
Full re-import recompiles `RequestOptions` and breaks settings sub-menus. Controls will still work; Gameplay / Display / Audio will crash.
|
||||
|
||||
---
|
||||
|
||||
## FFDec navigation
|
||||
|
||||
1. Open **`Interface/MainMenu.swf`** in FFDec.
|
||||
2. Left tree: **scripts → MainMenu**.
|
||||
3. You should see a **traits / members list** (constants, functions).
|
||||
4. **Double-click a method name** (e.g. `InitList`) to open **only that method** in the editor.
|
||||
5. After each edit: apply/save the method, then **File → Save** the SWF.
|
||||
|
||||
Do **not** use **Edit ActionScript** on the entire `MainMenu` class if FFDec offers a single giant class view — use per-method editing instead.
|
||||
|
||||
---
|
||||
|
||||
## Summary of changes
|
||||
|
||||
| # | Where in FFDec | What |
|
||||
|---|----------------|------|
|
||||
| 1 | Class constants | Add `MULTIPLAYER_INDEX` |
|
||||
| 2 | Method `InitList` | Push Multiplayer row after Load |
|
||||
| 3 | Method `onMainListItemPress` | Add `case` for Multiplayer |
|
||||
| 4 | New method `CloseMultiplayerBrowser` | Helper (recommended) |
|
||||
| 5 | Method `set currentState` | Call close helper when leaving main panel |
|
||||
| 6 | Method `onMainListItemPress` | Close browser when another item is selected |
|
||||
|
||||
Changes **4–6** are recommended so the HTML browser closes immediately when opening Settings, etc. The plugin also hides the browser when `currentState` leaves `MAIN_STATE`, but the SWF hooks make it feel instant.
|
||||
|
||||
---
|
||||
|
||||
## Change 1 — Add index constant
|
||||
|
||||
**FFDec:** `MainMenu` → constants section (near other `*_INDEX` constants)
|
||||
|
||||
**Find this block** (search for `INSTALLED_CONTENT_INDEX`):
|
||||
|
||||
```actionscript
|
||||
private const INSTALLED_CONTENT_INDEX:Number = 14;
|
||||
|
||||
private const MAIN_PANEL_BACKGROUND:uint = 0;
|
||||
```
|
||||
|
||||
**Insert between those lines:**
|
||||
|
||||
```actionscript
|
||||
private const MULTIPLAYER_INDEX:Number = 15;
|
||||
```
|
||||
|
||||
Index `15` is unused in vanilla (highest defined index is `14`).
|
||||
|
||||
---
|
||||
|
||||
## Change 2 — Add menu row in `InitList`
|
||||
|
||||
**FFDec:** `MainMenu` → method **`InitList`**
|
||||
|
||||
**Find this block** (search for `"$LOAD"` push — it appears after the `$NEW` block on title screen):
|
||||
|
||||
```actionscript
|
||||
this.MainPanel_mc.List_mc.entryList.push({
|
||||
"text":"$LOAD",
|
||||
"disabled":!this.HasRecentSave || !param6,
|
||||
"index":this.LOAD_INDEX
|
||||
});
|
||||
if(param8 == true)
|
||||
```
|
||||
|
||||
**Replace with** (add Multiplayer block between Load and `if(param8 == true)`):
|
||||
|
||||
```actionscript
|
||||
this.MainPanel_mc.List_mc.entryList.push({
|
||||
"text":"$LOAD",
|
||||
"disabled":!this.HasRecentSave || !param6,
|
||||
"index":this.LOAD_INDEX
|
||||
});
|
||||
if(!this.PauseMode)
|
||||
{
|
||||
this.MainPanel_mc.List_mc.entryList.push({
|
||||
"text":"Multiplayer",
|
||||
"index":this.MULTIPLAYER_INDEX
|
||||
});
|
||||
}
|
||||
if(param8 == true)
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
- `"Multiplayer"` is a literal label (no `$` prefix). Strings with `$` are looked up in `Interface/Translate_en.txt`.
|
||||
- `!this.PauseMode` limits the row to the **title screen**, not the in-game pause menu.
|
||||
- Row appears between **Load** and **Add-ons** (or PS5 transfer if enabled).
|
||||
|
||||
---
|
||||
|
||||
## Change 3 — Handle click in `onMainListItemPress`
|
||||
|
||||
**FFDec:** `MainMenu` → method **`onMainListItemPress`**
|
||||
|
||||
**Find this block** (search for `case this.HELP_INDEX:`):
|
||||
|
||||
```actionscript
|
||||
break;
|
||||
case this.HELP_INDEX:
|
||||
this.StartState(this.HELP_STATE);
|
||||
this.BGSCodeObj.PlayOKSound();
|
||||
break;
|
||||
```
|
||||
|
||||
**Insert immediately before** `case this.HELP_INDEX:`:
|
||||
|
||||
```actionscript
|
||||
break;
|
||||
case this.MULTIPLAYER_INDEX:
|
||||
this.BGSCodeObj.PlayOKSound();
|
||||
if(root.f4se != null && root.f4se.plugins != null && root.f4se.plugins.commonwealthOnline != null)
|
||||
{
|
||||
root.f4se.plugins.commonwealthOnline.openManager();
|
||||
}
|
||||
break;
|
||||
case this.HELP_INDEX:
|
||||
this.StartState(this.HELP_STATE);
|
||||
this.BGSCodeObj.PlayOKSound();
|
||||
break;
|
||||
```
|
||||
|
||||
This calls the C++ API registered by `CommonwealthOnline.dll` (`openManager` → PrismaUI server browser). **F9** still toggles the same browser.
|
||||
|
||||
---
|
||||
|
||||
## Change 4 — Add `CloseMultiplayerBrowser` method
|
||||
|
||||
**FFDec:** `MainMenu` → **add new method** (right-click class → Add script / Add method, or paste near other private helpers like `onMainListItemPress`)
|
||||
|
||||
**Add this entire method:**
|
||||
|
||||
```actionscript
|
||||
private function CloseMultiplayerBrowser() : *
|
||||
{
|
||||
if(root.f4se != null && root.f4se.plugins != null && root.f4se.plugins.commonwealthOnline != null)
|
||||
{
|
||||
root.f4se.plugins.commonwealthOnline.closeManager();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Change 5 — Close browser in `set currentState`
|
||||
|
||||
**FFDec:** `MainMenu` → method **`set currentState`** (may appear as `currentState` setter)
|
||||
|
||||
**Find:**
|
||||
|
||||
```actionscript
|
||||
public function set currentState(param1:String) : *
|
||||
{
|
||||
this.strCurrentState = param1;
|
||||
this.UpdateButtons(param1);
|
||||
this.UpdateStateFocus(param1);
|
||||
}
|
||||
```
|
||||
|
||||
**Change to:**
|
||||
|
||||
```actionscript
|
||||
public function set currentState(param1:String) : *
|
||||
{
|
||||
this.strCurrentState = param1;
|
||||
if(param1 != this.MAIN_STATE)
|
||||
{
|
||||
this.CloseMultiplayerBrowser();
|
||||
}
|
||||
this.UpdateButtons(param1);
|
||||
this.UpdateStateFocus(param1);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Change 6 — Close browser when another main-menu item is selected
|
||||
|
||||
**FFDec:** `MainMenu` → method **`onMainListItemPress`** (same method as Change 3)
|
||||
|
||||
**Find** the start of the `else` branch, after the `while` loop that returns to main state:
|
||||
|
||||
```actionscript
|
||||
while(this.strCurrentState != this.MAIN_STATE)
|
||||
{
|
||||
this.onCancelPress(true,false);
|
||||
}
|
||||
if(this.MainPanel_mc.List_mc.selectedEntry.index == this.CONTINUE_INDEX && this.PauseMode)
|
||||
```
|
||||
|
||||
**Insert between the `while` loop and the `CONTINUE_INDEX` check:**
|
||||
|
||||
```actionscript
|
||||
while(this.strCurrentState != this.MAIN_STATE)
|
||||
{
|
||||
this.onCancelPress(true,false);
|
||||
}
|
||||
if(this.MainPanel_mc.List_mc.selectedEntry.index != this.MULTIPLAYER_INDEX)
|
||||
{
|
||||
this.CloseMultiplayerBrowser();
|
||||
}
|
||||
if(this.MainPanel_mc.List_mc.selectedEntry.index == this.CONTINUE_INDEX && this.PauseMode)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Save and deploy
|
||||
|
||||
1. **File → Save** in FFDec (overwrite `Interface/MainMenu.swf`).
|
||||
2. Check file size: should be roughly **62,000–63,500 bytes**. If **64,000+**, restore vanilla and redo using **method-only** edits.
|
||||
3. Copy to game:
|
||||
|
||||
```
|
||||
Fallout 4/Data/Interface/MainMenu.swf
|
||||
```
|
||||
|
||||
4. Launch via **F4SE**.
|
||||
|
||||
---
|
||||
|
||||
## Test checklist
|
||||
|
||||
| Step | Expected |
|
||||
|------|----------|
|
||||
| Main menu (title screen) | **Multiplayer** appears after **Load** |
|
||||
| Pause menu → Settings | **No** Multiplayer row |
|
||||
| Click **Multiplayer** | Server browser opens |
|
||||
| **Settings → Gameplay** | No crash |
|
||||
| **Settings → Display** | No crash |
|
||||
| **Settings → Audio** | No crash |
|
||||
| **Settings → Controls** | Still works |
|
||||
| Open Multiplayer, then **Settings** | Browser closes |
|
||||
| **F9** | Still toggles browser |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Settings sub-menus crash again
|
||||
|
||||
You likely re-imported the full class. Run `.\tools\restore-vanilla-mainmenu.ps1` and redo changes **1–6** using **per-method** editing only.
|
||||
|
||||
### Multiplayer row missing
|
||||
|
||||
- Confirm `bInvalidateOlderFiles=1` in `Fallout4Custom.ini`.
|
||||
- Confirm loose `Data/Interface/MainMenu.swf` exists and is not overridden by another mod (check MO2 load order).
|
||||
- Confirm you added the `InitList` block with `if(!this.PauseMode)`.
|
||||
|
||||
### Multiplayer row visible but click does nothing
|
||||
|
||||
- Launch via **F4SE**, not vanilla exe.
|
||||
- Confirm `CommonwealthOnline.dll` is loading (check `Documents/My Games/Fallout4/F4SE/CommonwealthOnline.log`).
|
||||
- Confirm Change **3** (`case this.MULTIPLAYER_INDEX`) is present.
|
||||
|
||||
### Button shows `$MULTIPLAYER` or wrong text
|
||||
|
||||
Use literal `"Multiplayer"` (no `$`), or add a translation key to `Interface/Translate_en.txt`.
|
||||
|
||||
---
|
||||
|
||||
## Reference files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| [`MainMenu.swf.vanilla.bak`](../MainMenu.swf.vanilla.bak) | Untouched vanilla backup |
|
||||
| [`scripts/MainMenu.as`](scripts/MainMenu.as) | Full decompiled reference — **do not import wholesale** |
|
||||
| [`PATCH_MainMenu_Multiplayer.md`](PATCH_MainMenu_Multiplayer.md) | Short overview |
|
||||
| [`../../tools/restore-vanilla-mainmenu.ps1`](../../tools/restore-vanilla-mainmenu.ps1) | Restore vanilla SWF from game BA2 |
|
||||
|
||||
Plugin API (already implemented in C++):
|
||||
|
||||
- `root.f4se.plugins.commonwealthOnline.openManager()`
|
||||
- `root.f4se.plugins.commonwealthOnline.closeManager()`
|
||||
@@ -106,7 +106,7 @@ The `index` field is a **logical ID** used by `onMainListItemPress()` — it is
|
||||
|
||||
### Vanilla logical indices
|
||||
|
||||
From `Interface/exported/scripts/MainMenu.as` (reference only — do not re-import):
|
||||
From `docs/reference/Interface/exported/scripts/MainMenu.as` (reference only — do not re-import):
|
||||
|
||||
| Constant | Index | Typical label |
|
||||
|---|---|---|
|
||||
|
||||
@@ -55,9 +55,6 @@ In `CommonwealthOnline.log`, look for `[MainMenuInject]` lines:
|
||||
- `Injected Multiplayer row after Load.`
|
||||
- `Registered ITEM_PRESS listener ('…').`
|
||||
|
||||
## Deprecated approach
|
||||
## Reference material
|
||||
|
||||
The FFDec SWF patch guide is superseded by this document:
|
||||
|
||||
- [`FFDEC_MainMenu_Multiplayer_Guide.md`](FFDEC_MainMenu_Multiplayer_Guide.md) — historical reference only
|
||||
- [`PATCH_MainMenu_Multiplayer.md`](../Interface/exported/PATCH_MainMenu_Multiplayer.md) — historical reference only
|
||||
Decompiled vanilla `MainMenu` ActionScript for reading only (do not re-import) lives under [`docs/reference/Interface/`](reference/Interface/).
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
# MainMenu.swf — Multiplayer button patch guide
|
||||
|
||||
> **Full FFDec walkthrough:** see [`FFDEC_MainMenu_Multiplayer_Guide.md`](FFDEC_MainMenu_Multiplayer_Guide.md) for exact search anchors, before/after snippets, and a test checklist.
|
||||
|
||||
Add a **Multiplayer** item to the title-screen main menu. Selecting it calls `root.f4se.plugins.commonwealthOnline.openManager()`, which opens the PrismaUI server browser (same as **F9**).
|
||||
|
||||
## CRITICAL — edit methods, not the whole script
|
||||
|
||||
**Never** paste the full exported `MainMenu.as` into FFDec or use **Import script** on the whole class. That recompiles `RequestOptions` from decompiled source and **breaks Settings → Gameplay / Display / Audio**.
|
||||
|
||||
In FFDec, edit **individual methods** (double-click the method under **MainMenu** traits). FFDec then recompiles only that method and leaves vanilla bytecode (including `RequestOptions`) intact.
|
||||
|
||||
Restore vanilla any time:
|
||||
|
||||
```powershell
|
||||
.\tools\restore-vanilla-mainmenu.ps1
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [JPEXS FFDec](https://github.com/jindrapetrik/jpexs-decompiler/releases)
|
||||
- Game launched via **F4SE** with `CommonwealthOnline.dll` installed
|
||||
- Loose Interface overrides enabled in `Fallout4Custom.ini`:
|
||||
|
||||
```ini
|
||||
[Archive]
|
||||
bInvalidateOlderFiles=1
|
||||
```
|
||||
|
||||
## FFDec workflow
|
||||
|
||||
1. Copy `Interface/MainMenu.swf.vanilla.bak` → `Interface/MainMenu.swf` (start from vanilla every time).
|
||||
2. Open `Interface/MainMenu.swf` in FFDec.
|
||||
3. In the left tree: **scripts → MainMenu** (class traits list, not “Edit ActionScript” on the whole class).
|
||||
4. Apply the **four method-level edits** below (double-click each method to open its editor, save after each).
|
||||
5. **File → Save**.
|
||||
6. Sanity check: patched SWF should stay **~62–63 KB**. If it jumps to **64 KB+**, settings are likely broken — restore vanilla and retry using **method** editors only.
|
||||
7. Copy to `Fallout 4/Data/Interface/MainMenu.swf`.
|
||||
8. Test **Settings → Gameplay / Display / Audio** first, then **Multiplayer**.
|
||||
|
||||
---
|
||||
|
||||
## Edit 1 — Add constant (class body)
|
||||
|
||||
Open the **MainMenu** class initializer / constants section (or edit at top of class in trait view). After `INSTALLED_CONTENT_INDEX`:
|
||||
|
||||
```actionscript
|
||||
private const MULTIPLAYER_INDEX:Number = 15;
|
||||
```
|
||||
|
||||
Index `15` is unused in vanilla (highest is `INSTALLED_CONTENT_INDEX = 14`).
|
||||
|
||||
---
|
||||
|
||||
## Edit 2 — `InitList()` — add menu row
|
||||
|
||||
In **`InitList`**, find the `$LOAD` push (after `$NEW` on title screen). Insert **before** the PS5 transfer block:
|
||||
|
||||
```actionscript
|
||||
this.MainPanel_mc.List_mc.entryList.push({
|
||||
"text":"$LOAD",
|
||||
"disabled":!this.HasRecentSave || !param6,
|
||||
"index":this.LOAD_INDEX
|
||||
});
|
||||
if(!this.PauseMode)
|
||||
{
|
||||
this.MainPanel_mc.List_mc.entryList.push({
|
||||
"text":"Multiplayer",
|
||||
"index":this.MULTIPLAYER_INDEX
|
||||
});
|
||||
}
|
||||
if(param8 == true)
|
||||
```
|
||||
|
||||
Use literal `"Multiplayer"` (no `$` prefix) unless you add a row to `Interface/Translate_en.txt`.
|
||||
|
||||
---
|
||||
|
||||
## Edit 3 — `onMainListItemPress()` — handle click
|
||||
|
||||
In the main `switch(this.MainPanel_mc.List_mc.selectedEntry.index)`, add **before** `case this.HELP_INDEX:`:
|
||||
|
||||
```actionscript
|
||||
case this.MULTIPLAYER_INDEX:
|
||||
this.BGSCodeObj.PlayOKSound();
|
||||
if(root.f4se != null && root.f4se.plugins != null && root.f4se.plugins.commonwealthOnline != null)
|
||||
{
|
||||
root.f4se.plugins.commonwealthOnline.openManager();
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 4 (recommended) — auto-close browser
|
||||
|
||||
The plugin also hides the browser when the menu leaves `MAIN_STATE`, but these SWF hooks close it immediately when navigating away.
|
||||
|
||||
### 4a — Add new method `CloseMultiplayerBrowser`
|
||||
|
||||
In FFDec: **MainMenu → right-click → Add script** / add method:
|
||||
|
||||
```actionscript
|
||||
private function CloseMultiplayerBrowser() : *
|
||||
{
|
||||
if(root.f4se != null && root.f4se.plugins != null && root.f4se.plugins.commonwealthOnline != null)
|
||||
{
|
||||
root.f4se.plugins.commonwealthOnline.closeManager();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4b — Edit `set currentState`
|
||||
|
||||
After `this.strCurrentState = param1;`:
|
||||
|
||||
```actionscript
|
||||
this.strCurrentState = param1;
|
||||
if(param1 != this.MAIN_STATE)
|
||||
{
|
||||
this.CloseMultiplayerBrowser();
|
||||
}
|
||||
this.UpdateButtons(param1);
|
||||
```
|
||||
|
||||
### 4c — Edit `onMainListItemPress`
|
||||
|
||||
After the `while(this.strCurrentState != this.MAIN_STATE)` loop, before the `switch`:
|
||||
|
||||
```actionscript
|
||||
if(this.MainPanel_mc.List_mc.selectedEntry.index != this.MULTIPLAYER_INDEX)
|
||||
{
|
||||
this.CloseMultiplayerBrowser();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test plan
|
||||
|
||||
| Step | Expected |
|
||||
|------|----------|
|
||||
| Main menu shows **Multiplayer** between Load and Add-ons | Yes |
|
||||
| Click **Multiplayer** | Server browser opens |
|
||||
| Open **Settings → Display** | No crash |
|
||||
| Open **Settings → Gameplay / Audio** | No crash |
|
||||
| Select **Continue** while browser open | Browser closes |
|
||||
| **F9** | Still toggles browser |
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
Full decompiled class (do **not** import wholesale): [`scripts/MainMenu.as`](scripts/MainMenu.as)
|
||||
|
||||
Plugin API registered in `CommonwealthOnline.dll`:
|
||||
|
||||
- `root.f4se.plugins.commonwealthOnline.openManager()`
|
||||
- `root.f4se.plugins.commonwealthOnline.closeManager()`
|
||||
@@ -1,390 +0,0 @@
|
||||
# Phases 1-6 Complete: TiltedEvolution Alignment Implementation Summary
|
||||
|
||||
## Project Milestone: Animation Synchronization Architecture Complete ✅
|
||||
|
||||
**Date:** June 3, 2026
|
||||
**Status:** Ready for Phase 6 In-Game Testing
|
||||
**Effort:** ~40 hours (Phases 1-5 complete)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Successfully aligned Commonwealth Online with TiltedEvolution's proven multiplayer animation synchronization architecture. Implemented complete stack from network protocol through animation descriptor system to action replay infrastructure.
|
||||
|
||||
**Key Achievement:** Proxy actors can now theoretically display working animations by:
|
||||
1. Dynamically spawning proxies
|
||||
2. Injecting character controller velocity
|
||||
3. Replicating actor state flags
|
||||
4. Synchronizing animation graph variables via descriptor indexing
|
||||
5. Queuing discrete action events for replay
|
||||
|
||||
---
|
||||
|
||||
## Phase-by-Phase Deliverables
|
||||
|
||||
### Phase 1: Blueprint & Research ✅
|
||||
**Status:** Complete
|
||||
**Output:** Architecture documentation + TiltedEvolution alignment analysis
|
||||
|
||||
- Created `docs/f4-animation-descriptor.md`
|
||||
- FO4 humanoid graph variable mapping (9 floats, 6 bools, 3 ints)
|
||||
- Comparison with Skyrim SE model
|
||||
- Variable roles and network sync requirements
|
||||
|
||||
- Created `docs/animation-architecture-alignment.md`
|
||||
- Component-by-component alignment (7 major components)
|
||||
- Data flow diagrams (3 flow models)
|
||||
- FO4-specific adaptations identified
|
||||
- Integration points for Phases 2-5 documented
|
||||
|
||||
- Created `docs/animation-sync-analysis.md`
|
||||
- Detailed analysis of current failure modes
|
||||
- TiltedEvolution solutions explained
|
||||
- Root cause analysis documented
|
||||
|
||||
**Result:** Clear architectural path forward with no blockers identified.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: State & Protocol ✅
|
||||
**Status:** Complete
|
||||
**Output:** Network protocol extended + state structure updated
|
||||
|
||||
**Modified Files:**
|
||||
- `plugin/include/F4TRemotePlayerState.h`
|
||||
- Extended struct with actor state flags (flags1, flags2)
|
||||
- Added action event queue
|
||||
- Created `RemoteActionEvent` struct
|
||||
|
||||
- `plugin/src/F4TNetworking.cpp`
|
||||
- Updated JSON parsing for new fields
|
||||
- Maintained backward compatibility (optional fields)
|
||||
- Enhanced logging with flag values
|
||||
|
||||
- `server/fake_client.py`
|
||||
- Added actor state flag parsing
|
||||
- Updated display to show flags in hex format
|
||||
|
||||
**New Files:**
|
||||
- `docs/protocol-phase2-extensions.md`
|
||||
- Full protocol specification
|
||||
- Actor state flag reference table
|
||||
- Action event structure documented
|
||||
|
||||
**Result:** Network protocol extensible for action replay + state sync. Fully backward compatible.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Animation Descriptors ✅
|
||||
**Status:** Complete
|
||||
**Output:** Descriptor-based variable indexing infrastructure
|
||||
|
||||
**Created Files:**
|
||||
- `plugin/include/F4AnimationDescriptor.h`
|
||||
- Class definition with full API
|
||||
- Singleton pattern
|
||||
- Public query/read/write methods
|
||||
|
||||
- `plugin/src/F4AnimationDescriptor.cpp`
|
||||
- Implementation with humanoid graph variable tables
|
||||
- Reverse lookup maps (name → index)
|
||||
- Bulk read/write methods (`SaveAnimationVariablesFromCache`, `LoadAnimationVariablesToCache`)
|
||||
- Thread-safe initialization
|
||||
|
||||
**Features:**
|
||||
- Pre-computed variable tables (one-time init)
|
||||
- Efficient O(1) index-based access
|
||||
- Single graph manager lock for batch operations
|
||||
- Graceful fallback to string-based API
|
||||
|
||||
**Result:** Efficient animation variable synchronization with 3-5x performance gain over string-based writes.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Dynamic Proxy Spawning & Velocity ✅
|
||||
**Status:** Complete
|
||||
**Output:** Dynamic proxy spawning + velocity injection integration
|
||||
|
||||
**Modified Files:**
|
||||
- `plugin/src/F4TProxyActorController.cpp`
|
||||
- Added `g_dynamicProxyPool` (playerId → ActorHandle map)
|
||||
- Implemented `SpawnDynamicProxyActor()` function
|
||||
- Implemented `GetOrSpawnDynamicProxy()` function
|
||||
- Modified `TryResolveSlotProxy()` to prefer dynamic spawn
|
||||
- Enhanced `MoveSlotProxyToRemotePlayer()` with actor state flag application
|
||||
|
||||
**New Infrastructure:**
|
||||
- Dynamic spawning via `Player::PlaceAtMe()`
|
||||
- Proxy reuse system (keep actors, reuse by playerId)
|
||||
- Character controller velocity injection (already present, verified)
|
||||
- Actor state flag synchronization
|
||||
|
||||
**Result:** Proxies spawn dynamically with proper velocity injection + state synchronization. Fallback to pre-placed pool maintained.
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Action Replay & Descriptor Sync ✅
|
||||
**Status:** Complete
|
||||
**Output:** Action queue system + descriptor-based animation sync
|
||||
|
||||
**Created Files:**
|
||||
- `plugin/include/F4RemoteActionComponent.h`
|
||||
- `RemoteActionSnapshot` struct
|
||||
- `RemoteActionQueue` class
|
||||
- Enqueue/replay/clear methods
|
||||
|
||||
- `plugin/src/F4RemoteActionComponent.cpp`
|
||||
- Full action queue implementation
|
||||
- Actor state application from snapshot
|
||||
- Animation variable loading via descriptor
|
||||
- Comprehensive logging
|
||||
|
||||
**Modified Files:**
|
||||
- `plugin/src/F4TProxyActorController.cpp`
|
||||
- Added `actionQueue` member to `ProxyActorSlot`
|
||||
- Each proxy has its own action queue
|
||||
|
||||
- `plugin/src/F4TProxyAnimationSync.cpp`
|
||||
- Added `ApplyProxyAnimationFromRemoteStateDescriptorBased()`
|
||||
- Uses descriptor-based bulk writes
|
||||
- Replaces per-frame string lookups
|
||||
|
||||
**Result:** Action replay infrastructure + efficient descriptor-based animation sync ready for integration.
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Testing & Iteration Framework ✅
|
||||
**Status:** Ready for In-Game Testing
|
||||
**Output:** Comprehensive testing documentation + quick-start guide
|
||||
|
||||
**Created Files:**
|
||||
- `docs/phase6-testing-guide.md` (8,000+ words)
|
||||
- Complete testing procedures
|
||||
- 5 critical test categories
|
||||
- Performance benchmarking
|
||||
- Debugging checklist
|
||||
- Common issues + fixes
|
||||
- Success criteria
|
||||
|
||||
- `docs/phase6-quick-start.md`
|
||||
- 30-minute critical path for testing
|
||||
- Console log patterns (good vs bad)
|
||||
- Performance expectations
|
||||
- Common fixes quick reference
|
||||
- Results template
|
||||
|
||||
**Test Coverage:**
|
||||
1. Dynamic Proxy Spawning
|
||||
2. Smooth Movement Synchronization
|
||||
3. Velocity Injection & Animation Triggering (CRITICAL)
|
||||
4. Actor State Flags & Sneak Animation
|
||||
5. Multiple Proxy Actors (stability)
|
||||
6. Performance & Stability Profiling
|
||||
7. Velocity Scaling Tuning
|
||||
8. Variable Index Verification
|
||||
|
||||
**Result:** Ready for in-game validation. Testing framework ensures comprehensive coverage of all phases.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
### Before (Pre-Phase 1)
|
||||
```
|
||||
SetPosition-only movement
|
||||
↓
|
||||
No character controller velocity
|
||||
↓
|
||||
Animation system sees 0 velocity
|
||||
↓
|
||||
No animations play ❌
|
||||
```
|
||||
|
||||
### After (Post-Phase 5)
|
||||
```
|
||||
Dynamic proxy spawn
|
||||
+ Character controller velocity injection
|
||||
+ Actor state flag synchronization
|
||||
+ Descriptor-based animation variable sync
|
||||
+ Action event replay queue
|
||||
↓
|
||||
Animation system sees real velocity + proper graph state
|
||||
↓
|
||||
Animations should play ✅ (to be verified Phase 6)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Statistics
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Files created | 8 |
|
||||
| Files modified | 6 |
|
||||
| C++ code added | ~800 lines |
|
||||
| Python code updated | ~100 lines |
|
||||
| Documentation added | ~15,000 words |
|
||||
| Total lines of code | ~5,000 lines |
|
||||
|
||||
---
|
||||
|
||||
## Key Technical Achievements
|
||||
|
||||
1. **Descriptor-Based Animation Sync (Phase 3+5)**
|
||||
- O(1) indexed variable access vs O(n) string lookup
|
||||
- Single graph manager lock vs multiple locks
|
||||
- 3-5x performance improvement
|
||||
|
||||
2. **Dynamic Proxy Spawning (Phase 4)**
|
||||
- Replaced pre-placed pool with dynamic spawning
|
||||
- Maintains slot-based reuse (4 concurrent proxies)
|
||||
- Fallback to pre-placed for compatibility
|
||||
|
||||
3. **Character Controller Velocity Injection (Phase 4)**
|
||||
- Verified working via `Move(0.016f, delta, false)` + `SetLinearVelocityImpl()`
|
||||
- Enables animation system velocity evaluation
|
||||
- Clamps to 400 units/sec max
|
||||
|
||||
4. **Actor State Synchronization (Phase 4)**
|
||||
- Replicates `actorState.flags1/2` from remote player
|
||||
- Applied before animation sync for FSM precedence
|
||||
- Enables state-driven animation transitions
|
||||
|
||||
5. **Action Replay Infrastructure (Phase 5)**
|
||||
- `RemoteActionQueue` per proxy slot
|
||||
- Action snapshots with full state
|
||||
- Max 16 pending actions per queue
|
||||
- Ready for Phase 2.3 action capture
|
||||
|
||||
---
|
||||
|
||||
## Testing Readiness
|
||||
|
||||
**Ready for Phase 6 In-Game Testing:**
|
||||
- ✅ Dynamic proxies spawn successfully
|
||||
- ✅ Velocity injection framework in place
|
||||
- ✅ Actor state flags integrated
|
||||
- ✅ Descriptor-based sync implemented
|
||||
- ✅ Action queue infrastructure ready
|
||||
- ✅ Network protocol extended
|
||||
- ✅ Comprehensive testing documentation
|
||||
- ✅ Quick-start guide for testing
|
||||
|
||||
**Critical Test (Test 3):**
|
||||
- Will verify if velocity injection triggers animations
|
||||
- If PASS → Proceed to Phase 7
|
||||
- If FAIL → Iterate Phase 6.3 (tune variables/scaling)
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
### Phase 6: In-Game Testing (NOW)
|
||||
1. Build plugin with all changes
|
||||
2. Load Fallout 4 with F4SE
|
||||
3. Run Test 1-5 as per guide
|
||||
4. Document results
|
||||
5. Tune if needed (Phase 6.3)
|
||||
|
||||
### Phase 7: Cleanup & Documentation (After Phase 6)
|
||||
1. Remove debug gates/scaffolding
|
||||
2. Finalize architecture documentation
|
||||
3. Update protocol documentation
|
||||
4. Create animation descriptor guide
|
||||
5. Production-ready code
|
||||
|
||||
---
|
||||
|
||||
## Known Unknowns (To Be Verified Phase 6)
|
||||
|
||||
1. **Will velocity injection trigger animations on dynamic proxies?**
|
||||
- Theoretical: Yes (all pieces in place)
|
||||
- Practical: To be verified in-game
|
||||
|
||||
2. **Are animation variable indices correct for FO4?**
|
||||
- Based on: Skyrim SE comparison + FO4 theory
|
||||
- Verification: Phase 6 testing + debug logging
|
||||
|
||||
3. **Is 400 units/sec velocity clamping appropriate?**
|
||||
- Assumption: Yes, based on estimated graph limits
|
||||
- Tuning: Phase 6.3 iteration if needed
|
||||
|
||||
4. **Will actor state flags properly guide animation FSM?**
|
||||
- Theory: Yes (same as Skyrim)
|
||||
- Practice: Phase 6 testing
|
||||
|
||||
5. **Will action replay (Phase 2.3) integrate cleanly?**
|
||||
- Infrastructure: Ready (Phase 5.1)
|
||||
- Capture: Deferred to Phase 7 or later
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Phase 1-5 (Completed)
|
||||
- ✅ Architecture aligned with TiltedEvolution
|
||||
- ✅ Protocol extended without breaking changes
|
||||
- ✅ Descriptor infrastructure implemented
|
||||
- ✅ Dynamic spawning system operational
|
||||
- ✅ Action queue infrastructure ready
|
||||
- ✅ Code compiles without errors
|
||||
- ✅ ~5,000 lines of tested code
|
||||
|
||||
### Phase 6 (In Progress)
|
||||
- Testing framework: ✅ Complete
|
||||
- In-game validation: ⏳ Awaiting developer testing
|
||||
- Performance profiling: ⏳ Awaiting testing
|
||||
- Variable tuning: ⏳ Awaiting testing
|
||||
|
||||
### Phase 7 (Pending)
|
||||
- Code cleanup
|
||||
- Documentation finalization
|
||||
- Production release
|
||||
|
||||
---
|
||||
|
||||
## Repository Status
|
||||
|
||||
**All files committed:**
|
||||
```
|
||||
Phases 1-5 code: Complete
|
||||
Phases 1-5 docs: Complete
|
||||
Phase 6 testing framework: Complete
|
||||
Ready for in-game validation
|
||||
```
|
||||
|
||||
**Total effort:** ~40 hours (Phases 1-5)
|
||||
**Estimated Phase 6:** 2-4 hours testing + tuning
|
||||
**Estimated Phase 7:** 2-3 hours cleanup + docs
|
||||
**Total project:** ~48-50 hours
|
||||
|
||||
---
|
||||
|
||||
## Critical Success Path for Phase 6
|
||||
|
||||
1. Build + load plugin
|
||||
2. Run Test 3 (Animations)
|
||||
- IF PASS: Proceed to Phase 7
|
||||
- IF FAIL: Debug + iterate Phase 6.3
|
||||
3. Document results
|
||||
4. Proceed to Phase 7 when all tests pass
|
||||
|
||||
**Recommendation:** Run Test 3 first as it's the make-or-break test for the entire animation system.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Phases 1-5 successfully implement a complete, TiltedEvolution-aligned animation synchronization architecture for Commonwealth Online. All foundational systems are in place:
|
||||
|
||||
- ✅ Descriptor-based efficient variable sync
|
||||
- ✅ Dynamic proxy spawning with reuse
|
||||
- ✅ Character controller velocity injection
|
||||
- ✅ Actor state flag replication
|
||||
- ✅ Action event queue infrastructure
|
||||
|
||||
**Phase 6 will validate whether the theoretical implementation achieves the goal of working proxy animations in-game.**
|
||||
|
||||
Ready to test! 🚀
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Documentation
|
||||
|
||||
Start here for Commonwealth Online project docs.
|
||||
|
||||
## Core
|
||||
|
||||
| Document | Purpose |
|
||||
|---|---|
|
||||
| [`setup.md`](setup.md) | Build, deploy, submodules, main-menu notes |
|
||||
| [`architecture.md`](architecture.md) | Plugin, server, proxy, threading model |
|
||||
| [`protocol.md`](protocol.md) | Message types and LAN discovery overview |
|
||||
| [`protocol/`](protocol/) | Detailed packet and sync field reference |
|
||||
| [`limitations.md`](limitations.md) | Technical limits and unsupported systems |
|
||||
| [`version-targets.md`](version-targets.md) | Supported game/F4SE versions |
|
||||
| [`ROADMAP.md`](ROADMAP.md) | Vertical-slice roadmap and milestones |
|
||||
| [`dev-log.md`](dev-log.md) | Milestones, testing notes, next steps |
|
||||
|
||||
## Feature and integration guides
|
||||
|
||||
| Document | Purpose |
|
||||
|---|---|
|
||||
| [`weapon-animation-sync.md`](weapon-animation-sync.md) | Weapon animation graph reference |
|
||||
| [`f4-animation-descriptor.md`](f4-animation-descriptor.md) | Humanoid animation graph variable mapping |
|
||||
| [`animation-architecture-alignment.md`](animation-architecture-alignment.md) | Animation sync architecture notes |
|
||||
| [`animation-sync-analysis.md`](animation-sync-analysis.md) | Animation sync problem analysis |
|
||||
| [`protocol-phase2-extensions.md`](protocol-phase2-extensions.md) | Phase 2 protocol field extensions |
|
||||
| [`MainMenu_Injection.md`](MainMenu_Injection.md) | Runtime main-menu Multiplayer row injection |
|
||||
| [`MainMenu_Button_Injection_Guide.md`](MainMenu_Button_Injection_Guide.md) | Full main-menu injection how-to |
|
||||
| [`pause-menu-vanilla-settings-plan.md`](pause-menu-vanilla-settings-plan.md) | Planned vanilla settings integration |
|
||||
| [`project-comparison.md`](project-comparison.md) | Commonwealth Online vs DoxyCoSync |
|
||||
|
||||
## Research and reference
|
||||
|
||||
| Location | Purpose |
|
||||
|---|---|
|
||||
| [`research/`](research/) | Engine notes, sync research, related projects |
|
||||
| [`reference/Interface/`](reference/Interface/) | Decompiled vanilla main-menu SWF reference (read-only) |
|
||||
|
||||
## Outside this folder
|
||||
|
||||
| Document | Purpose |
|
||||
|---|---|
|
||||
| [`../README.md`](../README.md) | Project overview and quick start |
|
||||
| [`../changelog.md`](../changelog.md) | Change history |
|
||||
| [`../server/README.md`](../server/README.md) | Relay server and dev GUI |
|
||||
| [`../plugin/setup.md`](../plugin/setup.md) | Plugin build notes |
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
# Commonwealth Online Roadmap
|
||||
|
||||
## Current Status: Working Internal Vertical Slice
|
||||
|
||||
Commonwealth Online currently supports real Fallout 4 clients connecting through an external relay server, synchronizing player transforms, representing remote players with proxy actors, using the multiplayer browser UI, discovering LAN servers, saving recent servers, and syncing basic world state such as time and weather.
|
||||
|
||||
The roadmap now tracks playable vertical slices rather than early technical bootstrap milestones.
|
||||
|
||||
---
|
||||
|
||||
## Slice 0: Vault 109 Onboarding
|
||||
|
||||
### Goal
|
||||
|
||||
A first-time player joining a Commonwealth Online server is placed into Vault 109, creates their character, sets SPECIAL stats, receives basic server context, and exits into the Commonwealth as a valid multiplayer character.
|
||||
|
||||
### Player Experience
|
||||
|
||||
1. Player opens Multiplayer from the main menu.
|
||||
2. Player joins a server.
|
||||
3. If the player has no character/save for that server, a new character is created.
|
||||
4. Player spawns in Vault 109.
|
||||
5. Player customizes appearance.
|
||||
6. Player completes the Vault 109 SPECIAL setup process.
|
||||
7. Player receives basic lore/context for the server.
|
||||
8. Player exits the vault.
|
||||
9. The server/client marks onboarding as complete.
|
||||
10. Future joins spawn the player from their saved multiplayer state instead of restarting Vault 109.
|
||||
|
||||
### Includes
|
||||
|
||||
- Vault 109 interior
|
||||
- First-join detection
|
||||
- New multiplayer character creation flow
|
||||
- Appearance setup
|
||||
- SPECIAL setup
|
||||
- Intro quest or onboarding state
|
||||
- Vault exit sequence
|
||||
- Save/profile persistence
|
||||
- “Cannot return after leaving” rule if desired
|
||||
|
||||
### Internal Milestones
|
||||
|
||||
- [ ] Create Vault 109 start cell
|
||||
- [ ] Add first-spawn marker
|
||||
- [ ] Route first-time server players to Vault 109
|
||||
- [ ] Open appearance customization
|
||||
- [ ] Build SPECIAL allocation flow
|
||||
- [ ] Save onboarding completion state
|
||||
- [ ] Move player to Commonwealth exit point
|
||||
- [ ] Prevent repeat onboarding for existing server characters
|
||||
- [ ] Test joining same server again
|
||||
- [ ] Test joining a different server for the first time
|
||||
|
||||
### Done When
|
||||
|
||||
A player can join a server with no existing multiplayer save, create a character in Vault 109, assign SPECIAL stats, leave the vault, reconnect later, and continue as the same character.
|
||||
|
||||
---
|
||||
|
||||
## Slice 1: Same-Cell Multiplayer Foundation
|
||||
|
||||
### Goal
|
||||
|
||||
Two or more players can join the same controlled test cell and see each other moving reliably.
|
||||
|
||||
### Includes
|
||||
|
||||
- Server relay
|
||||
- Player IDs
|
||||
- Transform sync
|
||||
- Remote proxy actors
|
||||
- Movement smoothing
|
||||
- Disconnect cleanup
|
||||
- Fake client testing tools
|
||||
- Dev server GUI
|
||||
|
||||
### Internal Milestones
|
||||
|
||||
- [x] Plugin connects to server
|
||||
- [x] Server assigns player IDs
|
||||
- [x] Transform packets relay between clients
|
||||
- [x] Remote player state is stored safely
|
||||
- [x] Proxy actors represent remote players
|
||||
- [x] Movement smoothing works
|
||||
- [x] Disconnect removes or holds remote representation correctly
|
||||
- [x] Fake clients can simulate remote players
|
||||
- [x] Dev server GUI can start/stop server and manage fake clients
|
||||
|
||||
### Done When
|
||||
|
||||
A two-client test in `F4TTestCell01` works repeatedly without manual recovery.
|
||||
|
||||
---
|
||||
|
||||
## Slice 2: Multiplayer Menu and Join Flow
|
||||
|
||||
### Goal
|
||||
|
||||
Players can access Commonwealth Online from Fallout 4’s main menu using a native-feeling multiplayer browser.
|
||||
|
||||
### Includes
|
||||
|
||||
- Main menu Multiplayer button
|
||||
- PrismaUI / Ultralight browser
|
||||
- Direct Connect
|
||||
- Local servers tab
|
||||
- LAN discovery
|
||||
- Recent servers
|
||||
- Controller navigation
|
||||
- Mouse/controller cursor handling
|
||||
- UI scaling for different resolutions
|
||||
|
||||
### Internal Milestones
|
||||
|
||||
- [x] Multiplayer menu opens from main menu
|
||||
- [x] Direct Connect works
|
||||
- [x] Local tab scans LAN servers
|
||||
- [x] Recent servers persist
|
||||
- [x] Controller can navigate browser
|
||||
- [x] Mouse/controller mode switching works
|
||||
- [x] UI scales correctly at 1080p, 1440p, and 4K
|
||||
|
||||
### Done When
|
||||
|
||||
A player can launch Fallout 4, open Multiplayer, find or enter a server, connect, disconnect, and reconnect without console commands.
|
||||
|
||||
---
|
||||
|
||||
## Slice 3: Player State and Animation MVP
|
||||
|
||||
### Goal
|
||||
|
||||
Remote players feel more like players rather than sliding markers.
|
||||
|
||||
### Includes
|
||||
|
||||
- Movement state sync
|
||||
- Walk/run/sprint state
|
||||
- Sneak state
|
||||
- Jump state
|
||||
- Weapon drawn state
|
||||
- Proxy animation or animation-adjacent visual behavior
|
||||
- Fake-client animation test scripts
|
||||
|
||||
### Internal Milestones
|
||||
|
||||
- [x] Movement state fields are sent
|
||||
- [x] Fake clients can walk, circle, jump, sneak, teleport, leave cell, and return
|
||||
- [x] Proxy actors respond to synced movement state
|
||||
- [x] Animation graph and AI behavior experiments are documented
|
||||
- [x] Working in-game behavior is confirmed
|
||||
|
||||
### Done When
|
||||
|
||||
A remote player can move around the test cell and visually communicate their main state clearly enough for internal playtesting.
|
||||
|
||||
---
|
||||
|
||||
## Slice 4: World-State Sync MVP
|
||||
|
||||
### Goal
|
||||
|
||||
Connected players share basic world conditions so the session feels consistent.
|
||||
|
||||
### Includes
|
||||
|
||||
- Host-authoritative world state
|
||||
- Time sync
|
||||
- Weather sync
|
||||
- Host reassignment
|
||||
- Fake-client world-state visibility
|
||||
- Protocol documentation
|
||||
|
||||
### Internal Milestones
|
||||
|
||||
- [x] First client becomes world-state host
|
||||
- [x] Host sends time/weather state
|
||||
- [x] Server validates host-only world-state packets
|
||||
- [x] Non-host clients apply world state
|
||||
- [x] Host disconnect promotes next lowest player ID
|
||||
- [x] Promoted host seeds state from local synced values
|
||||
|
||||
### Done When
|
||||
|
||||
Three clients can connect, share time/weather, lose the host, promote a new host, and continue without a visible world-state jump.
|
||||
|
||||
---
|
||||
|
||||
## Slice 5: Internal Playtest Package
|
||||
|
||||
### Goal
|
||||
|
||||
The project can be packaged, deployed, and tested by someone other than the developer.
|
||||
|
||||
### Includes
|
||||
|
||||
- Build scripts
|
||||
- Deploy scripts
|
||||
- Proper Fallout 4 `Data` layout
|
||||
- ESP staging
|
||||
- Mesh/material/texture staging
|
||||
- PrismaUI staging
|
||||
- Setup documentation
|
||||
- Troubleshooting documentation
|
||||
- Known-good test profile
|
||||
|
||||
### Internal Milestones
|
||||
|
||||
- [x] Build output stages to `build/Fallout 4/Data`
|
||||
- [x] ESPs are included
|
||||
- [x] Assets are included
|
||||
- [x] PrismaUI and Ultralight files are included
|
||||
- [x] Fallout 4 path resolver works
|
||||
- [x] Clean install test
|
||||
- [x] Mod Organizer 2 install test
|
||||
- [x] Second-machine install test
|
||||
- [x] Steam Deck or LAN client test package
|
||||
|
||||
### Done When
|
||||
|
||||
A fresh machine can install the package, start the server, join from Fallout 4, and complete the test-cell playtest checklist.
|
||||
|
||||
---
|
||||
|
||||
## Slice 6: Small Multiplayer Session
|
||||
|
||||
### Goal
|
||||
|
||||
Commonwealth Online supports a small structured session with several players in a controlled area.
|
||||
|
||||
### Includes
|
||||
|
||||
- 2 to 4 real clients
|
||||
- Stable proxy assignment
|
||||
- Join/rejoin handling
|
||||
- Disconnect handling
|
||||
- LAN and direct connect support
|
||||
- World-state sync
|
||||
- Basic session rules
|
||||
- Debug logging for failures
|
||||
|
||||
### Internal Milestones
|
||||
|
||||
- [ ] 2 real clients stable for 15 minutes
|
||||
- [ ] 3 real clients stable for 15 minutes
|
||||
- [ ] 4 real clients stable for 15 minutes
|
||||
- [ ] Host disconnect test
|
||||
- [ ] Client reconnect test
|
||||
- [ ] Cell leave/return test
|
||||
- [ ] Server restart recovery behavior documented
|
||||
|
||||
### Done When
|
||||
|
||||
A small group can complete a repeatable test session without developer intervention.
|
||||
|
||||
---
|
||||
|
||||
## Slice 7: Gameplay Sync Experiments
|
||||
|
||||
### Goal
|
||||
|
||||
Start testing actual gameplay interactions after the multiplayer foundation is stable.
|
||||
|
||||
### Candidate Systems
|
||||
|
||||
- Basic health sync
|
||||
- Basic damage events
|
||||
- Weapon fire events
|
||||
- Simple interaction events
|
||||
- Power armor enter/exit
|
||||
- Settlement placement experiments
|
||||
- Chat or player identity
|
||||
- Enemy sync research
|
||||
|
||||
### Rules
|
||||
|
||||
These are not added until the earlier vertical slices are stable. Each gameplay system must be tested as an isolated experiment before becoming part of the main branch.
|
||||
|
||||
---
|
||||
|
||||
## Later Slices
|
||||
|
||||
### Wider World Support
|
||||
|
||||
- Exterior worldspace testing
|
||||
- Interior/exterior transitions
|
||||
- Multiple cells
|
||||
- Fast travel policy
|
||||
- Reconnect position handling
|
||||
|
||||
### Session UX
|
||||
|
||||
- Player names
|
||||
- Server metadata
|
||||
- Favorites
|
||||
- Passwords or private sessions
|
||||
- Server browser polish
|
||||
- Error messages
|
||||
|
||||
### Gameplay Layer
|
||||
|
||||
- Combat
|
||||
- Damage
|
||||
- Inventory policy
|
||||
- Quest policy
|
||||
- Settlement sync
|
||||
- NPC/enemy strategy
|
||||
@@ -1,65 +0,0 @@
|
||||
# Roadmap
|
||||
|
||||
## Phase 0: Research and Setup
|
||||
|
||||
- [ ] Create repository structure
|
||||
- [ ] Document project scope
|
||||
- [ ] Document legal boundaries
|
||||
- [ ] Set up Fallout 4 test profile
|
||||
- [ ] Install F4SE
|
||||
- [ ] Install Creation Kit
|
||||
- [ ] Choose CommonLibF4 template
|
||||
- [ ] Create test cell plan
|
||||
|
||||
## Phase 1: F4SE Plugin Bootstrap
|
||||
|
||||
- [ ] Create basic F4SE plugin project
|
||||
- [ ] Confirm plugin loads in Fallout 4
|
||||
- [ ] Create plugin log output
|
||||
- [ ] Detect game version
|
||||
- [ ] Read local player reference
|
||||
- [ ] Read local player position
|
||||
- [ ] Write position to log
|
||||
|
||||
## Phase 2: External Server Prototype
|
||||
|
||||
- [ ] Create basic local server
|
||||
- [ ] Define packet format
|
||||
- [ ] Support client connect/disconnect
|
||||
- [ ] Assign player IDs
|
||||
- [ ] Receive transform packets
|
||||
- [ ] Broadcast transform packets to other clients
|
||||
|
||||
## Phase 3: Fallout 4 Network Connection
|
||||
|
||||
- [ ] Connect F4SE plugin to local server
|
||||
- [ ] Send test packets from Fallout 4
|
||||
- [ ] Send real player transform packets
|
||||
- [ ] Receive remote transform packets
|
||||
- [ ] Store remote player state safely
|
||||
|
||||
## Phase 4: Remote Player Representation
|
||||
|
||||
- [ ] Create test cell in Creation Kit
|
||||
- [ ] Create placeholder remote player actor
|
||||
- [ ] Spawn or place remote actor
|
||||
- [ ] Move remote actor using received transform data
|
||||
- [ ] Smooth remote actor movement
|
||||
- [ ] Clean up actor on disconnect
|
||||
|
||||
## Phase 5: Basic Gameplay Sync
|
||||
|
||||
- [ ] Sync crouch state
|
||||
- [ ] Sync sprint/walk state
|
||||
- [ ] Sync weapon drawn state
|
||||
- [ ] Sync basic fire event
|
||||
- [ ] Sync simple health state
|
||||
- [ ] Add debug UI or console commands
|
||||
|
||||
## Phase 6: Controlled Multiplayer Test
|
||||
|
||||
- [ ] Two clients in same test cell
|
||||
- [ ] Both players visible
|
||||
- [ ] Movement sync working
|
||||
- [ ] Basic animation sync working
|
||||
- [ ] Basic LAN test documented
|
||||
@@ -9,6 +9,36 @@ failed experiments, successful tests, and next steps.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-10 - Repository cleanup
|
||||
|
||||
### Summary
|
||||
Reorganized documentation and reference assets to reduce root clutter without changing plugin, server, or game code.
|
||||
|
||||
### Files Changed
|
||||
- `docs/README.md` (new documentation index)
|
||||
- `docs/ROADMAP.md` (moved from repo root)
|
||||
- `docs/protocol/` (moved from repo root `protocol/`)
|
||||
- `docs/reference/Interface/` (moved from repo root `Interface/`)
|
||||
- `tools/scripts/setup-ultralight-link.ps1` (moved from `scripts/`)
|
||||
- Removed stale phase/milestone docs, deprecated FFDec main-menu guides, and unused `steam_interfaces.txt`
|
||||
- Updated `README.md`, `build-prismaui.bat`, `tools/README.md`, `CONTRIBUTING.md`, and related doc links
|
||||
|
||||
### Details
|
||||
- Root now holds entry-point batch scripts and core project files only; detailed protocol specs and roadmap live under `docs/`.
|
||||
- Decompiled vanilla interface files remain available as read-only reference material under `docs/reference/Interface/`.
|
||||
- Historical phase completion reports and deprecated SWF patching guides were removed after confirming they were superseded by current docs.
|
||||
|
||||
### Testing
|
||||
- Not run (documentation and file moves only).
|
||||
|
||||
### Known Issues
|
||||
- None currently known.
|
||||
|
||||
### Next Steps
|
||||
- Continue feature work against the cleaned documentation layout.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-09 - Consumer Server CLI v1
|
||||
|
||||
### Summary
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Project Overview
|
||||
|
||||
Commonwealth Online is a prototype multiplayer framework experiment for Fallout 4.
|
||||
|
||||
The first target is not full co-op. The first target is a controlled test where two clients can connect to a local server and see each other as synced actors in a custom test cell.
|
||||
|
||||
## Core Idea
|
||||
|
||||
Fallout 4 remains the client application. A native F4SE plugin reads local player state and communicates with an external server.
|
||||
|
||||
The server tracks connected players and broadcasts relevant state to other clients.
|
||||
|
||||
A Creation Kit plugin provides test cells, placeholder actors, scripts, and controlled test content.
|
||||
|
||||
## Basic Flow
|
||||
|
||||
```text
|
||||
Fallout 4 Client A
|
||||
|
|
||||
| local player transform
|
||||
v
|
||||
External Server
|
||||
|
|
||||
| remote player transform
|
||||
v
|
||||
Fallout 4 Client B
|
||||
```
|
||||
|
||||
## Early Limitations
|
||||
|
||||
The first prototype should assume:
|
||||
|
||||
- All players are in the same cell
|
||||
- No quest synchronization
|
||||
- No settlement synchronization
|
||||
- No VATS
|
||||
- No dialogue sync
|
||||
- No full inventory sync
|
||||
- No support for large modlists
|
||||
@@ -1,232 +0,0 @@
|
||||
# Phase 1-3 Completion Report: TiltedEvolution Alignment
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Successfully completed Phase 1-3 of the Commonwealth Online → TiltedEvolution alignment initiative. Established comprehensive architectural foundation, protocol extensions, and animation descriptor infrastructure to enable working proxy actor animations.
|
||||
|
||||
**Status:** Ready to proceed with Phase 4 (Dynamic Spawn).
|
||||
|
||||
---
|
||||
|
||||
## Phases Completed
|
||||
|
||||
### Phase 1: Blueprint & Research ✅
|
||||
|
||||
**Deliverables:**
|
||||
- `docs/f4-animation-descriptor.md` — FO4 animation graph variable mapping + comparison with Skyrim SE
|
||||
- `docs/animation-architecture-alignment.md` — Component-by-component alignment between TiltedEvolution and F4T with data flow diagrams
|
||||
- `docs/animation-sync-analysis.md` — Detailed analysis of why current approach fails and how TiltedEvolution solves it
|
||||
|
||||
**Key Findings:**
|
||||
- TiltedEvolution's architecture is compatible with FO4
|
||||
- Character controller velocity injection already tested and works
|
||||
- Main blockers: no descriptor tables, no action replay, no actor state flags
|
||||
- Variable indices need determination via debug extraction (template provided)
|
||||
|
||||
**Artifacts:**
|
||||
- 2 comprehensive alignment documents
|
||||
- 3 architecture diagrams (mermaid)
|
||||
- Comparison tables (TiltedEvolution vs F4T)
|
||||
- Risk mitigation strategy documented
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: State & Protocol ✅
|
||||
|
||||
**Deliverables:**
|
||||
- Extended `RemotePlayerState` struct with:
|
||||
- `uint32_t actorStateFlags1/2` — Actor state flags (combat, sneaking, animation state)
|
||||
- `std::vector<RemoteActionEvent> actionEvents` — Action queue for discrete action replay
|
||||
- New `RemoteActionEvent` struct with animation variable snapshots
|
||||
- Updated `F4TNetworking.cpp` to parse new optional JSON fields
|
||||
- Updated `server/fake_client.py` to handle and display new fields
|
||||
- Created `docs/protocol-phase2-extensions.md` — Full protocol specification
|
||||
|
||||
**Features:**
|
||||
- ✅ Fully backward compatible (missing fields = sensible defaults)
|
||||
- ✅ JSON protocol extended (new optional fields)
|
||||
- ✅ Server relay unchanged (transparent pass-through)
|
||||
- ✅ Test client displays actor state flags in hex
|
||||
- ✅ Thread-safe state storage (existing mutex preserved)
|
||||
|
||||
**Protocol Changes:**
|
||||
```json
|
||||
{
|
||||
"type": "transform",
|
||||
"playerId": 1,
|
||||
// ... existing fields ...
|
||||
|
||||
// Phase 2 NEW FIELDS (optional):
|
||||
"actorStateFlags1": 0x00000042,
|
||||
"actorStateFlags2": 0x00000001,
|
||||
"actionEvents": [
|
||||
{
|
||||
"type": 0,
|
||||
"eventName": "ActorMovementStart",
|
||||
"animationVariablesFloat": [...],
|
||||
"animationVariablesBool": [...],
|
||||
"animationVariablesInt": [...]
|
||||
// ... more fields ...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Animation Descriptors ✅
|
||||
|
||||
**Deliverables:**
|
||||
- `plugin/include/F4AnimationDescriptor.h` — Class definition with full API
|
||||
- `plugin/src/F4AnimationDescriptor.cpp` — Implementation with variable tables
|
||||
|
||||
**Features:**
|
||||
- Singleton pattern with one-time initialization
|
||||
- Pre-computed humanoid graph variable tables:
|
||||
- **9 float variables:** Speed, direction, speedDamped, speedSampled, pitchGunAim, weaponAdjust, velocityZ, speedWalk, speedRun
|
||||
- **6 bool variables:** isSprinting, isSneaking, isMoving, bMotionDriven, bInMoveState, bSprintOK
|
||||
- **3 int variables:** iLeftHandType, iRightHandEquipped, iIsInSneak
|
||||
- Reverse lookup maps (name → index) for diagnostics
|
||||
- Public API:
|
||||
- Variable name/index queries
|
||||
- Bulk read/write methods: `SaveAnimationVariablesFromCache()`, `LoadAnimationVariablesToCache()`
|
||||
- Thread-safe (one-time init per process)
|
||||
- Graceful fallback to string-based API if indexed access unavailable
|
||||
|
||||
**Performance Improvement:**
|
||||
- **Old (per-frame per-call):** O(n) string search × number of variables
|
||||
- **New (bulk indexed):** O(1) array access + single graph manager lock
|
||||
|
||||
---
|
||||
|
||||
## Code Changes Summary
|
||||
|
||||
### Files Modified
|
||||
- `docs/dev-log.md` — Added Phase 1-3 entries with technical details
|
||||
- `plugin/include/F4TRemotePlayerState.h` — Extended struct with actor state + action queue
|
||||
- `plugin/src/F4TNetworking.cpp` — Parse new JSON fields with backward compatibility
|
||||
- `server/fake_client.py` — Display actor state flags
|
||||
|
||||
### Files Created
|
||||
- `docs/f4-animation-descriptor.md` — FO4 variable mapping template
|
||||
- `docs/animation-architecture-alignment.md` — Component alignment guide
|
||||
- `docs/animation-sync-analysis.md` — Analysis of animation failures + solutions
|
||||
- `docs/protocol-phase2-extensions.md` — Protocol specification for new fields
|
||||
- `plugin/include/F4AnimationDescriptor.h` — Descriptor class definition
|
||||
- `plugin/src/F4AnimationDescriptor.cpp` — Descriptor implementation
|
||||
|
||||
### Net Result
|
||||
- **13 files** created/modified
|
||||
- **~1,200 lines** of C++ code (descriptors + headers)
|
||||
- **~1,500 lines** of documentation
|
||||
- **~300 lines** of Python (protocol extensions + test client)
|
||||
|
||||
---
|
||||
|
||||
## Architectural Changes
|
||||
|
||||
### Before Phase 1-3 (Current State)
|
||||
```
|
||||
Local Player State → JSON → Network → Remote State
|
||||
↓
|
||||
Proxy Actor (SetPosition only)
|
||||
No velocity → No animations ❌
|
||||
```
|
||||
|
||||
### After Phase 1-3 (Prepared for Phase 4+)
|
||||
```
|
||||
Local Player State + Actor State Flags + Actions → JSON (extended) → Network → Remote State (extended)
|
||||
↓
|
||||
Proxy Actor (Dynamic spawn)
|
||||
+ Velocity Injection
|
||||
+ Descriptor-based sync
|
||||
+ Action Replay
|
||||
= Animations Work ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps: Phase 4 (Dynamic Spawn)
|
||||
|
||||
### Phase 4.1: Implement Dynamic Proxy Spawning
|
||||
- Replace pre-placed pool with dynamic `PlaceAtMe()` spawning
|
||||
- Implement slot-based reuse (keep actors alive, reuse by playerId)
|
||||
- Start with single proxy, add safeguards incrementally
|
||||
- Keep pre-placed fallback active during testing
|
||||
|
||||
### Phase 4.2: Character Controller Velocity (Verify)
|
||||
- Test `Actor::Move(0.016f, {0,0,0}, false)` on dynamic proxies
|
||||
- Verify `SetLinearVelocityImpl()` works as expected
|
||||
- Set velocity before animation sync for proper precedence
|
||||
|
||||
### Phase 4.3: Actor State Application
|
||||
- Apply `actor→actorState.flags1/2` from remote state
|
||||
- Signal animation graph FSM about combat, sneaking, etc.
|
||||
- This enables state-driven graph transitions
|
||||
|
||||
---
|
||||
|
||||
## Testing Criteria (Phase 6)
|
||||
|
||||
- [ ] Plugin compiles with all new code
|
||||
- [ ] Descriptor singleton initializes correctly (9 floats, 6 bools, 3 ints)
|
||||
- [ ] Dynamic proxies spawn and move smoothly
|
||||
- [ ] Character controller velocity set each frame
|
||||
- [ ] Locomotion animations play (idle → walk → run → sprint)
|
||||
- [ ] Actor state flags affect visuals (sneak = crouch)
|
||||
- [ ] Descriptor bulk writes faster than string-based writes
|
||||
- [ ] No crashes with 4+ proxies simultaneously
|
||||
- [ ] Architecture aligns with TiltedEvolution patterns
|
||||
|
||||
---
|
||||
|
||||
## Decision Log
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Use TiltedEvolution as blueprint | Proven in production (Skyrim SE), matches Creation Engine fundamentals | Full architectural refactor, 28-40 hours total |
|
||||
| Singleton descriptor + tables | Efficient (init once, use always), extensible for creature graphs | Slightly more code, significant performance gain |
|
||||
| Backward-compatible protocol | Avoid breaking existing clients/servers | New fields are optional, defaults sensible |
|
||||
| Dynamic spawn instead of pool | Enables animations (proxy needs AI context), matches TiltedEvolution model | Requires testing for crash safety |
|
||||
|
||||
---
|
||||
|
||||
## Known Unknowns (Phase 1-3 Research Gaps)
|
||||
|
||||
1. **Exact FO4 animation variable indices** — Template created, values need debug extraction
|
||||
2. **Whether `ActorMediator::ForceAction()` works the same in FO4** — Assume yes, test in Phase 5
|
||||
3. **Whether dynamic proxies can have proper AI without pathfinding issues** — Assume yes, test in Phase 4
|
||||
4. **Performance of descriptor bulk writes vs string-based** — Expect faster, measure in Phase 6
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics (Achieved)
|
||||
|
||||
- ✅ Phase 1-3 all completed on schedule
|
||||
- ✅ Architecture documented and aligned with TiltedEvolution
|
||||
- ✅ Protocol extended without breaking compatibility
|
||||
- ✅ Descriptor infrastructure ready for action replay
|
||||
- ✅ No crashes or compilation errors
|
||||
- ✅ Clear integration points for Phase 4-5
|
||||
- ✅ Risk mitigation strategy in place
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Continue to Phase 4 immediately** — Architecture is solid, no blockers identified
|
||||
2. **Start with single dynamic proxy during Phase 4** — Test spawn/movement/velocity before scaling
|
||||
3. **Prioritize animation debugging logs** — Will be essential for Phase 6 testing
|
||||
4. **Keep pre-placed fallback active** — Safety net if dynamic spawn has issues
|
||||
5. **Coordinate Phase 2.3 action capture after Phase 5 animation sync works** — Get locomotion working first
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Phases 1-3 successfully establish the foundation for TiltedEvolution-style animation synchronization in F4T. All architectural decisions are well-documented, the protocol is extensible, and descriptor infrastructure is ready. The path to working proxy actor animations is clear. Phase 4 (Dynamic Spawn) is the critical next step that will unlock character controller velocity injection and enable proper animation system integration.
|
||||
|
||||
**Estimated remaining work:** 20-30 hours (Phases 4-7).
|
||||
**Confidence level:** High (architecture proven, integration points clear).
|
||||
**Risk level:** Low-Medium (dynamic spawn needs safety testing, animation variables need verification).
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
# Phase 2 Interpolation Debugging Guide
|
||||
|
||||
## Problem: "Still Jerky, Not Smooth Movement"
|
||||
|
||||
This guide will help diagnose why interpolation isn't producing smooth movement.
|
||||
|
||||
---
|
||||
|
||||
## Quick Diagnostics
|
||||
|
||||
### 1. **Check If Interpolation is Even Being Called**
|
||||
|
||||
**Log signature to find:**
|
||||
```
|
||||
[Local Player ID: X] Phase 2 interpolation: remote player Y, actor=ZZZZZZZZ, smooth blending active.
|
||||
```
|
||||
|
||||
**If this log is NOT appearing:**
|
||||
- Interpolation is not being triggered
|
||||
- Check: Is `distanceDrift > 0.01F`? (Remote player must be moving)
|
||||
- Check: Is `a_shouldSnap == false`? (Not a teleport/snap movement)
|
||||
- Check: Does `a_optSlot` exist? (Must be a runtime proxy, not fallback)
|
||||
|
||||
### 2. **Verify Update Rate is Sufficient**
|
||||
|
||||
**What's needed for smooth interpolation:**
|
||||
- Updates must arrive **frequently enough** to queue waypoints
|
||||
- Too infrequent updates = large gaps between waypoints = large lerps = still jerky
|
||||
|
||||
**Check fake client update rate:**
|
||||
```bash
|
||||
# In fake_client.py, look for the movement loop
|
||||
# It should be sending updates at least 10-30 Hz (10-30 per second)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Causes of Jerkiness
|
||||
|
||||
### **Root Cause 1: Not Enough Waypoints**
|
||||
|
||||
**Symptom:** Movement is still teleport-like, not smooth
|
||||
**Reason:** Interpolation needs AT LEAST 2 waypoints
|
||||
|
||||
**Diagram:**
|
||||
```
|
||||
Bad (jerky):
|
||||
Frame 1: ╔══ Position A
|
||||
Frame 2: ║ ╔══ Position B (teleport)
|
||||
Frame 3: ║ ║ ╔══ Position C (teleport)
|
||||
|
||||
Good (smooth):
|
||||
Frame 1: ╔══════════════════╗ WayPoint 1 (A) queued
|
||||
Frame 2: ║ Interp between ║ WayPoint 2 (B) queued, lerp A→B at 0.5
|
||||
║ A and B at 25% ║
|
||||
Frame 3: ║ Interp between ║ WayPoint 3 (C) queued, lerp B→C at 0.5
|
||||
║ B and C at 50% ║
|
||||
```
|
||||
|
||||
**Diagnosis:**
|
||||
- Add extra logging to `ApplyRuntimeProxyTransform()`:
|
||||
```cpp
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Waypoint queue size: {}, distanceDrift: {:.2f}, interpolating: {}",
|
||||
a_optSlot->interpolationComponent.TimePoints.size(),
|
||||
distanceDrift,
|
||||
interpResult.IsInterpolating));
|
||||
```
|
||||
|
||||
### **Root Cause 2: Update Arrival Rate Too Slow**
|
||||
|
||||
**Symptom:** Movement jumps happen, then freeze, then jump again
|
||||
**Reason:** Remote player updates arriving slower than game frame rate
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Game FPS: 60 (16ms per frame)
|
||||
Update Rate: 10 Hz (100ms per update)
|
||||
|
||||
Result:
|
||||
Frame 1-6: (100ms of frames) Interpolate slowly
|
||||
Frame 7: NEW UPDATE arrives, big jump
|
||||
Frame 8-13: Interpolate again
|
||||
```
|
||||
|
||||
**Fix:** Increase fake client update frequency
|
||||
```python
|
||||
# In fake_client.py, change sleep duration:
|
||||
time.sleep(0.033) # 30 Hz instead of slower
|
||||
```
|
||||
|
||||
### **Root Cause 3: Alpha Calculation Wrong**
|
||||
|
||||
**Symptom:** Movement speed changes unexpectedly, or stalls
|
||||
**Reason:** Frame counter might not be incrementing correctly
|
||||
|
||||
**Check the alpha calculation:**
|
||||
```cpp
|
||||
// In InterpolationSystem::Update()
|
||||
float alpha = static_cast<float>(tickElapsed) / static_cast<float>(tickDelta);
|
||||
|
||||
// If tickDelta is 0 or 1, alpha calculation breaks
|
||||
// tickDelta should be the FRAME DIFFERENCE between waypoints (e.g., 30-60)
|
||||
```
|
||||
|
||||
### **Root Cause 4: Waypoint Queue Pruning Too Aggressive**
|
||||
|
||||
**Symptom:** Interpolation logs appear but movement still jerky
|
||||
**Reason:** Queue size limited to 3, old waypoints popped too early
|
||||
|
||||
**Current code:**
|
||||
```cpp
|
||||
while (a_optSlot->interpolationComponent.TimePoints.size() > 3) {
|
||||
a_optSlot->interpolationComponent.TimePoints.pop_front();
|
||||
}
|
||||
```
|
||||
|
||||
**Try increasing to 5-10 waypoints** for more buffer.
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### **Step 1: Add Detailed Logging**
|
||||
|
||||
Edit `F4TProxyActorController.cpp` in `ApplyRuntimeProxyTransform()`:
|
||||
|
||||
```cpp
|
||||
if (a_optSlot && !a_shouldSnap && distanceDrift > 0.01F) {
|
||||
static uint64_t frameCounter = 0;
|
||||
frameCounter++;
|
||||
|
||||
// ADD THIS DEBUG LOG:
|
||||
if (frameCounter % 10 == 0) { // Log every 10th frame to avoid spam
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Phase2-DEBUG: frame={}, distanceDrift={:.2f}, queueSize={}, interpolating={}",
|
||||
frameCounter,
|
||||
distanceDrift,
|
||||
a_optSlot->interpolationComponent.TimePoints.size(),
|
||||
/* will know after Update */
|
||||
false));
|
||||
}
|
||||
|
||||
// ... rest of code
|
||||
}
|
||||
```
|
||||
|
||||
### **Step 2: Monitor Log Output**
|
||||
|
||||
After recompiling, run fake client and watch log:
|
||||
|
||||
```bash
|
||||
# Terminal: Watch logs in real-time
|
||||
Get-Content "c:\Users\User\Documents\My Games\Fallout4\F4SE\CommonwealthOnline.log" -Wait -Tail 50 | Select-String "Phase2-DEBUG"
|
||||
```
|
||||
|
||||
**Expected pattern (good interpolation):**
|
||||
```
|
||||
Phase2-DEBUG: frame=10, distanceDrift=345.62, queueSize=2, interpolating=true
|
||||
Phase2-DEBUG: frame=20, distanceDrift=298.45, queueSize=2, interpolating=true
|
||||
Phase2-DEBUG: frame=30, distanceDrift=251.23, queueSize=2, interpolating=true ← Smooth decrease
|
||||
```
|
||||
|
||||
**Bad pattern (not working):**
|
||||
```
|
||||
Phase2-DEBUG: frame=10, distanceDrift=345.62, queueSize=1, interpolating=false
|
||||
Phase2-DEBUG: frame=20, distanceDrift=1000.00, queueSize=1, interpolating=false ← Jumps!
|
||||
Phase2-DEBUG: frame=30, distanceDrift=500.00, queueSize=1, interpolating=false
|
||||
```
|
||||
|
||||
### **Step 3: Check Fake Client Update Rate**
|
||||
|
||||
Add logging to `fake_client.py`:
|
||||
|
||||
```python
|
||||
last_update_time = time.time()
|
||||
while True:
|
||||
# ... send update ...
|
||||
now = time.time()
|
||||
update_interval = now - last_update_time
|
||||
if update_interval > 0.05: # Log if >50ms between updates
|
||||
print(f"WARNING: Long update interval: {update_interval*1000:.1f}ms")
|
||||
last_update_time = now
|
||||
time.sleep(0.033) # ~30 Hz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comprehensive Fix Checklist
|
||||
|
||||
- [ ] **Verify waypoints are queuing**: Add frame counter debug log
|
||||
- [ ] **Confirm queue size > 1**: Should see "queueSize=2" in logs during movement
|
||||
- [ ] **Check update frequency**: Fake client sending at 20-30 Hz minimum
|
||||
- [ ] **Verify distanceDrift threshold**: Is `distanceDrift > 0.01F`?
|
||||
- [ ] **Test with moving fake client**: Stationary player won't trigger interpolation
|
||||
- [ ] **Increase waypoint buffer**: Try `queueSize > 5` instead of 3
|
||||
- [ ] **Monitor alpha calculation**: Should go 0.0 → 1.0 as player moves between waypoints
|
||||
|
||||
---
|
||||
|
||||
## Quick Fix: Increase Update Frequency
|
||||
|
||||
**Easiest fix:** Make fake client send updates more frequently
|
||||
|
||||
Edit `fake_client.py`:
|
||||
```python
|
||||
# Find the main loop:
|
||||
while True:
|
||||
# ... code ...
|
||||
time.sleep(0.033) # Change from 0.1 or higher to 0.033 (30 Hz)
|
||||
```
|
||||
|
||||
Or increase queue size in `ApplyRuntimeProxyTransform()`:
|
||||
```cpp
|
||||
// Change from:
|
||||
while (a_optSlot->interpolationComponent.TimePoints.size() > 3) {
|
||||
// To:
|
||||
while (a_optSlot->interpolationComponent.TimePoints.size() > 10) {
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## If Still Jerky After All This
|
||||
|
||||
The issue might be **architectural**: Maybe F4 doesn't support sub-frame smooth movement, and we need to adjust strategy.
|
||||
|
||||
**Next options:**
|
||||
1. **Reduce frame times via velocity injection**: Make proxies move via physics instead of SetPosition
|
||||
2. **Use different lerp approach**: Accumulate delta over frames instead of tick-based
|
||||
3. **Accept some jerkiness**: Design animations to hide it (blending animations during transitions)
|
||||
|
||||
---
|
||||
|
||||
## Need More Help?
|
||||
|
||||
Post the following in your logs:
|
||||
1. Output of "Phase2-DEBUG" log lines (10 consecutive)
|
||||
2. Fake client update interval timing
|
||||
3. FPS you're getting in-game
|
||||
4. Distance between waypoints in the logs
|
||||
|
||||
This will help diagnose the exact cause.
|
||||
@@ -1,365 +0,0 @@
|
||||
# Phase 2: Interpolation System Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Phase 2 tests the waypoint-based smooth movement interpolation system. This replaces direct position application with time-based lerp between waypoints, resulting in smoother remote player movement.
|
||||
|
||||
**Prerequisites:**
|
||||
- F4T plugin built with interpolation system (just completed ✓)
|
||||
- Fallout 4 running with F4SE
|
||||
- Network relay server running (`python server.py`)
|
||||
- Fake client ready (`python server/fake_client.py`)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Review
|
||||
|
||||
Before testing, understand what we're testing:
|
||||
|
||||
### Current System (Pre-Phase 2)
|
||||
```
|
||||
Remote Player Update
|
||||
↓
|
||||
GetRuntimeProxyVisibleTargetPosition()
|
||||
↓
|
||||
targetPosition = direct network position
|
||||
↓
|
||||
SetPosition(targetPosition) ← IMMEDIATE, no lerp
|
||||
↓
|
||||
Result: Jittery/choppy movement on low update rates
|
||||
```
|
||||
|
||||
### New System (Phase 2)
|
||||
```
|
||||
Remote Player Update
|
||||
↓
|
||||
GetRuntimeProxyVisibleTargetPosition()
|
||||
↓
|
||||
AddWaypoint(targetPosition, angleZ, currentTick) → InterpolationComponent
|
||||
↓
|
||||
InterpolationSystem::Update(currentTick)
|
||||
↓
|
||||
Lerp between waypoints if tick is between them
|
||||
↓
|
||||
SetPosition(interpolatedPosition) ← SMOOTH, tick-based
|
||||
↓
|
||||
Result: Smooth movement even on low update rates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 2.1: Interpolation System Unit Test
|
||||
|
||||
### Goal
|
||||
Verify interpolation math works correctly before testing in-game.
|
||||
|
||||
### Test: Manual Interpolation Check
|
||||
|
||||
Create a simple test script (`test_interpolation.py`):
|
||||
|
||||
```python
|
||||
# Pseudo-code showing what Phase 2 does
|
||||
# Position at tick 100: (1000, 2000, 500)
|
||||
# Position at tick 200: (1100, 2050, 550)
|
||||
|
||||
# At tick 150 (halfway):
|
||||
# alpha = (150 - 100) / (200 - 100) = 0.5
|
||||
# interpolated_x = 1000 + (1100 - 1000) * 0.5 = 1050
|
||||
# interpolated_y = 2000 + (2050 - 2000) * 0.5 = 2025
|
||||
# interpolated_z = 500 + (550 - 500) * 0.5 = 525
|
||||
|
||||
# Result: (1050, 2025, 525) ← smooth midpoint
|
||||
```
|
||||
|
||||
**Expected behavior:** Movement is linear blend between waypoints.
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 2.2: In-Game Setup
|
||||
|
||||
### Setup Step 1: Start Relay Server
|
||||
|
||||
```bash
|
||||
cd f:\Repos\Commonwealth-Online\server
|
||||
python server.py
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Server listening on 127.0.0.1:7777
|
||||
Waiting for connections...
|
||||
```
|
||||
|
||||
### Setup Step 2: Start Fake Client
|
||||
|
||||
In a new terminal:
|
||||
|
||||
```bash
|
||||
cd f:\Repos\Commonwealth-Online\server
|
||||
python fake_client.py
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Connecting to server...
|
||||
Connected successfully
|
||||
Fake player registered with ID: <player_id>
|
||||
```
|
||||
|
||||
### Setup Step 3: Launch Fallout 4
|
||||
|
||||
1. Start Fallout 4 with F4SE
|
||||
2. Load into a game with open space (avoid dungeons)
|
||||
3. Check console for F4T startup messages
|
||||
|
||||
Expected log output:
|
||||
```
|
||||
[Local Player ID: X] F4T Plugin initialized
|
||||
[Local Player ID: X] Proxy controller ready
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 2.3: Basic Interpolation Test
|
||||
|
||||
### Test: Remote Player Walks in Circle
|
||||
|
||||
This verifies waypoint buffering and interpolation.
|
||||
|
||||
**Setup:**
|
||||
1. Have fake client move in a small circle around you
|
||||
2. Watch the proxy actor move
|
||||
|
||||
**Expected Behavior (PRE-Phase 2):**
|
||||
- Proxy position updates in discrete jumps
|
||||
- Movement appears jittery/choppy
|
||||
- Proxy teleports between waypoints
|
||||
|
||||
**Expected Behavior (POST-Phase 2):**
|
||||
- Proxy moves smoothly
|
||||
- Movement is continuous, not jerky
|
||||
- Smooth arc instead of sharp jumps
|
||||
|
||||
**How to Verify:**
|
||||
1. Run fake client for 30 seconds with slow movement
|
||||
2. Observe proxy actor
|
||||
3. Compare smoothness to pre-Phase 2 behavior
|
||||
|
||||
**Log Indicators:**
|
||||
```
|
||||
[Local Player ID: 0] First smoothed runtime proxy movement for remote player X: actor=FORMID, alpha=0.15.
|
||||
```
|
||||
|
||||
This log should appear once per remote player.
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 2.4: Movement Speed Variations
|
||||
|
||||
### Test: Fast vs Slow Movement
|
||||
|
||||
**Objective:** Verify interpolation works across movement speeds.
|
||||
|
||||
**Test Case 1: Slow Movement**
|
||||
- Fake client walks slowly (1-2 units/sec)
|
||||
- Expected: Smooth continuous movement
|
||||
- Should NOT have position snapping
|
||||
|
||||
**Test Case 2: Fast Movement**
|
||||
- Fake client sprints (100+ units/sec)
|
||||
- Expected: Fast smooth movement
|
||||
- No lag or skipping
|
||||
|
||||
**Test Case 3: Teleport**
|
||||
- Fake client teleports 500 units away
|
||||
- Expected: Proxy snaps (shouldSnap=true for teleport movement type)
|
||||
- Movement type check happens first, no interpolation
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 2.5: Multiple Remote Players
|
||||
|
||||
### Test: Interpolation with 2-4 Remote Players
|
||||
|
||||
**Setup:**
|
||||
1. Start 2-4 fake clients
|
||||
2. Each sends unique movement patterns
|
||||
3. Observe all proxies
|
||||
|
||||
**Expected Behavior:**
|
||||
- Each proxy interpolates independently
|
||||
- No interaction/interference between proxies
|
||||
- Each has its own InterpolationComponent
|
||||
- Smooth movement for all
|
||||
|
||||
**Performance Check:**
|
||||
1. Monitor FPS with 4 remote players
|
||||
2. Should be similar to Phase 1 (baseline)
|
||||
3. Interpolation adds minimal CPU load (just lerp math)
|
||||
|
||||
**Command to simulate:**
|
||||
```bash
|
||||
# Terminal 1: Start main relay
|
||||
python server.py
|
||||
|
||||
# Terminal 2-5: Start 4 fake clients
|
||||
python fake_client.py
|
||||
python fake_client.py
|
||||
python fake_client.py
|
||||
python fake_client.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 2.6: Waypoint Queue Management
|
||||
|
||||
### Test: Verify Waypoint Cleanup
|
||||
|
||||
**Objective:** Ensure old waypoints are cleaned up, not accumulated.
|
||||
|
||||
**How to Check:**
|
||||
1. Run fake client for 2 minutes straight
|
||||
2. Monitor memory usage
|
||||
3. Should be stable (not growing)
|
||||
|
||||
**What's Happening:**
|
||||
```
|
||||
Tick 100: Add waypoint
|
||||
Tick 101-150: Lerp between waypoint 0 and 1
|
||||
Tick 151: Pop waypoint 0 (cleanup!)
|
||||
Tick 151: Add new waypoint
|
||||
Tick 152-200: Lerp between waypoint 0 and 1 (still 2 max)
|
||||
```
|
||||
|
||||
The InterpolationSystem::Update() automatically pops old waypoints when they're consumed.
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 2.7: Rotation Interpolation
|
||||
|
||||
### Test: Verify Smooth Rotation
|
||||
|
||||
**Objective:** Test angle lerp with wrap-around handling.
|
||||
|
||||
**Test Case 1: Normal Rotation**
|
||||
- Remote player rotates from 0° to 90°
|
||||
- Expected: Smooth rotation from 0° → 45° → 90°
|
||||
|
||||
**Test Case 2: Wrap-Around**
|
||||
- Remote player rotates from 350° to 10° (crossing 0°)
|
||||
- Expected: Shortest path rotation (350° → 0° → 10°), not 350° → 180° → 10°
|
||||
- Our `LerpRotation()` handles this
|
||||
|
||||
**Visual Check:**
|
||||
- Watch proxy heading as it turns
|
||||
- Should be smooth, not jerky
|
||||
- Should take shortest angular path
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 2.8: Integration with Animation Sync
|
||||
|
||||
### Test: Interpolation + Descriptor-Based Animation
|
||||
|
||||
**Setup:**
|
||||
1. Ensure Phase 1 (descriptor-based animation) is working
|
||||
2. Run Phase 2 interpolation in parallel
|
||||
3. Fake client walks around you
|
||||
|
||||
**Expected Behavior:**
|
||||
- Remote player position interpolates smoothly
|
||||
- Remote player animation (Speed, isSprinting) syncs correctly
|
||||
- No conflicts between systems
|
||||
|
||||
**Verification:**
|
||||
```
|
||||
[Local Player ID: 0] Runtime proxy animation sync initial for remote player X:
|
||||
moving=true, sprinting=false, graphSpeed=55.0, direction=0.123
|
||||
```
|
||||
|
||||
This should still appear alongside interpolation.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] **Unit Test**: Lerp math is correct (waypoint positions smooth)
|
||||
- [ ] **Single Player**: Fake client walks, proxy moves smoothly
|
||||
- [ ] **Slow Movement**: Proxy glides smoothly at 1-2 units/sec
|
||||
- [ ] **Fast Movement**: Proxy glides smoothly at 100+ units/sec
|
||||
- [ ] **Multiple Players**: 2-4 proxies move smoothly independently
|
||||
- [ ] **Memory**: Stable after 2+ minutes (no memory growth)
|
||||
- [ ] **Rotation**: Proxy heading rotates smoothly
|
||||
- [ ] **Wrap-Around**: Rotation handles 0°↔360° correctly
|
||||
- [ ] **Integration**: Works with Phase 1 animation sync
|
||||
- [ ] **Snapping**: Teleports still snap (movement type check first)
|
||||
|
||||
---
|
||||
|
||||
## Debugging: Common Issues & Fixes
|
||||
|
||||
### Issue: Proxy Still Jerky After Phase 2
|
||||
|
||||
**Cause:** Interpolation not being called
|
||||
**Fix:**
|
||||
1. Check that `AddWaypoint()` is being called
|
||||
2. Verify `Update()` is called before `SetPosition()`
|
||||
3. Check that `shouldSnap` isn't overriding interpolation
|
||||
|
||||
### Issue: Proxy Moves Too Slowly
|
||||
|
||||
**Cause:** Alpha blend factor too small
|
||||
**Fix:**
|
||||
1. Increase waypoint update rate (more frequent updates = smoother)
|
||||
2. Check tick calculation (verify currentTick is advancing)
|
||||
|
||||
### Issue: Memory Grows Over Time
|
||||
|
||||
**Cause:** Waypoints not being popped
|
||||
**Fix:**
|
||||
1. Verify `Update()` is cleaning up old waypoints
|
||||
2. Check that TimePoints.pop_front() is being called
|
||||
|
||||
### Issue: Rotation Wraps Incorrectly
|
||||
|
||||
**Cause:** LerpRotation() not handling 0°/360° boundary
|
||||
**Fix:**
|
||||
- This is handled in our implementation
|
||||
- If still wrong, check angle units (radians vs degrees)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps After Testing
|
||||
|
||||
1. **If interpolation works:** Move to Phase 3 or other features
|
||||
2. **If issues found:** Document in dev-log.md and fix
|
||||
3. **Performance baseline:** Record FPS with 1, 2, 4 remote players
|
||||
|
||||
---
|
||||
|
||||
## Quick Start Commands
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start relay server
|
||||
cd f:\Repos\Commonwealth-Online\server
|
||||
python server.py
|
||||
|
||||
# Terminal 2: Start fake client (single remote player)
|
||||
cd f:\Repos\Commonwealth-Online\server
|
||||
python fake_client.py
|
||||
|
||||
# In-game: Launch F4T and observe proxy movement
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected Logs for Phase 2
|
||||
|
||||
When interpolation is active, look for:
|
||||
|
||||
```
|
||||
[Local Player ID: 0] First smoothed runtime proxy movement for remote player 1: actor=0x14003ED8, alpha=0.15.
|
||||
[Local Player ID: 0] Moved runtime proxy actor 0x14003ED8 from remote player 1 state for the first time: X=1234.56, Y=2345.67, Z=-100.00, AngleZ=1.570, flags1=00000000, flags2=00000000.
|
||||
```
|
||||
|
||||
These confirm the proxy is spawned and moving smoothly.
|
||||
@@ -1,177 +0,0 @@
|
||||
# Phase 6 Quick Start Checklist
|
||||
|
||||
## Before You Start
|
||||
|
||||
- [ ] All Phases 1-5 code committed to git
|
||||
- [ ] F4T plugin builds without errors
|
||||
- [ ] F4SE installed in Fallout 4
|
||||
- [ ] Server relay running (`python server/server.py`)
|
||||
- [ ] Ready to launch Fallout 4 with F4SE
|
||||
|
||||
## Test Execution Order
|
||||
|
||||
### Critical Path (30 minutes)
|
||||
|
||||
1. **[5 min] Test 1: Proxy Spawning**
|
||||
- Start game, load F4TTestCell01
|
||||
- Connect fake client
|
||||
- [ ] Proxy appears near player
|
||||
- [ ] No crashes
|
||||
- **Result:** PASS / FAIL
|
||||
|
||||
2. **[5 min] Test 2: Movement**
|
||||
- Move fake client position
|
||||
- [ ] Proxy smoothly moves to new position
|
||||
- [ ] No jittering/snapping
|
||||
- **Result:** PASS / FAIL
|
||||
|
||||
3. **[10 min] TEST 3 - CRITICAL: Animations**
|
||||
- Fake client: stand still → should see idle
|
||||
- Fake client: walk speed → should see walk animation
|
||||
- Fake client: run speed → should see run animation
|
||||
- Fake client: sprint speed → should see sprint animation
|
||||
- [ ] At least idle/walk/run animations visible
|
||||
- **Result:** PASS / FAIL / PARTIAL
|
||||
- **If FAIL:** Debug with console logs
|
||||
|
||||
4. **[5 min] Test 4: Sneak**
|
||||
- Fake client: activate sneak
|
||||
- [ ] Proxy crouches
|
||||
- **Result:** PASS / FAIL / PARTIAL
|
||||
|
||||
5. **[5 min] Test 5: Multiple Proxies**
|
||||
- Connect 3 fake clients (3 proxies total)
|
||||
- All moving simultaneously
|
||||
- [ ] All animate correctly
|
||||
- [ ] No crashes
|
||||
- **Result:** PASS / FAIL
|
||||
|
||||
### If Test 3 FAILS (Animations not playing)
|
||||
|
||||
**Debug Checklist (10 minutes):**
|
||||
1. [ ] Check console logs for velocity being set
|
||||
- Search: `ApplyRuntimeProxyTransform`
|
||||
- Should show character controller access
|
||||
|
||||
2. [ ] Check animation graph manager available
|
||||
- Search: `animation graph manager`
|
||||
- Should NOT show "unavailable" messages
|
||||
|
||||
3. [ ] Check actor state flags
|
||||
- Search: `flags1=`
|
||||
- Should show valid hex values
|
||||
|
||||
4. [ ] Check Speed variable is changing
|
||||
- Search: `graphSpeed=`
|
||||
- Should see values > 0 when moving
|
||||
|
||||
5. [ ] Manual test: Set Speed directly in console
|
||||
- If manual setting works → indices correct, velocity issue
|
||||
- If manual setting doesn't work → index problem
|
||||
|
||||
**If still failing:** Document issue and proceed to Phase 6.3 iteration
|
||||
|
||||
## Key Logging Commands
|
||||
|
||||
### View Real-Time Logs
|
||||
|
||||
```powershell
|
||||
# In separate terminal, tail latest logs
|
||||
Get-Content "Documents\My Games\Fallout4\Logs\*.log" -Wait
|
||||
```
|
||||
|
||||
### Search Logs for Specific Info
|
||||
|
||||
```powershell
|
||||
# Proxy spawn
|
||||
Select-String "SpawnDynamicProxyActor" (Get-ChildItem "Documents\My Games\Fallout4\Logs\*")
|
||||
|
||||
# Animations
|
||||
Select-String "animation sync" (Get-ChildItem "Documents\My Games\Fallout4\Logs\*")
|
||||
|
||||
# Errors
|
||||
Select-String "WARNING\|ERROR" (Get-ChildItem "Documents\My Games\Fallout4\Logs\*")
|
||||
```
|
||||
|
||||
## Expected Console Log Patterns
|
||||
|
||||
### Good (Animations Working)
|
||||
|
||||
```
|
||||
[Local Player ID: 0] SpawnDynamicProxyActor: Spawned proxy actor 0xABCD1234 for remote player 1
|
||||
[Local Player ID: 0] Assigned dynamically spawned proxy 0xABCD1234 to remote player 1
|
||||
[Local Player ID: 0] First smoothed runtime proxy movement for remote player 1: actor=0xABCD1234, alpha=0.15
|
||||
[Local Player ID: 0] ApplyRuntimeProxyTransform: Proxy 0xABCD1234 has AIProcess: true
|
||||
[Local Player ID: 0] Runtime proxy animation sync initial for remote player 1: moving=true, sprinting=false, graphSpeed=55.0
|
||||
```
|
||||
|
||||
### Bad (Animations NOT Working)
|
||||
|
||||
```
|
||||
[Local Player ID: 0] animation graph manager unavailable
|
||||
[Local Player ID: 0] Failed to load animation variables for remote player 1
|
||||
[Local Player ID: 0] Proxy 0xABCD1234 has NO AIProcess
|
||||
```
|
||||
|
||||
## Performance Expectations
|
||||
|
||||
| Metric | Expected | Alert If |
|
||||
|--------|----------|----------|
|
||||
| Proxy spawn time | < 500ms | > 1 second |
|
||||
| Per-proxy update | < 1ms | > 5ms |
|
||||
| Frame rate | 60+ fps | < 30 fps with 1 proxy |
|
||||
| Animation smooth | No stutter | Visible jitter |
|
||||
|
||||
## Results Template
|
||||
|
||||
Copy this after testing:
|
||||
|
||||
```
|
||||
## Phase 6 Test Results - [DATE]
|
||||
|
||||
### Tests
|
||||
- Test 1 (Spawning): PASS / FAIL
|
||||
- Test 2 (Movement): PASS / FAIL
|
||||
- Test 3 (Animations): PASS / FAIL / PARTIAL
|
||||
- Test 4 (Sneak): PASS / FAIL / PARTIAL
|
||||
- Test 5 (Multiple): PASS / FAIL
|
||||
|
||||
### Issues Found
|
||||
1. <Issue>
|
||||
- Severity: Critical / Major / Minor
|
||||
- Fix: <action needed>
|
||||
|
||||
### Next Action
|
||||
- [ ] PASS ALL → Proceed to Phase 7
|
||||
- [ ] PARTIAL → Iterate Phase 6.3 (tune)
|
||||
- [ ] FAIL → Debug and retest
|
||||
```
|
||||
|
||||
## Common Fixes
|
||||
|
||||
### Animations not playing
|
||||
- **Try:** Increase velocity scale (multiply by 1.2x)
|
||||
- **Try:** Check animation variable indices in F4AnimationDescriptor
|
||||
- **Try:** Enable debug logging for Speed variable
|
||||
|
||||
### Proxy doesn't move smoothly
|
||||
- **Try:** Reduce lerp alpha (currently 0.15)
|
||||
- **Try:** Increase update frequency (more network packets)
|
||||
- **Try:** Check for network lag
|
||||
|
||||
### Multiple proxies crash
|
||||
- **Try:** Reduce from 4 to 2 proxies
|
||||
- **Try:** Check for memory leaks in action queue
|
||||
- **Try:** Verify proxy pool cleanup on disconnect
|
||||
|
||||
## Success = All PASS
|
||||
|
||||
When you see:
|
||||
1. ✅ Proxy spawns
|
||||
2. ✅ Proxy moves smoothly
|
||||
3. ✅ Proxy animates (idle/walk/run/sprint)
|
||||
4. ✅ Multiple proxies work
|
||||
5. ✅ No crashes
|
||||
|
||||
**Then Phase 6 is COMPLETE and Phase 7 ready!**
|
||||
|
||||
@@ -1,426 +0,0 @@
|
||||
# Phase 6: Testing & Iteration - Animation Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Phase 6 validates that the TiltedEvolution-aligned animation synchronization works end-to-end in Fallout 4. This document provides testing procedures, expected results, and debugging guidance.
|
||||
|
||||
**Prerequisites:**
|
||||
- F4T plugin built and installed
|
||||
- Fallout 4 running with F4SE
|
||||
- Network relay server running (`python server.py`)
|
||||
- Fake client or second F4T instance ready
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 6.1: In-Game Animation Testing
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Start the relay server:**
|
||||
```bash
|
||||
cd server/
|
||||
python server.py
|
||||
```
|
||||
|
||||
2. **Launch F4T with logging enabled:**
|
||||
- Start Fallout 4 with F4SE
|
||||
- Plugin loads (check console for logs)
|
||||
- Remote player should spawn near you
|
||||
|
||||
3. **If testing with second instance:**
|
||||
```bash
|
||||
# Terminal 1: Relay server
|
||||
python server/server.py
|
||||
|
||||
# Terminal 2: Fake client
|
||||
python server/fake_client.py
|
||||
|
||||
# In-game: Two separate F4T instances, each with F4SE plugin loaded
|
||||
```
|
||||
|
||||
### Test 1: Dynamic Proxy Spawning
|
||||
|
||||
**What to test:**
|
||||
- [ ] Remote player proxy spawns successfully
|
||||
- [ ] Proxy appears near your position (not at world origin)
|
||||
- [ ] Multiple proxies can spawn (up to 4 concurrent)
|
||||
|
||||
**Expected results:**
|
||||
- Proxy NPC visible in-game
|
||||
- Name/ID visible in console logs
|
||||
- No crashes on spawn
|
||||
|
||||
**Debug logs to check:**
|
||||
```
|
||||
[Local Player ID: 0] SpawnDynamicProxyActor: Spawned proxy actor <FORMID> for remote player <ID> with base <BASE_FORMID>.
|
||||
[Local Player ID: 0] Assigned dynamically spawned proxy <FORMID> to remote player <ID>
|
||||
```
|
||||
|
||||
### Test 2: Smooth Movement Synchronization
|
||||
|
||||
**What to test:**
|
||||
- [ ] Remote proxy moves smoothly to player position
|
||||
- [ ] Movement is not jerky/teleporting
|
||||
- [ ] Heading (rotation) updates correctly
|
||||
|
||||
**Expected results:**
|
||||
- Proxy walks/runs toward network position
|
||||
- Smooth interpolation (lerp alpha 0.15)
|
||||
- No position snapping
|
||||
|
||||
**Debug logs to check:**
|
||||
```
|
||||
[Local Player ID: 0] First smoothed runtime proxy movement for remote player <ID>: actor=<FORMID>, alpha=0.15.
|
||||
[Local Player ID: 0] Moved runtime proxy actor <FORMID> from remote player <ID> state for the first time
|
||||
```
|
||||
|
||||
### Test 3: Velocity Injection & Animation Triggering
|
||||
|
||||
**CRITICAL TEST:** Does velocity injection cause animations to play?
|
||||
|
||||
**What to test:**
|
||||
- [ ] Proxy plays idle animation when stationary
|
||||
- [ ] Proxy plays walk animation when moving slowly
|
||||
- [ ] Proxy plays run animation when moving fast
|
||||
- [ ] Proxy plays sprint animation when sprinting
|
||||
|
||||
**Expected results:**
|
||||
```
|
||||
Movement State | Expected Animation
|
||||
---------------------|---------------------
|
||||
Stationary | Idle
|
||||
Speed 0-50 units/s | Walk
|
||||
Speed 50-100 units/s | Run
|
||||
Speed 100+ units/s | Sprint
|
||||
```
|
||||
|
||||
**Debug logs to check:**
|
||||
```
|
||||
[Local Player ID: 0] ApplyRuntimeProxyTransform: Proxy <FORMID> has AIProcess: true
|
||||
[Local Player ID: 0] Runtime proxy animation sync <changed/initial> for remote player <ID>:
|
||||
moving=<bool>, sprinting=<bool>, graphSpeed=<float>
|
||||
```
|
||||
|
||||
**If animations DON'T play:**
|
||||
1. Check velocity is being set (see console logs)
|
||||
2. Verify character controller is accessible
|
||||
3. Check animation graph manager is available
|
||||
4. Look for "Failed to load animation variables" warnings
|
||||
|
||||
### Test 4: Actor State Flags & Sneak Animation
|
||||
|
||||
**What to test:**
|
||||
- [ ] Remote player sneaking → proxy crouches
|
||||
- [ ] Remote player normal → proxy stands
|
||||
- [ ] State transitions are smooth
|
||||
|
||||
**Expected results:**
|
||||
- Proxy visibly crouches when remote player crouches
|
||||
- Proxy crouch pose visible (animation, not just flag)
|
||||
- No lag in state transition
|
||||
|
||||
**Debug logs to check:**
|
||||
```
|
||||
[Local Player ID: 0] Moved runtime proxy actor <FORMID> ... flags1=<FLAGS1>, flags2=<FLAGS2>
|
||||
```
|
||||
|
||||
**If sneak animation doesn't work:**
|
||||
- Sneak might require more than just flags
|
||||
- May need dedicated action event (Phase 2.3 capture)
|
||||
- Document in iteration log
|
||||
|
||||
### Test 5: Multiple Proxy Actors
|
||||
|
||||
**What to test:**
|
||||
- [ ] 2 simultaneous proxies move smoothly
|
||||
- [ ] 3 simultaneous proxies move smoothly
|
||||
- [ ] 4 simultaneous proxies (max) move smoothly
|
||||
- [ ] No crashes with multiple proxies
|
||||
|
||||
**Expected results:**
|
||||
- Each proxy moves independently to its network position
|
||||
- All proxies animate correctly
|
||||
- Performance acceptable (no frame rate drops)
|
||||
|
||||
**Debug logs to check:**
|
||||
```
|
||||
[Local Player ID: 0] Runtime proxy slot created for remote player <ID> with holding index <INDEX>.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 6.2: Performance & Stability Profiling
|
||||
|
||||
### Benchmark: Per-Proxy Update Cost
|
||||
|
||||
**Metric:** Time to update one proxy per frame
|
||||
|
||||
**Measure:**
|
||||
1. Enable logging: `UpdateProxyAnimationStateDebug` calls
|
||||
2. Run single proxy moving
|
||||
3. Monitor console for timing data
|
||||
4. Expected: < 1ms per proxy update
|
||||
|
||||
**Command (in console):**
|
||||
```
|
||||
help F4T # Check if debug timers are available
|
||||
```
|
||||
|
||||
### Benchmark: Descriptor vs String-Based Writes
|
||||
|
||||
**Phase 5.3 improvement verification:**
|
||||
|
||||
**Old way (string-based):**
|
||||
- Multiple `TrySetGraphFloat()` calls
|
||||
- Each call: string allocation + graph lookup
|
||||
|
||||
**New way (descriptor-based):**
|
||||
- Single `LoadAnimationVariablesToCache()` call
|
||||
- Array indexing + one graph manager lock
|
||||
|
||||
**Expected result:** Descriptor approach should be ~3-5x faster for animation variable writes.
|
||||
|
||||
### Stability: Crash Testing
|
||||
|
||||
**What to test:**
|
||||
- [ ] No crash with 1 proxy moving
|
||||
- [ ] No crash with 4 proxies moving
|
||||
- [ ] No crash on proxy disconnect/reconnect
|
||||
- [ ] No crash on actor reload
|
||||
|
||||
**Procedure:**
|
||||
1. Start with 1 proxy moving smoothly
|
||||
2. Add proxies one by one
|
||||
3. Monitor for crashes
|
||||
4. Try disconnecting/reconnecting
|
||||
5. Run for 5-10 minutes per configuration
|
||||
|
||||
**If crashes occur:**
|
||||
- Note exact conditions
|
||||
- Check console for error logs
|
||||
- Review actor handle validity
|
||||
- Check for memory leaks
|
||||
|
||||
### Stability: Lag Simulation
|
||||
|
||||
**What to test:**
|
||||
- [ ] Proxy handles high-frequency position updates (no lag)
|
||||
- [ ] Proxy handles low-frequency updates (lerp smoothing works)
|
||||
- [ ] Proxy handles packet loss gracefully
|
||||
|
||||
**Procedure:**
|
||||
1. Normal case: Remote sends ~10 Hz (working)
|
||||
2. High frequency: Send 30 Hz updates
|
||||
3. Low frequency: Send 2 Hz updates
|
||||
4. Simulate packet loss: Drop 50% of packets
|
||||
5. Verify proxy still animates correctly
|
||||
|
||||
---
|
||||
|
||||
## Testing Phase 6.3: Iterate & Tune
|
||||
|
||||
### Variable Index Verification
|
||||
|
||||
**Goal:** Confirm animation graph variable indices are correct for FO4
|
||||
|
||||
**Current assumptions (from Phase 3):**
|
||||
```cpp
|
||||
snapshot.floats[0] = Speed;
|
||||
snapshot.bools[0] = isSprinting;
|
||||
snapshot.bools[1] = isSneaking;
|
||||
```
|
||||
|
||||
**How to verify:**
|
||||
1. If animations work → Indices likely correct
|
||||
2. If animations don't play:
|
||||
- Enable `F4TLocalAnimationGraphDebug.cpp` debug logging
|
||||
- Dump actual variable indices from local player
|
||||
- Compare with descriptor table
|
||||
- Update `docs/f4-animation-descriptor.md`
|
||||
|
||||
**Debug logging (add if needed):**
|
||||
```cpp
|
||||
// In ApplyProxyAnimationFromRemoteStateDescriptorBased
|
||||
LogInfo(std::format("Variable index 'Speed': {}, value: {}",
|
||||
descriptor.GetFloatVariableIndex("Speed"), desired.graphSpeed));
|
||||
```
|
||||
|
||||
### Velocity Scaling Tuning
|
||||
|
||||
**Current scaling (from Phase 4):**
|
||||
```cpp
|
||||
float speed = distanceDrift / 0.016F; // Convert to units/sec
|
||||
float clampedSpeed = min(speed, 400.0F); // Cap at 400 units/sec
|
||||
```
|
||||
|
||||
**Adjustment procedure:**
|
||||
1. Watch proxy animations while moving
|
||||
2. If walk animation plays but shouldn't → velocity too high
|
||||
3. If sprint animation doesn't trigger → velocity too low
|
||||
4. Adjust multipliers:
|
||||
```cpp
|
||||
// Try different scaling
|
||||
float speed = (distanceDrift / 0.016F) * 0.8F; // Reduce by 20%
|
||||
```
|
||||
|
||||
**Testing different speeds:**
|
||||
```
|
||||
Network Speed | Graph Speed | Expected Animation | Adjust If...
|
||||
2 units/frame | 125 units/s | Sprint | Too low → reduce divisor
|
||||
1 unit/frame | 62 units/s | Run | Too high → increase divisor
|
||||
0.5 units | 31 units/s | Walk | Not triggering → reduce
|
||||
```
|
||||
|
||||
### Animation Variable Tuning
|
||||
|
||||
**If animations partially work:**
|
||||
|
||||
1. **Speed blending isn't smooth:**
|
||||
- Check `speedDamped` variable (if implemented)
|
||||
- May need to scale Speed over time (lerp, not snap)
|
||||
|
||||
2. **Weapon drawn/sneak animation missing:**
|
||||
- Confirm variable indices in descriptor
|
||||
- May need dedicated action events (Phase 2.3)
|
||||
- Document as limitation
|
||||
|
||||
3. **Transitions are jerky:**
|
||||
- Velocity might be changing too rapidly
|
||||
- Add smoothing: lerp velocity over 0.1-0.2 seconds
|
||||
- Or increase lerp alpha on position (currently 0.15)
|
||||
|
||||
---
|
||||
|
||||
## Test Results Template
|
||||
|
||||
Create a new dev-log entry (example):
|
||||
|
||||
```
|
||||
## Test Session: <DATE> - <TESTER>
|
||||
|
||||
### Session Setup
|
||||
- Number of proxies: 1/2/4
|
||||
- Duration: 5 minutes
|
||||
- Network condition: Normal/Lag/Packet Loss
|
||||
|
||||
### Test 1: Dynamic Spawning
|
||||
- [x] Proxy spawned successfully
|
||||
- [ ] Proxy appeared at correct location
|
||||
- Result: PASS/FAIL/PARTIAL
|
||||
|
||||
### Test 2: Smooth Movement
|
||||
- [x] Movement is smooth
|
||||
- [ ] No visible jitter
|
||||
- Result: PASS/FAIL/PARTIAL
|
||||
|
||||
### Test 3: Animation Triggering (CRITICAL)
|
||||
- [ ] Idle animation plays
|
||||
- [ ] Walk animation plays
|
||||
- [ ] Run animation plays
|
||||
- [ ] Sprint animation plays
|
||||
- Result: PASS/FAIL/PARTIAL
|
||||
- Notes: <describe any issues>
|
||||
|
||||
### Test 4: Sneak Animation
|
||||
- [ ] Crouch plays when sneaking
|
||||
- Result: PASS/FAIL/PARTIAL
|
||||
|
||||
### Test 5: Multiple Proxies (4x)
|
||||
- [ ] All 4 proxies move smoothly
|
||||
- [ ] No crashes
|
||||
- [ ] Frame rate acceptable
|
||||
- Result: PASS/FAIL/PARTIAL
|
||||
|
||||
### Performance Metrics
|
||||
- Per-proxy update time: <N> ms
|
||||
- Descriptor bulk write time: <N> ms
|
||||
- Frame rate: <N> fps
|
||||
|
||||
### Issues Found
|
||||
1. <Issue description>
|
||||
- Severity: Critical/Major/Minor
|
||||
- Workaround: <if any>
|
||||
- Fix needed in: <file/phase>
|
||||
|
||||
### Conclusion
|
||||
Overall result: READY FOR PHASE 7 / NEEDS ITERATION / NEEDS FIXES
|
||||
|
||||
### Next Steps
|
||||
- <Action items>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
If animations aren't working:
|
||||
|
||||
1. **Verify velocity is being set:**
|
||||
```
|
||||
Search logs for: "ApplyRuntimeProxyTransform"
|
||||
Should show: Character controller velocity being set
|
||||
```
|
||||
|
||||
2. **Verify actor state flags are applied:**
|
||||
```
|
||||
Search logs for: "Moved runtime proxy actor ... flags1="
|
||||
Should show non-zero flags if remote player in combat/sneak
|
||||
```
|
||||
|
||||
3. **Verify animation variables are being written:**
|
||||
```
|
||||
Search logs for: "Runtime proxy animation sync"
|
||||
Should show Speed values changing (not always 0)
|
||||
```
|
||||
|
||||
4. **Check if animation graph manager is available:**
|
||||
```
|
||||
Search logs for: "animation graph manager unavailable"
|
||||
If present: Graph manager not accessible
|
||||
```
|
||||
|
||||
5. **Enable full animation debugging:**
|
||||
- Set logging interval to 100ms (from 2s)
|
||||
- Enables detailed animation state transitions
|
||||
- Generates large log files
|
||||
|
||||
---
|
||||
|
||||
## Common Issues & Fixes
|
||||
|
||||
| Issue | Possible Cause | Fix |
|
||||
|-------|---|---|
|
||||
| Proxy doesn't spawn | PlaceAtMe failed | Check proxy base actor exists |
|
||||
| Proxy stands still | Velocity not set | Verify character controller access |
|
||||
| Walk/run doesn't play | Speed variable wrong index | Check f4-animation-descriptor.md indices |
|
||||
| Sneak doesn't work | Needs action replay | Phase 2.3 action capture needed |
|
||||
| Multiple proxies crash | Handle leak | Check proxy pool cleanup |
|
||||
| Frame rate drops | Too many descriptor writes | Already optimized; check log frequency |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria (Phase 6 Complete)
|
||||
|
||||
- ✅ Proxy spawns and moves smoothly (dynamic)
|
||||
- ✅ Velocity injection triggers animation system
|
||||
- ✅ At least idle/walk/run animations visible
|
||||
- ✅ Multiple proxies (4x) stable and animating
|
||||
- ✅ No crashes in 10-minute test session
|
||||
- ✅ Performance acceptable (< 1ms per proxy update)
|
||||
- ✅ Actor state flags affect proxy (sneak at minimum)
|
||||
|
||||
---
|
||||
|
||||
## Ready for Phase 7?
|
||||
|
||||
If all tests PASS:
|
||||
→ Phase 7: Cleanup & Documentation
|
||||
|
||||
If tests FAIL/PARTIAL:
|
||||
→ Iterate Phase 6.3 (tune velocity/variables)
|
||||
→ Return to Phase 5 if infrastructure issues found
|
||||
|
||||
If tests CRASH:
|
||||
→ Debug and fix issues
|
||||
→ Return to Phase 4-5 as needed
|
||||
|
||||
@@ -96,7 +96,7 @@ This document describes the Phase 2 protocol extensions to support actor state f
|
||||
## World State Packet
|
||||
|
||||
Host-authoritative time and weather sync uses a separate `worldState` packet.
|
||||
Full field definitions live in [`protocol/world-state.md`](../protocol/world-state.md).
|
||||
Full field definitions live in [`protocol/world-state.md`](protocol/world-state.md).
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
+1
-1
@@ -63,4 +63,4 @@ The protocol should be testable outside Fallout 4 before it is used inside the F
|
||||
`transform` packets carry position, movement state, optional `equippedItems`
|
||||
apparel snapshots, and optional `appearance` snapshots for best-effort proxy
|
||||
body/face visuals. The canonical field list and compatibility rules live in
|
||||
[`protocol/packets.md`](../protocol/packets.md).
|
||||
[`protocol/packets.md`](protocol/packets.md).
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
# Vanilla Interface reference
|
||||
|
||||
Decompiled Fallout 4 interface assets kept for **reading and research only**.
|
||||
|
||||
Commonwealth Online injects the main-menu **Multiplayer** row at runtime from C++. Do **not** re-import or deploy patched SWFs from this folder.
|
||||
|
||||
See [`../../MainMenu_Injection.md`](../../MainMenu_Injection.md) for the supported approach.
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+28
@@ -0,0 +1,28 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import flash.display.MovieClip;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol64")]
|
||||
public dynamic class BSButtonHint_IconHolder_56 extends MovieClip
|
||||
{
|
||||
|
||||
public var IconAnimInstance:MovieClip;
|
||||
|
||||
public function BSButtonHint_IconHolder_56()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,59,this.frame60);
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame60() : *
|
||||
{
|
||||
gotoAndPlay("Flashing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import adobe.utils.*;
|
||||
import flash.accessibility.*;
|
||||
import flash.desktop.*;
|
||||
import flash.display.*;
|
||||
import flash.errors.*;
|
||||
import flash.events.*;
|
||||
import flash.external.*;
|
||||
import flash.filters.*;
|
||||
import flash.geom.*;
|
||||
import flash.globalization.*;
|
||||
import flash.media.*;
|
||||
import flash.net.*;
|
||||
import flash.net.drm.*;
|
||||
import flash.printing.*;
|
||||
import flash.profiler.*;
|
||||
import flash.sampler.*;
|
||||
import flash.sensors.*;
|
||||
import flash.system.*;
|
||||
import flash.text.*;
|
||||
import flash.text.engine.*;
|
||||
import flash.text.ime.*;
|
||||
import flash.ui.*;
|
||||
import flash.utils.*;
|
||||
import flash.xml.*;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol135")]
|
||||
public dynamic class ControlsFader_38 extends MovieClip
|
||||
{
|
||||
|
||||
public var List_mc:InputMappingList;
|
||||
|
||||
public function ControlsFader_38()
|
||||
{
|
||||
super();
|
||||
this.__setProp_List_mc_ControlsFader_List_0();
|
||||
}
|
||||
|
||||
internal function __setProp_List_mc_ControlsFader_List_0() : *
|
||||
{
|
||||
try
|
||||
{
|
||||
this.List_mc["componentInspectorSetting"] = true;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
this.List_mc.disableSelection = false;
|
||||
this.List_mc.listEntryClass = "InputMappingListEntry";
|
||||
this.List_mc.numListItems = 8;
|
||||
this.List_mc.restoreListIndex = true;
|
||||
this.List_mc.textOption = "None";
|
||||
this.List_mc.verticalSpacing = 1.2;
|
||||
try
|
||||
{
|
||||
this.List_mc["componentInspectorSetting"] = false;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import flash.display.MovieClip;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol136")]
|
||||
public dynamic class ControlsPanel_37 extends MovieClip
|
||||
{
|
||||
|
||||
public var Fader_mc:MovieClip;
|
||||
|
||||
public function ControlsPanel_37()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,2,this.frame3,4,this.frame5);
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame3() : *
|
||||
{
|
||||
visible = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame5() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import adobe.utils.*;
|
||||
import flash.accessibility.*;
|
||||
import flash.desktop.*;
|
||||
import flash.display.*;
|
||||
import flash.errors.*;
|
||||
import flash.events.*;
|
||||
import flash.external.*;
|
||||
import flash.filters.*;
|
||||
import flash.geom.*;
|
||||
import flash.globalization.*;
|
||||
import flash.media.*;
|
||||
import flash.net.*;
|
||||
import flash.net.drm.*;
|
||||
import flash.printing.*;
|
||||
import flash.profiler.*;
|
||||
import flash.sampler.*;
|
||||
import flash.sensors.*;
|
||||
import flash.system.*;
|
||||
import flash.text.*;
|
||||
import flash.text.engine.*;
|
||||
import flash.text.ime.*;
|
||||
import flash.ui.*;
|
||||
import flash.utils.*;
|
||||
import flash.xml.*;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol117")]
|
||||
public dynamic class DLCPanel_16 extends MovieClip
|
||||
{
|
||||
|
||||
public var DLCInstalledText_tf:TextField;
|
||||
|
||||
public var EmptyWarning_mc:MovieClip;
|
||||
|
||||
public var List_mc:DLCList;
|
||||
|
||||
public function DLCPanel_16()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,3,this.frame4,5,this.frame6);
|
||||
this.__setProp_List_mc_DLCPanel_List_0();
|
||||
}
|
||||
|
||||
internal function __setProp_List_mc_DLCPanel_List_0() : *
|
||||
{
|
||||
try
|
||||
{
|
||||
this.List_mc["componentInspectorSetting"] = true;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
this.List_mc.disableSelection = true;
|
||||
this.List_mc.listEntryClass = "DLCListEntry";
|
||||
this.List_mc.numListItems = 8;
|
||||
this.List_mc.restoreListIndex = true;
|
||||
this.List_mc.textOption = "Shrink To Fit";
|
||||
this.List_mc.verticalSpacing = 0;
|
||||
try
|
||||
{
|
||||
this.List_mc["componentInspectorSetting"] = false;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame4() : *
|
||||
{
|
||||
visible = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame6() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import flash.display.MovieClip;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol28")]
|
||||
public dynamic class EquippedIcon_87 extends MovieClip
|
||||
{
|
||||
|
||||
public function EquippedIcon_87()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1);
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import adobe.utils.*;
|
||||
import flash.accessibility.*;
|
||||
import flash.desktop.*;
|
||||
import flash.display.*;
|
||||
import flash.errors.*;
|
||||
import flash.events.*;
|
||||
import flash.external.*;
|
||||
import flash.filters.*;
|
||||
import flash.geom.*;
|
||||
import flash.globalization.*;
|
||||
import flash.media.*;
|
||||
import flash.net.*;
|
||||
import flash.net.drm.*;
|
||||
import flash.printing.*;
|
||||
import flash.profiler.*;
|
||||
import flash.sampler.*;
|
||||
import flash.sensors.*;
|
||||
import flash.system.*;
|
||||
import flash.text.*;
|
||||
import flash.text.engine.*;
|
||||
import flash.text.ime.*;
|
||||
import flash.ui.*;
|
||||
import flash.utils.*;
|
||||
import flash.xml.*;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol134")]
|
||||
public dynamic class HelpPanel_mc_34 extends MovieClip
|
||||
{
|
||||
|
||||
public var HelpListBrackets_mc:MovieClip;
|
||||
|
||||
public var HelpList_mc:HelpList;
|
||||
|
||||
public var background_mc:MovieClip;
|
||||
|
||||
public function HelpPanel_mc_34()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,2,this.frame3,4,this.frame5);
|
||||
this.__setProp_HelpList_mc_HelpPanel_mc_List_0();
|
||||
}
|
||||
|
||||
internal function __setProp_HelpList_mc_HelpPanel_mc_List_0() : *
|
||||
{
|
||||
try
|
||||
{
|
||||
this.HelpList_mc["componentInspectorSetting"] = true;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
this.HelpList_mc.disableSelection = false;
|
||||
this.HelpList_mc.listEntryClass = "MainMenuHelpListEntry";
|
||||
this.HelpList_mc.numListItems = 12;
|
||||
this.HelpList_mc.restoreListIndex = true;
|
||||
this.HelpList_mc.textOption = "None";
|
||||
this.HelpList_mc.verticalSpacing = 0;
|
||||
try
|
||||
{
|
||||
this.HelpList_mc["componentInspectorSetting"] = false;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame3() : *
|
||||
{
|
||||
visible = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame5() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import adobe.utils.*;
|
||||
import flash.accessibility.*;
|
||||
import flash.desktop.*;
|
||||
import flash.display.*;
|
||||
import flash.errors.*;
|
||||
import flash.events.*;
|
||||
import flash.external.*;
|
||||
import flash.filters.*;
|
||||
import flash.geom.*;
|
||||
import flash.globalization.*;
|
||||
import flash.media.*;
|
||||
import flash.net.*;
|
||||
import flash.net.drm.*;
|
||||
import flash.printing.*;
|
||||
import flash.profiler.*;
|
||||
import flash.sampler.*;
|
||||
import flash.sensors.*;
|
||||
import flash.system.*;
|
||||
import flash.text.*;
|
||||
import flash.text.engine.*;
|
||||
import flash.text.ime.*;
|
||||
import flash.ui.*;
|
||||
import flash.utils.*;
|
||||
import flash.xml.*;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol128")]
|
||||
public dynamic class InstalledContentPanel_mc_30 extends MovieClip
|
||||
{
|
||||
|
||||
public var InstalledContentListBrackets_mc:MovieClip;
|
||||
|
||||
public var InstalledContentList_mc:InstalledContentList;
|
||||
|
||||
public var background_mc:MovieClip;
|
||||
|
||||
public function InstalledContentPanel_mc_30()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,2,this.frame3,4,this.frame5);
|
||||
this.__setProp_InstalledContentList_mc_InstalledContentPanel_mc_List_0();
|
||||
}
|
||||
|
||||
internal function __setProp_InstalledContentList_mc_InstalledContentPanel_mc_List_0() : *
|
||||
{
|
||||
try
|
||||
{
|
||||
this.InstalledContentList_mc["componentInspectorSetting"] = true;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
this.InstalledContentList_mc.disableSelection = false;
|
||||
this.InstalledContentList_mc.listEntryClass = "MainMenuInstalledContentListEntry";
|
||||
this.InstalledContentList_mc.numListItems = 12;
|
||||
this.InstalledContentList_mc.restoreListIndex = true;
|
||||
this.InstalledContentList_mc.textOption = "None";
|
||||
this.InstalledContentList_mc.verticalSpacing = 0;
|
||||
try
|
||||
{
|
||||
this.InstalledContentList_mc["componentInspectorSetting"] = false;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame3() : *
|
||||
{
|
||||
visible = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame5() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import adobe.utils.*;
|
||||
import flash.accessibility.*;
|
||||
import flash.desktop.*;
|
||||
import flash.display.*;
|
||||
import flash.errors.*;
|
||||
import flash.events.*;
|
||||
import flash.external.*;
|
||||
import flash.filters.*;
|
||||
import flash.geom.*;
|
||||
import flash.globalization.*;
|
||||
import flash.media.*;
|
||||
import flash.net.*;
|
||||
import flash.net.drm.*;
|
||||
import flash.printing.*;
|
||||
import flash.profiler.*;
|
||||
import flash.sampler.*;
|
||||
import flash.sensors.*;
|
||||
import flash.system.*;
|
||||
import flash.text.*;
|
||||
import flash.text.engine.*;
|
||||
import flash.text.ime.*;
|
||||
import flash.ui.*;
|
||||
import flash.utils.*;
|
||||
import flash.xml.*;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol148")]
|
||||
public dynamic class MainListPanel_49 extends MovieClip
|
||||
{
|
||||
|
||||
public var List_mc:MainMenuList;
|
||||
|
||||
public var MainPanelBackground_mc:MovieClip;
|
||||
|
||||
public function MainListPanel_49()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,2,this.frame3,4,this.frame5);
|
||||
this.__setProp_List_mc_MainListPanel_List_0();
|
||||
}
|
||||
|
||||
internal function __setProp_List_mc_MainListPanel_List_0() : *
|
||||
{
|
||||
try
|
||||
{
|
||||
this.List_mc["componentInspectorSetting"] = true;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
this.List_mc.disableSelection = false;
|
||||
this.List_mc.listEntryClass = "MainMenuListEntry";
|
||||
this.List_mc.numListItems = 9;
|
||||
this.List_mc.restoreListIndex = true;
|
||||
this.List_mc.textOption = "None";
|
||||
this.List_mc.verticalSpacing = 1.2;
|
||||
try
|
||||
{
|
||||
this.List_mc["componentInspectorSetting"] = false;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
gotoAndPlay("start");
|
||||
}
|
||||
|
||||
internal function frame3() : *
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame5() : *
|
||||
{
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import adobe.utils.*;
|
||||
import flash.accessibility.*;
|
||||
import flash.desktop.*;
|
||||
import flash.display.*;
|
||||
import flash.errors.*;
|
||||
import flash.events.*;
|
||||
import flash.external.*;
|
||||
import flash.filters.*;
|
||||
import flash.geom.*;
|
||||
import flash.globalization.*;
|
||||
import flash.media.*;
|
||||
import flash.net.*;
|
||||
import flash.net.drm.*;
|
||||
import flash.printing.*;
|
||||
import flash.profiler.*;
|
||||
import flash.sampler.*;
|
||||
import flash.sensors.*;
|
||||
import flash.system.*;
|
||||
import flash.text.*;
|
||||
import flash.text.engine.*;
|
||||
import flash.text.ime.*;
|
||||
import flash.ui.*;
|
||||
import flash.utils.*;
|
||||
import flash.xml.*;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol138")]
|
||||
public dynamic class OptionsListFader_41 extends MovieClip
|
||||
{
|
||||
|
||||
public var List_mc:OptionsList;
|
||||
|
||||
public function OptionsListFader_41()
|
||||
{
|
||||
super();
|
||||
this.__setProp_List_mc_OptionsListFader_List_0();
|
||||
}
|
||||
|
||||
internal function __setProp_List_mc_OptionsListFader_List_0() : *
|
||||
{
|
||||
try
|
||||
{
|
||||
this.List_mc["componentInspectorSetting"] = true;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
this.List_mc.disableSelection = false;
|
||||
this.List_mc.listEntryClass = "SettingsOptionItem";
|
||||
this.List_mc.numListItems = 8;
|
||||
this.List_mc.restoreListIndex = true;
|
||||
this.List_mc.textOption = "None";
|
||||
this.List_mc.verticalSpacing = 0.6;
|
||||
try
|
||||
{
|
||||
this.List_mc["componentInspectorSetting"] = false;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import flash.display.MovieClip;
|
||||
import flash.text.TextField;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol139")]
|
||||
public dynamic class OptionsListPanel_40 extends MovieClip
|
||||
{
|
||||
|
||||
public var Fader_mc:MovieClip;
|
||||
|
||||
public var GuideText_tf:TextField;
|
||||
|
||||
public function OptionsListPanel_40()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,2,this.frame3,4,this.frame5);
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame3() : *
|
||||
{
|
||||
visible = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame5() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import flash.display.MovieClip;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol140")]
|
||||
public dynamic class SaveLoadPanelHolder_43 extends MovieClip
|
||||
{
|
||||
|
||||
public var Panel_mc:SaveLoadPanel;
|
||||
|
||||
public var icsaveLoadList_mc:MovieClip;
|
||||
|
||||
public function SaveLoadPanelHolder_43()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,2,this.frame3,4,this.frame5);
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame3() : *
|
||||
{
|
||||
visible = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame5() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import adobe.utils.*;
|
||||
import flash.accessibility.*;
|
||||
import flash.desktop.*;
|
||||
import flash.display.*;
|
||||
import flash.errors.*;
|
||||
import flash.events.*;
|
||||
import flash.external.*;
|
||||
import flash.filters.*;
|
||||
import flash.geom.*;
|
||||
import flash.globalization.*;
|
||||
import flash.media.*;
|
||||
import flash.net.*;
|
||||
import flash.net.drm.*;
|
||||
import flash.printing.*;
|
||||
import flash.profiler.*;
|
||||
import flash.sampler.*;
|
||||
import flash.sensors.*;
|
||||
import flash.system.*;
|
||||
import flash.text.*;
|
||||
import flash.text.engine.*;
|
||||
import flash.text.ime.*;
|
||||
import flash.ui.*;
|
||||
import flash.utils.*;
|
||||
import flash.xml.*;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol118")]
|
||||
public dynamic class SettingsListPanel_25 extends MovieClip
|
||||
{
|
||||
|
||||
public var SettingsList_mc:SettingsList;
|
||||
|
||||
public function SettingsListPanel_25()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,2,this.frame3,4,this.frame5);
|
||||
this.__setProp_SettingsList_mc_SettingsListPanel_List_0();
|
||||
}
|
||||
|
||||
internal function __setProp_SettingsList_mc_SettingsListPanel_List_0() : *
|
||||
{
|
||||
try
|
||||
{
|
||||
this.SettingsList_mc["componentInspectorSetting"] = true;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
this.SettingsList_mc.disableSelection = false;
|
||||
this.SettingsList_mc.listEntryClass = "SettingsCategoryListEntry";
|
||||
this.SettingsList_mc.numListItems = 4;
|
||||
this.SettingsList_mc.restoreListIndex = true;
|
||||
this.SettingsList_mc.textOption = "None";
|
||||
this.SettingsList_mc.verticalSpacing = 0;
|
||||
try
|
||||
{
|
||||
this.SettingsList_mc["componentInspectorSetting"] = false;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame3() : *
|
||||
{
|
||||
visible = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame5() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import adobe.utils.*;
|
||||
import flash.accessibility.*;
|
||||
import flash.desktop.*;
|
||||
import flash.display.*;
|
||||
import flash.errors.*;
|
||||
import flash.events.*;
|
||||
import flash.external.*;
|
||||
import flash.filters.*;
|
||||
import flash.geom.*;
|
||||
import flash.globalization.*;
|
||||
import flash.media.*;
|
||||
import flash.net.*;
|
||||
import flash.net.drm.*;
|
||||
import flash.printing.*;
|
||||
import flash.profiler.*;
|
||||
import flash.sampler.*;
|
||||
import flash.sensors.*;
|
||||
import flash.system.*;
|
||||
import flash.text.*;
|
||||
import flash.text.engine.*;
|
||||
import flash.text.ime.*;
|
||||
import flash.ui.*;
|
||||
import flash.utils.*;
|
||||
import flash.xml.*;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol118")]
|
||||
public dynamic class SettingsListPanel_25 extends MovieClip
|
||||
{
|
||||
|
||||
public var SettingsList_mc:SettingsList;
|
||||
|
||||
public function SettingsListPanel_25()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,2,this.frame3,4,this.frame5);
|
||||
this.__setProp_SettingsList_mc_SettingsListPanel_List_0();
|
||||
}
|
||||
|
||||
internal function __setProp_SettingsList_mc_SettingsListPanel_List_0() : *
|
||||
{
|
||||
try
|
||||
{
|
||||
this.SettingsList_mc["componentInspectorSetting"] = true;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
this.SettingsList_mc.disableSelection = false;
|
||||
this.SettingsList_mc.listEntryClass = "SettingsCategoryListEntry";
|
||||
this.SettingsList_mc.numListItems = 4;
|
||||
this.SettingsList_mc.restoreListIndex = true;
|
||||
this.SettingsList_mc.textOption = "None";
|
||||
this.SettingsList_mc.verticalSpacing = 0;
|
||||
try
|
||||
{
|
||||
this.SettingsList_mc["componentInspectorSetting"] = false;
|
||||
}
|
||||
catch(e:Error)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame3() : *
|
||||
{
|
||||
visible = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame5() : *
|
||||
{
|
||||
visible = false;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package MainMenu_fla
|
||||
{
|
||||
import Shared.AS3.BSButtonHint;
|
||||
import flash.display.MovieClip;
|
||||
import flash.text.TextField;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol152")]
|
||||
public dynamic class Upsell_53 extends MovieClip
|
||||
{
|
||||
|
||||
public var MainPanelBackground_mc:MovieClip;
|
||||
|
||||
public var Upsell_Button_mc:BSButtonHint;
|
||||
|
||||
public var Upsell_tf:TextField;
|
||||
|
||||
public function Upsell_53()
|
||||
{
|
||||
super();
|
||||
addFrameScript(0,this.frame1,2,this.frame3,4,this.frame5);
|
||||
}
|
||||
|
||||
internal function frame1() : *
|
||||
{
|
||||
gotoAndPlay("start");
|
||||
}
|
||||
|
||||
internal function frame3() : *
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
internal function frame5() : *
|
||||
{
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package
|
||||
{
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol26")]
|
||||
public dynamic class SettingsCategoryListEntry extends MainMenuListEntry
|
||||
{
|
||||
|
||||
public function SettingsCategoryListEntry()
|
||||
{
|
||||
super();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package
|
||||
{
|
||||
import Shared.AS3.BSScrollingList;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol84")]
|
||||
public dynamic class SettingsList extends BSScrollingList
|
||||
{
|
||||
|
||||
public function SettingsList()
|
||||
{
|
||||
super();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package
|
||||
{
|
||||
import Shared.AS3.BSScrollingListEntry;
|
||||
import Shared.GlobalFunc;
|
||||
import flash.display.MovieClip;
|
||||
import flash.events.Event;
|
||||
import flash.events.KeyboardEvent;
|
||||
import flash.geom.ColorTransform;
|
||||
import scaleform.gfx.Extensions;
|
||||
import scaleform.gfx.TextFieldEx;
|
||||
|
||||
[Embed(source="/_assets/assets.swf", symbol="symbol41")]
|
||||
public class SettingsOptionItem extends BSScrollingListEntry
|
||||
{
|
||||
|
||||
public static const VALUE_CHANGE:String = "SettingsOptionItem::value_change";
|
||||
|
||||
private var OptionItem:MovieClip;
|
||||
|
||||
private var uiMovieType:uint;
|
||||
|
||||
private var uiID:uint;
|
||||
|
||||
private var bHUDColorUpdate:Boolean;
|
||||
|
||||
private var bPipboyColorUpdate:Boolean;
|
||||
|
||||
private var bDifficultyUpdate:Boolean;
|
||||
|
||||
public function SettingsOptionItem()
|
||||
{
|
||||
super();
|
||||
this.uiID = uint.MAX_VALUE;
|
||||
this.bHUDColorUpdate = false;
|
||||
this.bPipboyColorUpdate = false;
|
||||
addEventListener(Option_Checkbox.VALUE_CHANGE,this.onValueChange);
|
||||
addEventListener(Option_OptionStepper.VALUE_CHANGE,this.onValueChange);
|
||||
addEventListener(Option_Scrollbar.VALUE_CHANGE,this.onValueChange);
|
||||
Extensions.enabled = true;
|
||||
TextFieldEx.setTextAutoSize(textField,"shrink");
|
||||
}
|
||||
|
||||
public function get movieType() : uint
|
||||
{
|
||||
return this.uiMovieType;
|
||||
}
|
||||
|
||||
public function set movieType(param1:uint) : *
|
||||
{
|
||||
this.uiMovieType = param1;
|
||||
if(this.OptionItem != null)
|
||||
{
|
||||
removeChild(this.OptionItem);
|
||||
this.OptionItem = null;
|
||||
}
|
||||
switch(this.uiMovieType)
|
||||
{
|
||||
case 0:
|
||||
this.OptionItem = new Option_Scrollbar();
|
||||
break;
|
||||
case 1:
|
||||
this.OptionItem = new Option_OptionStepper();
|
||||
break;
|
||||
case 2:
|
||||
this.OptionItem = new Option_Checkbox();
|
||||
}
|
||||
addChild(this.OptionItem);
|
||||
this.OptionItem.x = 210;
|
||||
this.OptionItem.y = -1;
|
||||
}
|
||||
|
||||
public function get ID() : uint
|
||||
{
|
||||
return this.uiID;
|
||||
}
|
||||
|
||||
public function set ID(param1:uint) : *
|
||||
{
|
||||
this.uiID = param1;
|
||||
}
|
||||
|
||||
public function get hudColorUpdate() : Boolean
|
||||
{
|
||||
return this.bHUDColorUpdate;
|
||||
}
|
||||
|
||||
public function set hudColorUpdate(param1:Boolean) : *
|
||||
{
|
||||
this.bHUDColorUpdate = param1;
|
||||
}
|
||||
|
||||
public function get pipboyColorUpdate() : Boolean
|
||||
{
|
||||
return this.bPipboyColorUpdate;
|
||||
}
|
||||
|
||||
public function set pipboyColorUpdate(param1:Boolean) : *
|
||||
{
|
||||
this.bPipboyColorUpdate = param1;
|
||||
}
|
||||
|
||||
public function get difficultyUpdate() : Boolean
|
||||
{
|
||||
return this.bDifficultyUpdate;
|
||||
}
|
||||
|
||||
public function set difficultyUpdate(param1:Boolean) : void
|
||||
{
|
||||
this.bDifficultyUpdate = param1;
|
||||
}
|
||||
|
||||
public function get value() : Number
|
||||
{
|
||||
var _loc1_:Number = NaN;
|
||||
switch(this.uiMovieType)
|
||||
{
|
||||
case 0:
|
||||
_loc1_ = (this.OptionItem as Option_Scrollbar).value;
|
||||
break;
|
||||
case 1:
|
||||
_loc1_ = (this.OptionItem as Option_OptionStepper).index;
|
||||
break;
|
||||
case 2:
|
||||
_loc1_ = (this.OptionItem as Option_Checkbox).checked ? 1 : 0;
|
||||
}
|
||||
return _loc1_;
|
||||
}
|
||||
|
||||
public function set value(param1:Number) : *
|
||||
{
|
||||
switch(this.uiMovieType)
|
||||
{
|
||||
case 0:
|
||||
(this.OptionItem as Option_Scrollbar).value = param1;
|
||||
break;
|
||||
case 1:
|
||||
(this.OptionItem as Option_OptionStepper).index = param1;
|
||||
break;
|
||||
case 2:
|
||||
(this.OptionItem as Option_Checkbox).checked = param1 == 1 ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
public function SetOptionStepperOptions(param1:Array) : *
|
||||
{
|
||||
if(this.uiMovieType == 1)
|
||||
{
|
||||
(this.OptionItem as Option_OptionStepper).options = param1;
|
||||
}
|
||||
}
|
||||
|
||||
override public function SetEntryText(param1:Object, param2:String) : *
|
||||
{
|
||||
var _loc3_:ColorTransform = null;
|
||||
super.SetEntryText(param1,param2);
|
||||
if(this.border != null)
|
||||
{
|
||||
this.border.alpha = this.selected ? GlobalFunc.SELECTED_RECT_ALPHA : 0;
|
||||
}
|
||||
if(this.textField != null)
|
||||
{
|
||||
this.textField.textColor = this.selected ? 0 : 16777215;
|
||||
}
|
||||
if(this.OptionItem != null)
|
||||
{
|
||||
_loc3_ = this.OptionItem.transform.colorTransform;
|
||||
_loc3_.redOffset = this.selected ? -255 : 0;
|
||||
_loc3_.greenOffset = this.selected ? -255 : 0;
|
||||
_loc3_.blueOffset = this.selected ? -255 : 0;
|
||||
this.OptionItem.transform.colorTransform = _loc3_;
|
||||
}
|
||||
}
|
||||
|
||||
public function onItemPressed() : *
|
||||
{
|
||||
if(this.OptionItem != null)
|
||||
{
|
||||
switch(this.uiMovieType)
|
||||
{
|
||||
case 1:
|
||||
(this.OptionItem as Option_OptionStepper).onItemPressed();
|
||||
break;
|
||||
case 2:
|
||||
(this.OptionItem as Option_Checkbox).onItemPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function HandleKeyboardInput(param1:KeyboardEvent) : *
|
||||
{
|
||||
if(this.OptionItem != null)
|
||||
{
|
||||
switch(this.uiMovieType)
|
||||
{
|
||||
case 0:
|
||||
(this.OptionItem as Option_Scrollbar).HandleKeyboardInput(param1);
|
||||
break;
|
||||
case 1:
|
||||
(this.OptionItem as Option_OptionStepper).HandleKeyboardInput(param1);
|
||||
break;
|
||||
case 2:
|
||||
(this.OptionItem as Option_Checkbox).HandleKeyboardInput(param1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function onValueChange(param1:Event) : *
|
||||
{
|
||||
dispatchEvent(new Event(VALUE_CHANGE,true,true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user