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:
Binary file not shown.
@@ -1,144 +0,0 @@
|
||||
# MainMenu.swf — Multiplayer button patch guide
|
||||
|
||||
Reference export: [`scripts/MainMenu.as`](scripts/MainMenu.as)
|
||||
Edit the script **inside FFDec** on the SWF (or paste changes back into FFDec after editing the export). Saving only the `.as` file does not update `MainMenu.swf`.
|
||||
|
||||
## Why texts/sprites only showed CONTINUE
|
||||
|
||||
The main menu is a **dynamic list** (`MainPanel_mc.List_mc`). Items like `$NEW`, `$LOAD`, `$SETTINGS` are pushed in **`InitList()`**, not stored as static text tags.
|
||||
|
||||
## Three edits in `MainMenu` class
|
||||
|
||||
### 1. Add index constant (~line 214)
|
||||
|
||||
After `INSTALLED_CONTENT_INDEX`:
|
||||
|
||||
```actionscript
|
||||
private const MULTIPLAYER_INDEX:Number = 15;
|
||||
```
|
||||
|
||||
Index `15` is unused in vanilla (highest defined is `INSTALLED_CONTENT_INDEX = 14`).
|
||||
|
||||
### 2. Add list entry in `InitList()` (~line 589)
|
||||
|
||||
Insert **after** the `$LOAD` push and **before** the PS5 transfer / DLC block — only on the title screen (`!PauseMode`):
|
||||
|
||||
```actionscript
|
||||
this.MainPanel_mc.List_mc.entryList.push({
|
||||
"text":"$LOAD",
|
||||
"disabled":!this.HasRecentSave || !param6,
|
||||
"index":this.LOAD_INDEX
|
||||
});
|
||||
// --- ADD BELOW ---
|
||||
if(!this.PauseMode)
|
||||
{
|
||||
this.MainPanel_mc.List_mc.entryList.push({
|
||||
"text":"Multiplayer",
|
||||
"index":this.MULTIPLAYER_INDEX
|
||||
});
|
||||
}
|
||||
// --- ADD ABOVE ---
|
||||
if(param8 == true)
|
||||
```
|
||||
|
||||
Using literal `"MULTIPLAYER"` avoids needing a `.strings` entry for the PoC. Later switch to `"$COSYNC_MULTIPLAYER"` + `Interface/Translate_en.txt` if you want localization.
|
||||
|
||||
**Label text:** For the PoC use a **literal** label (no `$` prefix):
|
||||
|
||||
```actionscript
|
||||
"text":"Multiplayer",
|
||||
```
|
||||
|
||||
Strings starting with `$` are looked up in `Interface/Translate_en.txt` (from `Fallout4 - Interface.ba2`). A lone `Interface/Translations/MainMenu_en.txt` is **not** how vanilla resolves main-menu keys, so `$MULTIPLAYER` shows as-is until you merge the key into `Translate_en.txt` (extract from BA2, append `$MULTIPLAYER Multiplayer`, redeploy loose file).
|
||||
|
||||
Regenerate optional translation stub: `python tools/write_mainmenu_translation.py` (for future i18n / Translate_en merge only).
|
||||
|
||||
### Fallout 4 must load loose Interface files
|
||||
|
||||
Loose `Data/Interface/*.swf` overrides are **ignored** unless archive invalidation is on. Add to `%USERPROFILE%/Documents/My Games/Fallout4/Fallout4Custom.ini`:
|
||||
|
||||
```ini
|
||||
[Archive]
|
||||
bInvalidateOlderFiles=1
|
||||
```
|
||||
|
||||
(MO2 enables this automatically; manual `Data/` copies need it.)
|
||||
|
||||
Without invalidation, the game keeps using `Fallout4 - Interface.ba2` — your patched SWF never loads even though the file is on disk.
|
||||
|
||||
Suggested placement: between **Load** and **Add-ons** so it reads naturally on the main menu.
|
||||
|
||||
### 3. Handle selection in `onMainListItemPress()` switch (~line 864)
|
||||
|
||||
Add **before** `case this.HELP_INDEX:` (or anywhere in the switch):
|
||||
|
||||
```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;
|
||||
```
|
||||
|
||||
Requires the C++ plugin to register `commonwealthOnline.openManager()` and `commonwealthOnline.closeManager()` via F4SE Scaleform.
|
||||
|
||||
### 4. Auto-close when leaving Multiplayer (~line 716 and ~onMainListItemPress)
|
||||
|
||||
Add a helper and call it when the main menu navigates to any other screen (Settings, Load, Continue, etc.):
|
||||
|
||||
```actionscript
|
||||
private function CloseMultiplayerBrowser() : *
|
||||
{
|
||||
if(root.f4se != null && root.f4se.plugins != null && root.f4se.plugins.commonwealthOnline != null)
|
||||
{
|
||||
root.f4se.plugins.commonwealthOnline.closeManager();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the `currentState` setter, after assigning `strCurrentState`:
|
||||
|
||||
```actionscript
|
||||
if(param1 != this.MAIN_STATE)
|
||||
{
|
||||
this.CloseMultiplayerBrowser();
|
||||
}
|
||||
```
|
||||
|
||||
At the start of `onMainListItemPress()` (after the cancel/reset loop), before the `switch`:
|
||||
|
||||
```actionscript
|
||||
if(this.MainPanel_mc.List_mc.selectedEntry.index != this.MULTIPLAYER_INDEX)
|
||||
{
|
||||
this.CloseMultiplayerBrowser();
|
||||
}
|
||||
```
|
||||
|
||||
This closes the HTML server browser when sub-menus open (Settings, Add-ons, etc.) and when any other main-menu item is selected.
|
||||
|
||||
## FFDec workflow
|
||||
|
||||
1. Open `Interface/MainMenu.swf` in FFDec.
|
||||
2. **scripts → MainMenu** → right-click → **Replace / Edit** (or open script, paste from [`scripts/MainMenu.as`](scripts/MainMenu.as)).
|
||||
3. **File → Save** (overwrites SWF).
|
||||
4. Deploy to `Fallout 4/Data/Interface/MainMenu.swf` or your mod folder.
|
||||
5. Launch via F4SE and test.
|
||||
|
||||
**Re-import from edited export:** In FFDec, select the **MainMenu** script under **scripts**, use **Edit ActionScript** and paste the full contents of `Interface/exported/scripts/MainMenu.as`, then save the SWF. FFDec may also offer **Import script** / **Replace script** from file depending on version.
|
||||
|
||||
Keep `MainMenu.swf.vanilla.bak` unchanged.
|
||||
|
||||
## Related functions (no edit needed for PoC)
|
||||
|
||||
| Function | Role |
|
||||
|----------|------|
|
||||
| `InitList()` | Builds `entryList` with `$CONTINUE`, `$NEW`, `$LOAD`, … |
|
||||
| `onMainListItemPress()` | `switch` on `selectedEntry.index` |
|
||||
| `onListItemPress()` | Routes list press events to `onMainListItemPress()` |
|
||||
| Index constants (~184–212) | `CONTINUE_INDEX` … `INSTALLED_CONTENT_INDEX` |
|
||||
|
||||
## Next step (code)
|
||||
|
||||
Implement `commonwealthOnline.openManager()` in `CommonwealthOnline.dll` and `CoSyncServerBrowser.swf` per [docs/custom-swf-ui.md](../../docs/custom-swf-ui.md).
|
||||
@@ -1,5 +1,7 @@
|
||||
package
|
||||
{
|
||||
// REFERENCE ONLY — edit the vanilla MainMenu.swf surgically in FFDec.
|
||||
// Do NOT paste this entire file back into FFDec; full re-import breaks settings sub-menus.
|
||||
import Shared.AS3.BSButtonHint;
|
||||
import Shared.AS3.BSButtonHintBar;
|
||||
import Shared.AS3.BSButtonHintData;
|
||||
@@ -2281,10 +2283,6 @@ package
|
||||
"text":"$Settings_FrameCap",
|
||||
"movieType":1,
|
||||
"options":["$Settings_FrameCap_On","$Settings_FrameCap_60"]
|
||||
},{
|
||||
"text":"$Settings_FrameCap",
|
||||
"movieType":1,
|
||||
"options":["$Settings_FrameCap_On","$Settings_FrameCap_40","$Settings_FrameCap_60"]
|
||||
},{
|
||||
"text":"$Settings_VisualFidelity",
|
||||
"movieType":1,
|
||||
|
||||
@@ -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()`
|
||||
@@ -6,7 +6,11 @@
|
||||
#include "F4TServerBrowserBridge.h"
|
||||
#include "PrismaUI_F4_API.h"
|
||||
|
||||
#include "Scaleform/G/GFx_Movie.h"
|
||||
#include "Scaleform/G/GFx_Value.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string_view>
|
||||
|
||||
@@ -73,24 +77,43 @@ namespace F4T::PrismaUI
|
||||
|
||||
void HideServerBrowserInternal()
|
||||
{
|
||||
if (!g_visible) {
|
||||
return;
|
||||
}
|
||||
const bool wasVisible = g_visible;
|
||||
g_visible = false;
|
||||
F4T::MenuInput::SetBrowserActive(false);
|
||||
|
||||
if (!g_api || !g_api->IsValid(g_serverBrowserView)) {
|
||||
g_visible = false;
|
||||
F4T::MenuInput::SetBrowserActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
g_visible = false;
|
||||
F4T::MenuInput::SetBrowserActive(false);
|
||||
if (wasVisible) {
|
||||
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserHidden();
|
||||
}
|
||||
|
||||
g_api->Unfocus(g_serverBrowserView);
|
||||
g_api->Hide(g_serverBrowserView);
|
||||
RequestNativeCursorRestore();
|
||||
}
|
||||
|
||||
bool IsMainMenuOnMainPanel(const RE::UI* a_ui)
|
||||
{
|
||||
if (!a_ui) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto mainMenu = a_ui->GetMenu<RE::MainMenu>();
|
||||
if (!mainMenu || !mainMenu->uiMovie) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Scaleform::GFx::Value state;
|
||||
if (!mainMenu->uiMovie->GetVariable(&state, "currentState") || !state.IsString()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* currentState = state.GetString();
|
||||
return currentState == nullptr || std::strcmp(currentState, "MAIN_STATE") == 0;
|
||||
}
|
||||
|
||||
void WatchMainMenuState()
|
||||
{
|
||||
if (!g_visible) {
|
||||
@@ -98,7 +121,7 @@ namespace F4T::PrismaUI
|
||||
}
|
||||
|
||||
const auto* ui = RE::UI::GetSingleton();
|
||||
if (!ui || !ui->GetMenuOpen<RE::MainMenu>()) {
|
||||
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !IsMainMenuOnMainPanel(ui)) {
|
||||
HideServerBrowserInternal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,15 +40,16 @@ namespace F4T::ScaleformUI
|
||||
{
|
||||
(void)a_params;
|
||||
|
||||
// Hide immediately so the overlay cannot stay focused while the
|
||||
// main menu transitions into Settings sub-panels (Display, Audio, etc.).
|
||||
F4T::PrismaUI::HideServerBrowser();
|
||||
|
||||
const auto* taskInterface = F4SE::GetTaskInterface();
|
||||
if (taskInterface) {
|
||||
taskInterface->AddTask([]() {
|
||||
F4T::PrismaUI::HideServerBrowser();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
F4T::PrismaUI::HideServerBrowser();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TPrismaUI.h"
|
||||
#include "F4TServerBrowserBridge.h"
|
||||
#include "F4TServerBrowserData.h"
|
||||
|
||||
@@ -30,9 +31,18 @@ namespace F4T::ServerBrowser
|
||||
REX::WARN("{}{}", GetLogPrefix(), a_message);
|
||||
}
|
||||
|
||||
bool CanDispatchToBrowser()
|
||||
{
|
||||
return g_api &&
|
||||
g_view != 0 &&
|
||||
g_api->IsValid(g_view) &&
|
||||
!g_api->IsHidden(g_view) &&
|
||||
F4T::PrismaUI::IsBrowserVisible();
|
||||
}
|
||||
|
||||
void DispatchEvent(const nlohmann::json& a_event)
|
||||
{
|
||||
if (!g_api || g_view == 0 || !g_api->IsValid(g_view)) {
|
||||
if (!CanDispatchToBrowser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,7 +69,7 @@ namespace F4T::ServerBrowser
|
||||
{ "serverCount", g_data.GetFilteredServers().size() }
|
||||
});
|
||||
|
||||
if (g_api && g_view != 0 && g_api->IsValid(g_view)) {
|
||||
if (CanDispatchToBrowser()) {
|
||||
const auto prefix = F4T::Networking::GetLocalPlayerLogPrefix();
|
||||
const auto script = std::format(
|
||||
"if(window.setConnectionStatus){{window.setConnectionStatus({},{});}}",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract a single file from a Fallout 4 BA2 (BTDX) archive."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_name(data: bytes, offset: int) -> tuple[str, int]:
|
||||
length = struct.unpack_from("<H", data, offset)[0]
|
||||
start = offset + 2
|
||||
name = data[start : start + length].decode("utf-8", errors="replace")
|
||||
return name, start + length
|
||||
|
||||
|
||||
def extract_file(archive: Path, target: str, output: Path) -> None:
|
||||
data = archive.read_bytes()
|
||||
if data[:4] != b"BTDX":
|
||||
raise SystemExit(f"Not a BA2 archive: {archive}")
|
||||
|
||||
version = struct.unpack_from("<I", data, 4)[0]
|
||||
if version not in (1, 7, 8):
|
||||
raise SystemExit(f"Unsupported BA2 version {version}")
|
||||
|
||||
archive_type = data[8:12].decode("ascii", errors="replace")
|
||||
if archive_type != "GNRL":
|
||||
raise SystemExit(f"Unsupported archive type {archive_type!r} (only GNRL supported)")
|
||||
|
||||
num_files = struct.unpack_from("<I", data, 12)[0]
|
||||
name_table = struct.unpack_from("<Q", data, 16)[0]
|
||||
file_table = 24
|
||||
|
||||
target_norm = target.replace("/", "\\").lower()
|
||||
name_cursor = name_table
|
||||
for i in range(num_files):
|
||||
name, name_cursor = read_name(data, name_cursor)
|
||||
entry_offset = file_table + i * 36
|
||||
(
|
||||
_name_hash,
|
||||
_ext,
|
||||
_dir_hash,
|
||||
_flags,
|
||||
file_offset,
|
||||
packed_size,
|
||||
unpacked_size,
|
||||
_unk,
|
||||
) = struct.unpack_from("<I4sIIQIII", data, entry_offset)
|
||||
|
||||
if name.lower() != target_norm:
|
||||
continue
|
||||
|
||||
if packed_size and unpacked_size and packed_size != unpacked_size:
|
||||
payload = zlib.decompress(data[file_offset : file_offset + packed_size])
|
||||
else:
|
||||
size = unpacked_size or packed_size
|
||||
payload = data[file_offset : file_offset + size]
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_bytes(payload)
|
||||
print(f"Extracted {name} ({len(payload)} bytes) -> {output}")
|
||||
return
|
||||
|
||||
raise SystemExit(f"File not found in archive: {target}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("archive", type=Path)
|
||||
parser.add_argument("target", help="Archive path, e.g. Interface\\MainMenu.swf")
|
||||
parser.add_argument("output", type=Path)
|
||||
args = parser.parse_args()
|
||||
extract_file(args.archive, args.target, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
# Restore vanilla MainMenu.swf from the game BA2 archive.
|
||||
# The patched SWF must NOT be rebuilt via full FFDec script re-import — that breaks settings.
|
||||
|
||||
param(
|
||||
[string]$GameData = "D:\SteamLibrary\steamapps\common\Fallout 4\Data"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path $PSScriptRoot -Parent
|
||||
$ba2 = Join-Path $GameData "Fallout4 - Interface.ba2"
|
||||
$extractor = Join-Path $repoRoot "tools\extract_ba2.py"
|
||||
$vanillaOut = Join-Path $repoRoot "Interface\MainMenu.swf.vanilla.bak"
|
||||
$repoSwf = Join-Path $repoRoot "Interface\MainMenu.swf"
|
||||
$deploySwf = Join-Path $GameData "Interface\MainMenu.swf"
|
||||
|
||||
if (-not (Test-Path $ba2)) {
|
||||
Write-Error "Interface BA2 not found: $ba2"
|
||||
}
|
||||
|
||||
python $extractor $ba2 "Interface\MainMenu.swf" $vanillaOut
|
||||
Copy-Item -Force $vanillaOut $repoSwf
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $deploySwf -Parent) | Out-Null
|
||||
Copy-Item -Force $vanillaOut $deploySwf
|
||||
|
||||
Write-Host "Restored vanilla MainMenu.swf to:"
|
||||
Write-Host " $repoSwf"
|
||||
Write-Host " $deploySwf"
|
||||
Write-Host ""
|
||||
Write-Host "Settings -> Gameplay/Display/Audio should work again."
|
||||
Write-Host "Re-add the Multiplayer button with surgical FFDec edits only (see Interface/exported/PATCH_MainMenu_Multiplayer.md)."
|
||||
Reference in New Issue
Block a user