]>
| Commit | Line | Data |
|---|---|---|
| 948cce3b | 1 | # Borderlands 2 — 2-player split-screen / dual-monitor co-op on Linux |
| cb05cf8e | 2 | |
| 948cce3b MV |
3 | Runs **two local instances** of Borderlands 2 that connect to each other over |
| 4 | Goldberg's emulated Steam LAN, so two people can play co-op on one PC — each with | |
| 5 | their own controller, save, and screen. Tested on **Arch + KDE Plasma 6 (Wayland)**. | |
| cb05cf8e | 6 | |
| 948cce3b MV |
7 | - **Two monitors:** one instance fullscreen on each (auto-detected). |
| 8 | - **One monitor:** side-by-side split. | |
| 9 | - Each Xbox controller drives only its own instance. | |
| cb05cf8e | 10 | |
| 948cce3b MV |
11 | > BL2 has no native PC split-screen; this launches two copies networked over LAN. |
| 12 | > Only legitimate because you own the game. No game files or saves are committed here. | |
| cb05cf8e | 13 | |
| 948cce3b | 14 |  |
| cb05cf8e | 15 | |
| 948cce3b | 16 | ## Requirements |
| cb05cf8e | 17 | |
| 948cce3b MV |
18 | - **Borderlands 2 installed via Steam, forced to the Windows build under Proton** |
| 19 | (see setup step 1). The native Linux (Aspyr) build is supported as a fallback but | |
| 20 | is crash-prone with two instances — Proton is the recommended path. | |
| 21 | - Packages: `umu-launcher`, `gamescope` *(optional)*, `qt6-tools` (for `qdbus6`), | |
| 22 | `bubblewrap`, `libarchive` (`bsdtar`), `curl`, `binutils` (`strings`). | |
| 23 | ``` | |
| 24 | sudo pacman -S umu-launcher qt6-tools bubblewrap libarchive curl binutils | |
| 25 | ``` | |
| 320d816d MV |
26 | - *Optional:* `gamemode` (+ `lib32-gamemode`) — the script runs each game under |
| 27 | `gamemoderun` if present (performance CPU governor while playing). Not required. | |
| 948cce3b | 28 | - Two Xbox controllers (any two distinguishable pads). |
| cb05cf8e | 29 | |
| 948cce3b | 30 | ## Setup |
| cb05cf8e | 31 | |
| 948cce3b MV |
32 | 1. **Point Steam at the Windows build.** Steam → *Borderlands 2 → Properties → |
| 33 | Compatibility →* tick **"Force the use of a specific Steam Play compatibility | |
| 34 | tool"** → pick **Proton Experimental** (or a stable Proton). Steam re-downloads | |
| 35 | the Windows depot. (To use the native Linux build instead, skip this — the script | |
| 36 | auto-detects which is installed.) | |
| 37 | ||
| 38 | 2. **Build the two copies:** | |
| 39 | ``` | |
| 40 | cd bl2-split | |
| 41 | ./bl2-splitscreen.sh check # verifies prerequisites; shows detected mode | |
| 42 | ./bl2-splitscreen.sh setup # downloads Goldberg + builds p1/ and p2/ | |
| 43 | ``` | |
| 44 | `setup` auto-downloads the Goldberg emulator (it is **not** committed to this | |
| 45 | repo) and builds two symlink-mirror copies of the game with per-player Steam | |
| 46 | identities. First `run` also makes umu download Proton + build each prefix (slow, | |
| 47 | one time). | |
| cb05cf8e | 48 | |
| 948cce3b MV |
49 | 3. **Play** (turn on both controllers first): |
| 50 | ``` | |
| 51 | ./bl2-splitscreen.sh run | |
| 52 | ``` | |
| 320d816d MV |
53 | Both instances launch at once. While they load, **wiggle the mouse** until both |
| 54 | reach the main menu (the script prints a loud reminder), and leave the windows | |
| 55 | alone — after ~30 s the script moves each onto its screen (fullscreen per monitor | |
| 56 | with two displays, side-by-side with one). In-game: **Player 1** *Play → host over | |
| 57 | LAN*; **Player 2** *Play → Join → pick the LAN game*. `Ctrl-C` kills both. | |
| cb05cf8e MV |
58 | |
| 59 | ## Commands | |
| 60 | ||
| 61 | | Command | Does | | |
| 62 | |---|---| | |
| 948cce3b MV |
63 | | `./bl2-splitscreen.sh fetch` | download the Goldberg emulator into `goldberg/` | |
| 64 | | `./bl2-splitscreen.sh check` | verify prerequisites; print the detected mode | | |
| 65 | | `./bl2-splitscreen.sh setup` | fetch Goldberg + build `p1/`, `p2/` | | |
| 66 | | `./bl2-splitscreen.sh run` | launch both instances + place windows | | |
| 67 | | `./bl2-splitscreen.sh tile` | re-place the two windows | | |
| e02b2f88 MV |
68 | | `./bl2-splitscreen.sh kill` | terminate all running Borderlands 2 instances | |
| 69 | | `./bl2-splitscreen.sh clean` | kill instances, then remove `p1/`, `p2/` (copies + saves + prefixes) | | |
| 948cce3b MV |
70 | |
| 71 | Useful env overrides: `MODE=native|proton`, `DISPLAY_MODE=dual|split`, | |
| 72 | `ISOLATION=bwrap|sdl`, `SPLASH_WAIT=<secs>`, `STAGGER=<secs>`. | |
| cb05cf8e | 73 | |
| 948cce3b | 74 | ## How it works |
| cb05cf8e MV |
75 | |
| 76 | | Concern | Solution | | |
| 77 | |---|---| | |
| 948cce3b MV |
78 | | Two instances at once | Each copy uses **Goldberg** (`steam_api.dll`) instead of real Steam; they connect over emulated LAN | |
| 79 | | Goldberg actually loads | The `.exe` is copied **real** (not symlinked) so Wine loads DLLs from our dir, not the real install's `steam_api.dll` | | |
| 80 | | Two different LAN players | Per-copy `force_account_name.txt` + `force_steamid.txt` | | |
| 81 | | No startup crash | `steam_interfaces.txt` extracted from the original Steam lib per copy | | |
| 82 | | Separate saves | Each instance has its own Proton prefix (`p1/prefix`, `p2/prefix`) | | |
| 83 | | Co-op finds each other | Goldberg `custom_broadcasts.txt` → `127.0.0.1` (broadcast doesn't cross the two Proton containers) | | |
| 84 | | Skips slow "Creating online session" | Goldberg `configs.main.ini` → `offline=1` | | |
| 85 | | Windowed at the right size | `-windowed -ResX -ResY`; **no gamescope** (it hides raw controllers from nested clients) | | |
| 86 | | One controller per instance | **bwrap** masks the other pad's `/dev/input` nodes (the Xbox pads are evdev-only, no hidraw) | | |
| 87 | | Window placement | KWin script via `qdbus6` — one monitor each (dual) or side-by-side (single) | | |
| 88 | | No intro-logo wait | Intro movies disabled in each prefix's `WillowEngine.ini` | | |
| 89 | | No 13 GB duplication | `cp -as` symlink-mirror; only `steam_api.dll` + the `.exe` are real copies | | |
| 90 | ||
| 91 | ## What's in git (and what isn't) | |
| 92 | ||
| 93 | **Committed** (enough for anyone to reproduce): | |
| 94 | ``` | |
| 95 | bl2-splitscreen.sh the launcher/setup script | |
| 96 | README.md this file | |
| 97 | .gitignore | |
| 98 | goldberg/.gitkeep empty dir; setup downloads binaries into it | |
| 99 | p1/.gitkeep p2/.gitkeep | |
| 100 | ``` | |
| cb05cf8e | 101 | |
| 948cce3b MV |
102 | **Never committed** (`.gitignore`): |
| 103 | - `goldberg/steam_api.dll`, `goldberg/libsteam_api.so` — the Goldberg emulator | |
| 104 | binaries. They implement Steam's proprietary API, so they aren't redistributed | |
| 105 | here; `setup` downloads them from the upstream releases (gbe_fork / Mr_Goldberg). | |
| 106 | - `p1/`, `p2/` (except `.gitkeep`) — the game copies, **your save games**, and Wine | |
| 107 | prefixes. Machine-specific and large. | |
| 108 | - `*.log`. | |
| cb05cf8e | 109 | |
| 948cce3b MV |
110 | So another person clones the repo, forces Proton on their own BL2 in Steam, runs |
| 111 | `./bl2-splitscreen.sh setup`, and everything else is pulled/generated locally. | |
| cb05cf8e | 112 | |
| 948cce3b | 113 | ## Troubleshooting |
| cb05cf8e | 114 | |
| 948cce3b MV |
115 | - **Both pads control both screens / keyboard controls both:** the games read input |
| 116 | globally. Controllers are isolated by `bwrap` device-masking (default). Keyboard & | |
| 117 | mouse are *not* isolated — use a controller per player; kb/mouse is only for menus. | |
| 118 | - **A pad drives the wrong screen or none:** ensure `ISOLATION=bwrap` (default). If | |
| 119 | **Steam is running**, its Steam Input virtual pads can interfere — quit Steam. | |
| 120 | - **Stuck on "Creating online session":** should be quick with `offline=1` (set by | |
| 121 | setup). If a co-op *join* ever breaks, remove the offline setting: | |
| 122 | `rm p{1,2}/game/Binaries/Win32/steam_settings/configs.main.ini` | |
| 123 | - **They don't see each other on LAN:** Player 1 hosts first (the script staggers | |
| 124 | launches). Discovery uses `custom_broadcasts.txt` (localhost). | |
| 320d816d MV |
125 | - **The 2nd instance is visible but stuck on the loading screen — and only advances |
| 126 | when you jiggle the mouse.** Root cause: BL2's `bPauseOnLossOfFocus=TRUE` — the | |
| 127 | game **pauses itself whenever its window isn't focused**. In split-screen only one | |
| 128 | window is focused at a time, so the unfocused instance freezes until an input event | |
| 129 | wakes it. `setup`/`run` patch each prefix's `WillowEngine.ini` to | |
| 130 | `bPauseOnLossOfFocus=FALSE` so every instance keeps running unfocused. (This was | |
| 131 | *not* CPU scaling, entropy, or window occlusion — those were dead ends.) The | |
| 132 | instances also share a DXVK shader cache so the 2nd reuses the 1st's compiled | |
| 133 | shaders; `STAGGER=45` further separates their loads if a cold first run is slow. | |
| 134 | - **Two soundtracks at once:** each instance sets `bMuteAudioWhenNotInFocus=TRUE`, so | |
| 135 | only the focused window plays audio — the sound follows whichever instance you're | |
| 136 | focused on, instead of both playing together. | |
| 5765b872 MV |
137 | - **In-game "Quit Borderlands 2 → Yes" does nothing / hangs:** the game's shutdown |
| 138 | path stalls under Goldberg (Steam-networking cleanup, worse with offline mode). | |
| 139 | Don't use in-game Quit — close with **`./bl2-splitscreen.sh kill`** or **Ctrl-C** | |
| 140 | in the run terminal (safe: you're at the menu, nothing is saving). | |
| e02b2f88 MV |
141 | - **Ctrl-C / kill left something running:** `./bl2-splitscreen.sh kill` tears down |
| 142 | the whole game + Proton/Wine tree. | |
| 948cce3b MV |
143 | - **A window won't sit on its monitor:** re-run `./bl2-splitscreen.sh tile`; raise |
| 144 | `SPLASH_WAIT` if it fires before the game settles. | |
| 145 | - **Reset everything:** `./bl2-splitscreen.sh clean` (removes copies + saves; the | |
| 146 | real Steam install is untouched). | |
| 147 | ||
| 148 | ## Native (Aspyr Linux) fallback | |
| 149 | ||
| 150 | If Steam has the native Linux build installed, the script auto-detects `native` mode: | |
| 151 | it uses the **classic Mr_Goldberg** `.so` (gbe_fork crashes that old binary), runs | |
| 152 | inside the Steam scout runtime, and isolates controllers the same way. It works, but | |
| 153 | the second instance is prone to crashing — prefer the Proton path. |