# experimental and regular) segfaults this binary; classic goldberg works.
NATIVE_RUNTIME="${NATIVE_RUNTIME:-/home/max/.steam/steam/ubuntu12_32/steam-runtime/run.sh}"
-# Seconds to wait before auto-tiling, i.e. how long the intro logos/movies take to
-# clear on your machine. Bump it if the windows snap into place too early (while
-# still on the splash). You can always re-run `./bl2-splitscreen.sh tile`.
-SPLASH_WAIT="${SPLASH_WAIT:-45}"
-
-# Seconds to wait after launching Player 1 before launching Player 2. Giving P1
-# time to reach the menu and start hosting reduces a co-op-startup race that can
-# crash the second (fragile Aspyr) instance.
-STAGGER="${STAGGER:-25}"
+# Seconds after launch before the windows are moved onto their screens (long enough
+# for both to load past the resize-y splash). You can always re-run `.../tile`.
+SPLASH_WAIT="${SPLASH_WAIT:-30}"
+
+# Seconds between launching Player 1 and Player 2. 0 = both at once (this machine is
+# fast enough). Native mode can be crash-prone launching both together - set e.g.
+# STAGGER=25 there if the 2nd instance dies on startup.
+STAGGER="${STAGGER:-0}"
# ===========================================================================
c_red=$'\e[31m'; c_grn=$'\e[32m'; c_yel=$'\e[33m'; c_dim=$'\e[2m'; c_rst=$'\e[0m'
ok() { printf '%s✓%s %s\n' "$c_grn" "$c_rst" "$*"; }
warn() { printf '%s!%s %s\n' "$c_yel" "$c_rst" "$*"; }
die() { printf '%s✗ %s%s\n' "$c_red" "$*" "$c_rst" >&2; exit 1; }
+# Loud, unmissable, bold-bright-yellow banner. Each arg is one line (keep it CAPS).
+banner() {
+ local b=$'\e[1;93m' r=$'\e[0m' bar="############################################################" l
+ printf '\n%s%s%s\n' "$b" "$bar" "$r"
+ for l in "$@"; do printf '%s## %-54s##%s\n' "$b" "$l" "$r"; done
+ printf '%s%s%s\n\n' "$b" "$bar" "$r"
+}
# -------------------------------------------------------- mode detection ---
WIN_EXE_REL="Binaries/Win32/Borderlands2.exe"
}' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
}
-# Proton keeps its config inside the Wine prefix; drop the intro movies there so
-# the game goes straight to the menu (resolution is handled by -ResX/-ResY args).
+# Proton keeps its config inside the Wine prefix. We patch WillowEngine.ini to:
+# - drop the intro movies (straight to menu; resolution is set by -ResX/-ResY);
+# - always keep RUNNING when unfocused (bPauseOnLossOfFocus=FALSE) - split-screen
+# focuses only one window at a time, and the default TRUE freezes the unfocused
+# instance (its load stalls until you nudge the mouse);
+# - mute when unfocused (bMuteAudioWhenNotInFocus=TRUE) so audio follows the focused
+# window instead of every instance playing at once.
patch_proton_config() { # $1 = prefix dir
local f
f=$(find "$1/drive_c/users" -ipath "*Borderlands 2*Config/WillowEngine.ini" 2>/dev/null | head -1)
/^\[/ { sec=$0 }
{ l=$0
if (sec=="[FullScreenMovie]") { if (l ~ /^StartupMovies=/) next; else if (l ~ /^bForceNoMovies=/) l="bForceNoMovies=TRUE" }
+ else if (sec=="[Engine.Engine]") {
+ if (l ~ /^bPauseOnLossOfFocus=/) l="bPauseOnLossOfFocus=FALSE"
+ else if (l ~ /^bMuteAudioWhenNotInFocus=/) l="bMuteAudioWhenNotInFocus=TRUE"
+ }
printf "%s%s\n", l, cr }' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
}
local n="$1" pad="$2" vidpid="$3" pg="$BASE/p$1/game" wd
local rw="${GW[$n]}" rh="${GH[$n]}" # this player's render size
local -a env cmd
+ # Run under gamemode if installed: it flips the CPU to the performance governor
+ # (system-wide) while playing, so instances load at full speed instead of stalling
+ # on a power-saving governor until you jiggle the mouse. No-op if not installed.
+ local -a gm=(); command -v gamemoderun >/dev/null && gm=(gamemoderun)
# No gamescope in either mode: it doesn't pass raw controllers to nested clients,
# so the game sees no pad. Run the game windowed and let KWin place it.
if [ "$MODE" = native ]; then
"LD_LIBRARY_PATH=$pg:${LD_LIBRARY_PATH:-}"
"SteamAppId=$APPID" "SteamGameId=$APPID"
"SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1" )
- cmd=( "$NATIVE_RUNTIME" "./$NATIVE_BIN" -windowed "-ResX=$rw" "-ResY=$rh" )
+ cmd=( "${gm[@]}" "$NATIVE_RUNTIME" "./$NATIVE_BIN" -windowed "-ResX=$rw" "-ResY=$rh" )
else
wd="$pg/Binaries/Win32"
- patch_proton_config "$BASE/p$n/prefix" # skip intro movies (if prefix exists)
- env=( "WINEPREFIX=$BASE/p$n/prefix" "GAMEID=0" "STORE=none" )
+ patch_proton_config "$BASE/p$n/prefix" # movies off, no focus-pause, mute-when-unfocused
+ mkdir -p "$BASE/dxvk-cache"
+ env=( "WINEPREFIX=$BASE/p$n/prefix" "GAMEID=0" "STORE=none"
+ "UMU_RUNTIME_UPDATE=0" # skip umu's startup update check
+ # Shared D3D shader cache: the 2nd instance reuses shaders the 1st already
+ # compiled instead of recompiling them while the 1st is using the GPU.
+ "DXVK_STATE_CACHE_PATH=$BASE/dxvk-cache" )
# Only use the SDL VID/PID filter in "sdl" mode. With bwrap masking it is
# redundant AND can over-filter (it wrongly dropped the 360 pad), so skip it.
[ "$ISOLATION" = sdl ] && env+=( "SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT=$vidpid" )
[ -n "$PROTONPATH" ] && env+=( "PROTONPATH=$PROTONPATH" )
- cmd=( umu-run "$pg/$WIN_EXE_REL" -windowed "-ResX=$rw" "-ResY=$rh" )
+ cmd=( "${gm[@]}" umu-run "$pg/$WIN_EXE_REL" -windowed "-ResX=$rw" "-ResY=$rh" )
fi
# bwrap (if used) masks the other pad's /dev/input nodes for this instance.
say "${c_dim}P1 ${GW[1]}x${GH[1]}@${GX[1]},${GY[1]} P2 ${GW[2]}x${GH[2]}@${GX[2]},${GY[2]}${c_rst}"
local pid1 pid2
launch_player 1 "$p1" "$P1_VIDPID"; pid1=$LAUNCHED_PID
- sleep "$STAGGER"
+ [ "$STAGGER" -gt 0 ] 2>/dev/null && sleep "$STAGGER"
launch_player 2 "$p2" "$P2_VIDPID"; pid2=$LAUNCHED_PID
- # Place windows AFTER the splash (the game resizes itself during it). Fire a few
- # times so both instances get caught once settled.
+ local where="THEIR MONITORS"; [ "$DISPLAY_MODE" = split ] && where="THEIR HALVES"
+ banner "WIGGLE YOUR MOUSE NOW -- KEEP IT MOVING" \
+ "UNTIL BOTH GAMES REACH THE MAIN MENU." \
+ "" \
+ "DO NOT TOUCH THE WINDOWS: I WILL MOVE THEM" \
+ "ONTO $where IN $SPLASH_WAIT SECONDS."
+
+ # Move windows AFTER the splash - the game resizes itself during it, so tiling too
+ # early makes the window flicker. Fire a few times so both settle.
( sleep "$SPLASH_WAIT"; cmd_tile >/dev/null 2>&1
- sleep 10; cmd_tile >/dev/null 2>&1
- sleep 15; cmd_tile >/dev/null 2>&1 ) &
+ sleep 8; cmd_tile >/dev/null 2>&1
+ sleep 12; cmd_tile >/dev/null 2>&1 ) &
- say ""
ok "Both instances launching (pids $pid1 / $pid2)."
say "In-game: ${c_grn}Player 1${c_rst} → Play → host over LAN. ${c_grn}Player 2${c_rst} → Play → Join → pick the LAN game."
- local where="their monitors"; [ "$DISPLAY_MODE" = split ] && where="their halves"
- say "${c_dim}Windows settle onto $where ~${SPLASH_WAIT}s in (after the splash)."
- say "Re-place any time with: ./bl2-splitscreen.sh tile${c_rst}"
- say ""; say "Press Ctrl-C to kill both instances."
- trap 'kill "$pid1" "$pid2" 2>/dev/null' INT TERM
+ say "${c_dim}Re-place windows any time with: ./bl2-splitscreen.sh tile${c_rst}"
+ say "Press Ctrl-C to kill both instances."
+ # Killing $pid1/$pid2 only hits the top of each tree; the game runs deeper inside
+ # umu/pressure-vessel and survives. kill_instances tears down the whole tree.
+ trap 'echo; kill_instances; exit 130' INT TERM
wait "$pid1" "$pid2"
}
+# ------------------------------------------------------------------ kill ----
+# Terminate every Borderlands 2 process we launched (game exe + umu/Proton/Wine
+# tree for our prefixes), SIGTERM first then SIGKILL. Scoped to our BASE dir so it
+# won't touch unrelated Wine apps.
+kill_instances() {
+ local sig
+ for sig in TERM KILL; do
+ pkill -"$sig" -f 'Borderlands2\.exe' 2>/dev/null # proton game
+ pkill -"$sig" -x 'Borderlands2' 2>/dev/null # native game
+ pkill -"$sig" -f "$BASE/p[12]/" 2>/dev/null # umu/proton/wine for our copies+prefixes
+ [ "$sig" = TERM ] && sleep 2
+ done
+}
+
+cmd_kill() {
+ # Match the process NAME (comm) as a substring, not the full cmdline - otherwise
+ # helper shells that merely have the game path in their args get counted. This
+ # matches "Borderlands2.ex" (Proton, 15-char truncated) and "Borderlands2".
+ pgrep 'Borderlands2' >/dev/null || { ok "No Borderlands 2 instances running."; return 0; }
+ say "Killing Borderlands 2 instances..."
+ kill_instances
+ # pgrep -c prints "0" and exits non-zero on no match, so DON'T add `|| echo 0`
+ # (that would double it). Command substitution keeps the "0" regardless of exit.
+ local left; left=$(pgrep -c 'Borderlands2' 2>/dev/null); left=${left:-0}
+ [ "$left" = 0 ] && ok "All instances killed." || warn "$left process(es) still lingering."
+}
+
# ------------------------------------------------------------------ clean ---
-cmd_clean() { say "Removing generated copies/saves (real game untouched)..."; rm -rf "$BASE/p1" "$BASE/p2"; ok "Done."; }
+cmd_clean() { cmd_kill; say "Removing generated copies/saves (real game untouched)..."; rm -rf "$BASE/p1" "$BASE/p2"; ok "Done."; }
case "${1:-}" in
fetch) cmd_fetch ;;
setup) cmd_setup ;;
run) cmd_run ;;
tile) cmd_tile ;;
+ kill) cmd_kill ;;
clean) cmd_clean ;;
- *) say "Usage: $0 {fetch|check|setup|run|tile|clean} (MODE=native|proton to force)"; exit 1 ;;
+ *) say "Usage: $0 {fetch|check|setup|run|tile|kill|clean} (MODE=native|proton to force)"; exit 1 ;;
esac