From: Max Vilimpoc Date: Mon, 13 Jul 2026 08:02:46 +0000 (+0200) Subject: Update README.md and script X-Git-Url: https://vilimpoc.org/repos/bl2-split/commitdiff_plain/948cce3bcb1068f479c61d99526fe7ad69bf815f Update README.md and script --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3451bd5 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index b94f4c7..2ce0f74 100644 --- a/README.md +++ b/README.md @@ -1,142 +1,130 @@ -# 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 | |---|---| -| `./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=`, `STAGGER=`. -## How each piece works +## How it works | 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. diff --git a/bl2-splitscreen.sh b/bl2-splitscreen.sh index 8406345..2b1a7ce 100755 --- a/bl2-splitscreen.sh +++ b/bl2-splitscreen.sh @@ -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 -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; } +# --------------------------------------------------------------- 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 @@ -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" + # 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" <