]> vilimpoc.org git repositories - bl2-split/commitdiff
Update README.md and script
authorMax Vilimpoc <max@vilimpoc.org>
Mon, 13 Jul 2026 08:02:46 +0000 (10:02 +0200)
committerMax Vilimpoc <max@vilimpoc.org>
Mon, 13 Jul 2026 08:02:46 +0000 (10:02 +0200)
.gitignore [new file with mode: 0644]
README.md
bl2-splitscreen.sh
sxs.jpg [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..3451bd5
--- /dev/null
@@ -0,0 +1,14 @@
+# Goldberg emulator binaries — NOT redistributed here (they emulate Steam's
+# proprietary API). `./bl2-splitscreen.sh setup` downloads them into goldberg/.
+/goldberg/*
+!/goldberg/.gitkeep
+
+# Per-player game copies, SAVE GAMES, and Wine/Proton prefixes — generated locally
+# by `setup`/`run`, specific to each machine, and large. Never commit.
+/p1/*
+/p2/*
+!/p1/.gitkeep
+!/p2/.gitkeep
+
+# Logs and scratch
+*.log
index b94f4c737126366cce69a76b929620e8fb779982..2ce0f741e99973db8626c1348bce0c570f178a38 100644 (file)
--- a/README.md
+++ b/README.md
-# Borderlands 2 — 2-player split screen (Linux / KDE)
+# Borderlands 2 — 2-player split-screen / dual-monitor co-op on Linux
 
 
-Borderlands 2 has **no built-in PC split screen**. This runs **two copies** of the
-game side-by-side that connect over Goldberg's emulated Steam LAN — each with its
-own save, its own identity, and its own controller — auto-tiled on KDE (KWin).
+Runs **two local instances** of Borderlands 2 that connect to each other over
+Goldberg's emulated Steam LAN, so two people can play co-op on one PC — each with
+their own controller, save, and screen. Tested on **Arch + KDE Plasma 6 (Wayland)**.
 
 
-**Status: working & validated on this machine.** Both instances start, initialize
-Steamworks + engine, and run simultaneously. Goldberg is already downloaded and
-staged; `setup` has been run. You just need both controllers on, then `run`.
+- **Two monitors:** one instance fullscreen on each (auto-detected).
+- **One monitor:** side-by-side split.
+- Each Xbox controller drives only its own instance.
 
 
-## What this install is, and why the setup looks the way it does
+> BL2 has no native PC split-screen; this launches two copies networked over LAN.
+> Only legitimate because you own the game. No game files or saves are committed here.
 
 
-Your BL2 is the **native Aspyr Linux build** (a 2012, 32-bit `Borderlands2` ELF) —
-not the Windows/Proton version. Getting split screen out of it took solving three
-real problems, all handled by the script:
+![Two Borderlands 2 instances running co-op across two monitors, each on its own main menu in the shared LAN lobby with per-player Xbox controller prompts](sxs.jpg)
 
 
-1. **The binary is too old for modern system libs** → it's launched inside the
-   **Steam scout runtime** (`ubuntu12_32/steam-runtime/run.sh`), which ships the
-   old libraries it expects.
-2. **gbe_fork (the modern Goldberg) segfaults this binary** → we use the
-   **classic Mr_Goldberg 0.2.5** emulator instead (`goldberg/libsteam_api.so`).
-   gbe_fork's experimental *and* regular builds both crash it at a fixed address
-   right after Steam init; classic goldberg works.
-3. **The old binary crashes if the emu returns unexpected Steam interfaces** → at
-   setup we extract the exact interface versions from the game's original
-   `libsteam_api.so` into each copy's `steam_settings/steam_interfaces.txt`.
+## Requirements
 
 
-A Windows **proton** mode is also supported (see below); the script auto-detects
-which build is installed.
+- **Borderlands 2 installed via Steam, forced to the Windows build under Proton**
+  (see setup step 1). The native Linux (Aspyr) build is supported as a fallback but
+  is crash-prone with two instances — Proton is the recommended path.
+- Packages: `umu-launcher`, `gamescope` *(optional)*, `qt6-tools` (for `qdbus6`),
+  `bubblewrap`, `libarchive` (`bsdtar`), `curl`, `binutils` (`strings`).
+  ```
+  sudo pacman -S umu-launcher qt6-tools bubblewrap libarchive curl binutils
+  ```
+- Two Xbox controllers (any two distinguishable pads).
 
 
-## Playing (native — your current install)
+## Setup
 
 
-```
-cd /home/max/Games/bl2-split
-# 1. QUIT STEAM COMPLETELY (see why below)
-# 2. turn on BOTH controllers (the wireless 360 pad idle-sleeps — wake it)
-./bl2-splitscreen.sh run
-```
+1. **Point Steam at the Windows build.** Steam → *Borderlands 2 → Properties →
+   Compatibility →* tick **"Force the use of a specific Steam Play compatibility
+   tool"** → pick **Proton Experimental** (or a stable Proton). Steam re-downloads
+   the Windows depot. (To use the native Linux build instead, skip this — the script
+   auto-detects which is installed.)
+
+2. **Build the two copies:**
+   ```
+   cd bl2-split
+   ./bl2-splitscreen.sh check     # verifies prerequisites; shows detected mode
+   ./bl2-splitscreen.sh setup     # downloads Goldberg + builds p1/ and p2/
+   ```
+   `setup` auto-downloads the Goldberg emulator (it is **not** committed to this
+   repo) and builds two symlink-mirror copies of the game with per-player Steam
+   identities. First `run` also makes umu download Proton + build each prefix (slow,
+   one time).
 
 
-> **Quit Steam first.** While Steam runs, Steam Input creates *virtual* gamepads
-> that the game grabs instead of the real pads — which breaks per-player controller
-> isolation (and can leave input not working). Goldberg replaces Steam, so you don't
-> need it running. The script warns and pauses if it detects Steam.
-
-- Two game windows open (no gamescope). During the intro logos they look
-  unaligned — that's expected; **~45 s in (after the splash) they snap to their
-  halves.** The delay is because the game resizes itself during the splash, so
-  tiling earlier just gets overridden.
-- The split is computed in **logical** pixels (your 2560×1440 panel at 125% scale
-  = 2048×1152 logical → two 1024×1152 halves), so it lands right despite scaling.
-- **Player 1** (Xbox Series S|X pad): *Play → host over LAN*.
-- **Player 2** (Xbox 360 wireless pad): *Play → Join → pick the LAN game*.
-- Each window is pinned to one controller and, thanks to SDL background events,
-  responds even when the other window has focus.
-- Re-tile any time (e.g. after both reach the menu): `./bl2-splitscreen.sh tile`.
-- Snapping too early (still on the splash)? Raise the wait: `SPLASH_WAIT=70 ./bl2-splitscreen.sh run`.
-- `Ctrl-C` in the terminal kills both instances.
-
-If you change hardware or want to rebuild the copies: `./bl2-splitscreen.sh setup`.
+3. **Play** (turn on both controllers first):
+   ```
+   ./bl2-splitscreen.sh run
+   ```
+   With two monitors each game opens fullscreen on its own display; with one, they
+   tile side-by-side. In-game: **Player 1** *Play → host over LAN*; **Player 2**
+   *Play → Join → pick the LAN game*. `Ctrl-C` kills both.
 
 ## Commands
 
 | Command | Does |
 |---|---|
 
 ## Commands
 
 | Command | Does |
 |---|---|
-| `./bl2-splitscreen.sh check` | verify prerequisites for the detected mode |
-| `./bl2-splitscreen.sh setup` | build the two patched game copies (already done) |
-| `./bl2-splitscreen.sh run`   | launch both halves + tile |
-| `./bl2-splitscreen.sh tile`  | (re)tile the two windows on KDE |
-| `./bl2-splitscreen.sh clean` | remove generated copies/saves (real game untouched) |
+| `./bl2-splitscreen.sh fetch` | download the Goldberg emulator into `goldberg/` |
+| `./bl2-splitscreen.sh check` | verify prerequisites; print the detected mode |
+| `./bl2-splitscreen.sh setup` | fetch Goldberg + build `p1/`, `p2/` |
+| `./bl2-splitscreen.sh run`   | launch both instances + place windows |
+| `./bl2-splitscreen.sh tile`  | re-place the two windows |
+| `./bl2-splitscreen.sh clean` | remove `p1/`, `p2/` (copies + saves + prefixes) |
+
+Useful env overrides: `MODE=native|proton`, `DISPLAY_MODE=dual|split`,
+`ISOLATION=bwrap|sdl`, `SPLASH_WAIT=<secs>`, `STAGGER=<secs>`.
 
 
-## How each piece works
+## How it works
 
 | Concern | Solution |
 |---|---|
 
 | Concern | Solution |
 |---|---|
-| Two instances at once | Each copy uses classic Mr_Goldberg (no Steam needed); they LAN together |
-| Runs the ancient binary | Launched via the Steam scout runtime |
-| No crash on Steam init | `steam_interfaces.txt` extracted from the original lib, per copy |
-| Separate saves | Per-player `HOME` (`p1/home`, `p2/home`) — also isolates each `~/.steam` |
-| Two different LAN players | `force_account_name.txt` + `force_steamid.txt` per copy |
-| Half-screen windows | Game runs windowed at the **logical** half size (`-ResX/-ResY` + config), **no gamescope** — gamescope doesn't pass raw controllers to nested clients, so the SDL game would see no pad |
-| Correct size on a scaled display | Split computed from KDE's **logical** screen size (physical ÷ scale), auto-detected via `kscreen-doctor`. Using physical px tiles at the wrong fraction (2/3 + 1/3 at 125%) |
-| Both windows get input | `SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1` (SDL otherwise ignores the pad of an unfocused window) |
-| One controller per instance | `bwrap` masks the other pad's `/dev/input` nodes — reliable only with Steam quit (else Steam Input's virtual pads leak through) |
-| Side-by-side placement | KWin script (`qdbus6`) sizes each window to a logical half + removes borders, fired **after** the splash so the game has stopped resizing itself |
-| No 13 GB duplication | `cp -as` symlink-mirror; only the Steam API lib is a real copy |
-
-> The game statically links an **old SDL2** (pre-2.0.9), so it reads controllers via
-> SDL/udev, needs `ALLOW_BACKGROUND_EVENTS` for split focus, and can't use SDL's
-> newer per-VID/PID filtering — hence bwrap for isolation.
-
-## Switching to the Windows / Proton version (optional)
-
-If the native port misbehaves, the Windows build under Proton is an alternative:
-
-1. Steam → **Borderlands 2 → Properties → Compatibility →** *Force a specific
-   Steam Play compatibility tool* → e.g. Proton Experimental.
-2. Steam re-downloads the **Windows** depot (`Borderlands2.exe`).
-3. Re-run the script — it auto-detects **proton** mode and uses the already-staged
-   Windows `goldberg/steam_api.dll` (gbe_fork) via `umu-run`:
-   ```
-   ./bl2-splitscreen.sh check    # mode: proton
-   ./bl2-splitscreen.sh setup && ./bl2-splitscreen.sh run
-   ```
-   First launch downloads Proton + builds each Wine prefix (slow, one time).
+| Two instances at once | Each copy uses **Goldberg** (`steam_api.dll`) instead of real Steam; they connect over emulated LAN |
+| 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` |
+| Two different LAN players | Per-copy `force_account_name.txt` + `force_steamid.txt` |
+| No startup crash | `steam_interfaces.txt` extracted from the original Steam lib per copy |
+| Separate saves | Each instance has its own Proton prefix (`p1/prefix`, `p2/prefix`) |
+| Co-op finds each other | Goldberg `custom_broadcasts.txt` → `127.0.0.1` (broadcast doesn't cross the two Proton containers) |
+| Skips slow "Creating online session" | Goldberg `configs.main.ini` → `offline=1` |
+| Windowed at the right size | `-windowed -ResX -ResY`; **no gamescope** (it hides raw controllers from nested clients) |
+| One controller per instance | **bwrap** masks the other pad's `/dev/input` nodes (the Xbox pads are evdev-only, no hidraw) |
+| Window placement | KWin script via `qdbus6` — one monitor each (dual) or side-by-side (single) |
+| No intro-logo wait | Intro movies disabled in each prefix's `WillowEngine.ini` |
+| No 13 GB duplication | `cp -as` symlink-mirror; only `steam_api.dll` + the `.exe` are real copies |
+
+## What's in git (and what isn't)
+
+**Committed** (enough for anyone to reproduce):
+```
+bl2-splitscreen.sh     the launcher/setup script
+README.md              this file
+.gitignore
+goldberg/.gitkeep      empty dir; setup downloads binaries into it
+p1/.gitkeep p2/.gitkeep
+```
 
 
-> Native and Windows builds share the same folder, so only one is installed at a
-> time. Switching modes = switching what Steam has downloaded.
+**Never committed** (`.gitignore`):
+- `goldberg/steam_api.dll`, `goldberg/libsteam_api.so` — the Goldberg emulator
+  binaries. They implement Steam's proprietary API, so they aren't redistributed
+  here; `setup` downloads them from the upstream releases (gbe_fork / Mr_Goldberg).
+- `p1/`, `p2/` (except `.gitkeep`) — the game copies, **your save games**, and Wine
+  prefixes. Machine-specific and large.
+- `*.log`.
 
 
-## Troubleshooting
+So another person clones the repo, forces Proton on their own BL2 in Steam, runs
+`./bl2-splitscreen.sh setup`, and everything else is pulled/generated locally.
 
 
-- **They don't see each other on LAN:** let Player 1 host first (the script starts
-  it ~10 s earlier). Both use classic goldberg's LAN broadcast; each player's own
-  `~/.steam`/config is isolated via the per-player HOME. If needed, check each
-  copy's `steam_settings/`.
-- **Controllers don't work at all:** make sure **Steam is fully quit** before `run`
-  (Steam Input's virtual pads block the real ones). Native runs the game *without*
-  gamescope for this reason — gamescope hides raw controllers from nested clients.
-- **Both pads control both screens / wrong window got a controller:** almost always
-  Steam still running (virtual pads bypass the `bwrap` mask). Quit Steam. Isolation
-  is `ISOLATION=bwrap` (default); masking real pad nodes only works with Steam off.
-- **Halves look uneven (one fills ~2/3, the other ~1/3):** display-scaling mismatch
-  — the script now auto-detects the *logical* screen size (`kscreen-doctor`), but if
-  detection fails set it explicitly, e.g. `SCREEN_W=2048 SCREEN_H=1152 ./bl2-splitscreen.sh run`.
-- **Windows tile while still on the splash / too early:** raise `SPLASH_WAIT`
-  (seconds), e.g. `SPLASH_WAIT=70 ./bl2-splitscreen.sh run`, or just re-run
-  `./bl2-splitscreen.sh tile` once both are at the menu.
-- **A game crashes on launch:** confirm `./bl2-splitscreen.sh check` is all green;
-  the native path needs the scout runtime + classic goldberg + `steam_interfaces.txt`
-  (all set up by `setup`).
-- **Reset everything:** `./bl2-splitscreen.sh clean`.
-
-## Files
+## Troubleshooting
 
 
-```
-bl2-split/
-├── bl2-splitscreen.sh          the launcher
-├── goldberg/
-│   ├── libsteam_api.so         classic Mr_Goldberg 0.2.5, 32-bit (native mode)
-│   └── steam_api.dll           gbe_fork, Windows 32-bit (proton mode)
-├── p1/ , p2/                   per-player: game/ (symlink mirror) + home/ (saves)
-└── README.md
-```
+- **Both pads control both screens / keyboard controls both:** the games read input
+  globally. Controllers are isolated by `bwrap` device-masking (default). Keyboard &
+  mouse are *not* isolated — use a controller per player; kb/mouse is only for menus.
+- **A pad drives the wrong screen or none:** ensure `ISOLATION=bwrap` (default). If
+  **Steam is running**, its Steam Input virtual pads can interfere — quit Steam.
+- **Stuck on "Creating online session":** should be quick with `offline=1` (set by
+  setup). If a co-op *join* ever breaks, remove the offline setting:
+  `rm p{1,2}/game/Binaries/Win32/steam_settings/configs.main.ini`
+- **They don't see each other on LAN:** Player 1 hosts first (the script staggers
+  launches). Discovery uses `custom_broadcasts.txt` (localhost).
+- **A window won't sit on its monitor:** re-run `./bl2-splitscreen.sh tile`; raise
+  `SPLASH_WAIT` if it fires before the game settles.
+- **Reset everything:** `./bl2-splitscreen.sh clean` (removes copies + saves; the
+  real Steam install is untouched).
+
+## Native (Aspyr Linux) fallback
+
+If Steam has the native Linux build installed, the script auto-detects `native` mode:
+it uses the **classic Mr_Goldberg** `.so` (gbe_fork crashes that old binary), runs
+inside the Steam scout runtime, and isolates controllers the same way. It works, but
+the second instance is prone to crashing — prefer the Proton path.
index 8406345b6a261f8398b3bbcc14e02e5f8e0bc7de..2b1a7ceb875ba804b916a69a1cf31a3f10003d66 100755 (executable)
@@ -107,11 +107,47 @@ detect_mode() {
 MODE="$(detect_mode)"
 [ "$MODE" = unknown ] && die "Cannot find BL2 - neither $NATIVE_BIN nor $WIN_EXE_REL under $GAME_DIR"
 if [ -z "$ISOLATION" ]; then ISOLATION=bwrap; fi   # device-masking works for both modes
 MODE="$(detect_mode)"
 [ "$MODE" = unknown ] && die "Cannot find BL2 - neither $NATIVE_BIN nor $WIN_EXE_REL under $GAME_DIR"
 if [ -z "$ISOLATION" ]; then ISOLATION=bwrap; fi   # device-masking works for both modes
-GOLDBERG_SO="$BASE/goldberg/libsteam_api.so"    # native
-GOLDBERG_DLL="$BASE/goldberg/steam_api.dll"     # proton
+GOLDBERG_SO="$BASE/goldberg/libsteam_api.so"    # native (classic Mr_Goldberg)
+GOLDBERG_DLL="$BASE/goldberg/steam_api.dll"     # proton (gbe_fork)
+
+# Goldberg emulator downloads (NOT committed to git - fetched by `fetch`). Pinned
+# to known-good releases; override via env if these ever move.
+GBE_FORK_TAG="${GBE_FORK_TAG:-release-2026_05_30}"
+GBE_FORK_WIN_URL="${GBE_FORK_WIN_URL:-https://github.com/Detanup01/gbe_fork/releases/download/$GBE_FORK_TAG/emu-win-release.7z}"
+CLASSIC_GOLDBERG_URL="${CLASSIC_GOLDBERG_URL:-https://gitlab.com/Mr_Goldberg/goldberg_emulator/uploads/2524331e488ec6399c396cf48bbe9903/Goldberg_Lan_Steam_Emu_v0.2.5.zip}"
 
 resolve_pad() { local m; for m in $1; do [ -e "$m" ] && { readlink -f "$m"; return; }; done; }
 
 
 resolve_pad() { local m; for m in $1; do [ -e "$m" ] && { readlink -f "$m"; return; }; done; }
 
+# --------------------------------------------------------------- fetch deps -
+# Download the Goldberg emulator binaries (kept out of git). Proton needs the
+# gbe_fork Windows steam_api.dll; native needs the classic Mr_Goldberg Linux .so.
+cmd_fetch() {  # downloads the Goldberg binary the detected MODE needs
+  command -v bsdtar >/dev/null || die "bsdtar missing (sudo pacman -S libarchive)"
+  command -v curl   >/dev/null || die "curl missing (sudo pacman -S curl)"
+  mkdir -p "$BASE/goldberg"
+  local tmp; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN
+
+  if [ "$MODE" = proton ]; then
+    if [ -f "$GOLDBERG_DLL" ]; then ok "gbe_fork steam_api.dll already present"; return; fi
+    say "Downloading gbe_fork (Windows) $GBE_FORK_TAG ..."
+    curl -fsSL -o "$tmp/win.7z" "$GBE_FORK_WIN_URL" || die "download failed: $GBE_FORK_WIN_URL"
+    bsdtar -xf "$tmp/win.7z" -C "$tmp" release/experimental/x86/steam_api.dll \
+      || die "could not extract steam_api.dll from the archive"
+    cp "$tmp/release/experimental/x86/steam_api.dll" "$GOLDBERG_DLL"
+    file "$GOLDBERG_DLL" | grep -qi "PE32 .*i386" && ok "steam_api.dll (proton) fetched" \
+      || die "fetched steam_api.dll is not the expected 32-bit PE"
+  else
+    if [ -f "$GOLDBERG_SO" ]; then ok "classic goldberg libsteam_api.so already present"; return; fi
+    say "Downloading classic Mr_Goldberg (Linux) ..."
+    curl -fsSL -o "$tmp/lin.zip" "$CLASSIC_GOLDBERG_URL" || die "download failed: $CLASSIC_GOLDBERG_URL"
+    bsdtar -xf "$tmp/lin.zip" -C "$tmp" linux/x86/libsteam_api.so \
+      || die "could not extract libsteam_api.so from the archive"
+    cp "$tmp/linux/x86/libsteam_api.so" "$GOLDBERG_SO"
+    file "$GOLDBERG_SO" | grep -q "ELF 32-bit" && ok "libsteam_api.so (native) fetched" \
+      || die "fetched libsteam_api.so is not the expected 32-bit ELF"
+  fi
+}
+
 # ------------------------------------------------------------------ check ---
 cmd_check() {
   local fail=0
 # ------------------------------------------------------------------ check ---
 cmd_check() {
   local fail=0
@@ -175,6 +211,13 @@ EOF
   # other via UDP broadcast, so co-op hangs at "Creating online session". Tell the
   # emu to send discovery directly to localhost so they always find each other.
   printf '127.0.0.1\n' > "$ss/custom_broadcasts.txt"
   # other via UDP broadcast, so co-op hangs at "Creating online session". Tell the
   # emu to send discovery directly to localhost so they always find each other.
   printf '127.0.0.1\n' > "$ss/custom_broadcasts.txt"
+  # BL2 stalls on "Creating online session" waiting on (unreachable) online Steam
+  # servers; offline mode makes the emu report Steam offline so it skips that wait
+  # and goes to LAN. LAN co-op still works. Remove this file if it ever breaks join.
+  cat > "$ss/configs.main.ini" <<EOF
+[main::connectivity]
+offline=1
+EOF
 }
 
 # Old games hard-crash if the emu returns interfaces they don't expect. Extract
 }
 
 # Old games hard-crash if the emu returns interfaces they don't expect. Extract
@@ -215,6 +258,7 @@ build_player() {  # $1=n  $2=account  $3=steamid
 }
 
 cmd_setup() {
 }
 
 cmd_setup() {
+  cmd_fetch                                     # download Goldberg if not present
   REQUIRE_PADS=0 cmd_check || die "check failed - resolve the above before setup"
   say ""; say "== Building player copies ($MODE) =="
   build_player 1 "Player1" "76561197960287930"
   REQUIRE_PADS=0 cmd_check || die "check failed - resolve the above before setup"
   say ""; say "== Building player copies ($MODE) =="
   build_player 1 "Player1" "76561197960287930"
@@ -398,10 +442,11 @@ cmd_run() {
 cmd_clean() { say "Removing generated copies/saves (real game untouched)..."; rm -rf "$BASE/p1" "$BASE/p2"; ok "Done."; }
 
 case "${1:-}" in
 cmd_clean() { say "Removing generated copies/saves (real game untouched)..."; rm -rf "$BASE/p1" "$BASE/p2"; ok "Done."; }
 
 case "${1:-}" in
+  fetch) cmd_fetch ;;
   check) cmd_check ;;
   setup) cmd_setup ;;
   run)   cmd_run ;;
   tile)  cmd_tile ;;
   clean) cmd_clean ;;
   check) cmd_check ;;
   setup) cmd_setup ;;
   run)   cmd_run ;;
   tile)  cmd_tile ;;
   clean) cmd_clean ;;
-  *) say "Usage: $0 {check|setup|run|tile|clean}   (MODE=native|proton to force)"; exit 1 ;;
+  *) say "Usage: $0 {fetch|check|setup|run|tile|clean}   (MODE=native|proton to force)"; exit 1 ;;
 esac
 esac
diff --git a/sxs.jpg b/sxs.jpg
new file mode 100644 (file)
index 0000000..946da89
Binary files /dev/null and b/sxs.jpg differ