Add FFDec docs/tools and server-browser fixes

Add comprehensive FFDec walkthrough and short patch docs for safely adding a Multiplayer item to MainMenu.swf, plus helper scripts to extract the vanilla SWF and restore it (tools/extract_ba2.py, tools/restore-vanilla-mainmenu.ps1). Update exported MainMenu.as with a warning comment and remove an outdated exported PATCH markdown. Patch binary Interface/MainMenu.swf (updated).

Hardening fixes in the plugin: include GFx headers, avoid leaving the PrismaUI overlay focused after menu changes, only call OnBrowserHidden when the browser was actually visible, and add IsMainMenuOnMainPanel() so WatchMainMenuState ignores MainMenu when not on the MAIN_STATE. Ensure events are only dispatched when the browser is valid & visible (CanDispatchToBrowser) and make Scaleform UI hide the browser immediately during transitions. These changes prevent the overlay from staying focused during Settings transitions and stop JS events from being sent to a hidden/invalid view.
This commit is contained in:
2026-06-08 22:26:00 +12:00
parent 50d48f1f99
commit 9c84c9d4bc
10 changed files with 636 additions and 165 deletions
+315
View File
@@ -0,0 +1,315 @@
# FFDec guide — add Multiplayer button to MainMenu.swf
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**.
---
## 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()`
+159
View File
@@ -0,0 +1,159 @@
# 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()`