Introduce a C++ runtime injection that adds a Multiplayer row to the Fallout 4 main menu instead of patching MainMenu.swf. Adds F4TMainMenuInject.h/cpp implementing injection, ITEM_PRESS handling, accept/button hooks, resync tasks and logging; integrates installation by calling MainMenuInject::Install() from PrismaUI init. Update PrismaUI to use the new helper and remove a duplicate IsMainMenuOnMainPanel implementation. Documentation updated to deprecate FFDec SWF patching (new docs/MainMenu_Injection.md, note in FFDEC guide, and setup.md) and restore-vanilla-mainmenu.ps1 messaging updated to instruct using the runtime injection.
320 lines
10 KiB
Markdown
320 lines
10 KiB
Markdown
# 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()`
|