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:
@@ -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()`
|
||||
@@ -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()`
|
||||
Reference in New Issue
Block a user