#!/usr/bin/env bash # ============================================================================ # UltraWidePatches - installer (Linux / Proton, also usable on Windows via # manual extraction of the release zip) # # Usage: # ./install.sh # list available games # ./install.sh GundamBreaker4 # install that game's patch # ./install.sh GundamBreaker4 uninstall # # Overrides: # GAME_DIR=/path/to/Binaries/Win64 ./install.sh # nonstandard install # PREFIX_INI_FILE=/path/to/Engine.ini ./install.sh # nonstandard prefix # LOADER_NAME=version.dll ./install.sh # loader collision # ============================================================================ set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" GAMES_DIR="$HERE/games" list_games() { echo "Available patches:" for d in "$GAMES_DIR"/*/; do [ -f "$d/game.conf" ] || continue ( TITLE=""; STATUS=""; . "$d/game.conf" >/dev/null 2>&1 || true printf " %-22s %s\n" "$(basename "$d")" "${TITLE:-} — ${STATUS:-}" ) done echo echo "Usage: ./install.sh [uninstall]" } GAME="${1:-}" ACTION="${2:-install}" if [ -z "$GAME" ]; then list_games; exit 0; fi GAME_CONF="$GAMES_DIR/$GAME/game.conf" if [ ! -f "$GAME_CONF" ]; then echo "[!] Unknown game: $GAME"; echo; list_games; exit 1 fi # shellcheck disable=SC1090 . "$GAME_CONF" LOADER="${LOADER_NAME:-${LOADER:-dsound.dll}}" echo "=== ${TITLE:-$GAME} ===" # --- locate the game's exe directory across Steam libraries ------------------- find_gamedir() { local cand for base in \ "$HOME/.steam/steam" "$HOME/.local/share/Steam" \ "/media/$USER"/*/steam "/media/$USER"/*/SteamLibrary \ "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"; do cand="$base/steamapps/common/$COMMON_SUBDIR" [ -f "$cand/$EXE" ] && { echo "$cand"; return 0; } done cand=$(find /home /media /mnt -maxdepth 8 -name "$EXE" 2>/dev/null | head -1) [ -n "$cand" ] && { dirname "$cand"; return 0; } return 1 } GAMEDIR="${GAME_DIR:-$(find_gamedir || true)}" if [ -z "${GAMEDIR:-}" ] || [ ! -f "$GAMEDIR/$EXE" ]; then echo "[!] Could not find $EXE." echo " Set it manually: GAME_DIR=/path/to/exe/dir ./install.sh $GAME" exit 1 fi echo "[+] Game dir: $GAMEDIR" # --- locate the Proton prefix ini (optional, per-game) ------------------------ find_prefix_ini() { [ -n "${PREFIX_INI_FILE:-}" ] && { [ -f "$PREFIX_INI_FILE" ] && echo "$PREFIX_INI_FILE" && return 0; return 1; } [ -z "${PREFIX_INI:-}" ] && return 1 local cand for base in \ "$HOME/.steam/steam" "$HOME/.local/share/Steam" \ "/media/$USER"/*/steam "/media/$USER"/*/SteamLibrary \ "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"; do cand="$base/steamapps/compatdata/$APPID/pfx/$PREFIX_INI" [ -f "$cand" ] && { echo "$cand"; return 0; } done return 1 } MARK_BEGIN="; >>> UltraWidePatches ($GAME) >>>" MARK_END="; <<< UltraWidePatches ($GAME) <<<" apply_ini() { [ -z "${INI_BLOCK:-}" ] && return local ini; ini="$(find_prefix_ini || true)" [ -z "$ini" ] && { echo "[i] Prefix ini not found (launch the game once first); skipping config tweak."; return; } if grep -qF "$MARK_BEGIN" "$ini"; then echo "[i] Config tweak already present."; return; fi cp "$ini" "$ini.bak-uwp" { echo ""; echo "$MARK_BEGIN"; echo "$INI_BLOCK"; echo "$MARK_END"; } >> "$ini" echo "[+] Config tweak applied (backup: $(basename "$ini").bak-uwp)" } remove_ini() { local ini; ini="$(find_prefix_ini || true)" [ -z "$ini" ] && return if grep -qF "$MARK_BEGIN" "$ini"; then local esc_b esc_e esc_b=$(printf '%s' "$MARK_BEGIN" | sed 's/[][\/.*^$]/\\&/g') esc_e=$(printf '%s' "$MARK_END" | sed 's/[][\/.*^$]/\\&/g') sed -i "/$esc_b/,/$esc_e/d" "$ini" echo "[+] Config tweak removed." fi } METHOD="${METHOD:-suwsf}" if [ "$ACTION" = "uninstall" ]; then echo "[+] Uninstalling..." if [ "$METHOD" = "external" ]; then for f in $EXTERNAL_FILES; do [ -f "$GAMEDIR/$f" ] && rm -v "$GAMEDIR/$f"; done elif [ "$METHOD" = "bepinex" ]; then rm -vf "$GAMEDIR/winhttp.dll" "$GAMEDIR/doorstop_config.ini" "$GAMEDIR/.doorstop_version" \ "$GAMEDIR/changelog.txt" "$GAMEDIR/BepInEx/plugins/$PLUGIN_DLL" # leave BepInEx/ itself: the user may have other mods installed echo "[i] Left BepInEx/ in place (other mods may rely on it). Delete it manually if unused." else for f in SUWSF.asi SUWSF.ini dsound.dll dinput8.dll version.dll winmm.dll; do [ -f "$GAMEDIR/$f" ] && rm -v "$GAMEDIR/$f" done fi remove_ini echo "[+] Done. No game file was ever modified." exit 0 fi # --- install ------------------------------------------------------------------ if [ "$METHOD" = "external" ]; then SRC="$HERE/vendor/$VENDOR_PAYLOAD" [ -d "$SRC" ] || { echo "[!] missing vendored payload: $SRC"; exit 1; } cp -v "$SRC"/* "$GAMEDIR/" 2>/dev/null || true # our tuned config wins over the upstream default if [ -n "${EXTERNAL_CONFIG:-}" ] && [ -f "$GAMES_DIR/$GAME/$EXTERNAL_CONFIG" ]; then cp -v "$GAMES_DIR/$GAME/$EXTERNAL_CONFIG" "$GAMEDIR/$EXTERNAL_CONFIG" fi elif [ "$METHOD" = "bepinex" ]; then BEPZIP=$(ls "$HERE"/vendor/BepInEx_win_x64_*.zip 2>/dev/null | head -1) PLUGIN="$GAMES_DIR/$GAME/$PLUGIN_DLL" [ -f "$BEPZIP" ] || { echo "[!] missing BepInEx zip in vendor/"; exit 1; } [ -f "$PLUGIN" ] || { echo "[!] missing prebuilt plugin: $PLUGIN"; exit 1; } if [ -f "$GAMEDIR/winhttp.dll" ]; then echo "[i] BepInEx already present; updating plugin only." else command -v unzip >/dev/null || { echo "[!] unzip required"; exit 1; } unzip -o -q "$BEPZIP" -d "$GAMEDIR" echo "[+] Installed BepInEx ($(basename "$BEPZIP"))" fi mkdir -p "$GAMEDIR/BepInEx/plugins" cp -v "$PLUGIN" "$GAMEDIR/BepInEx/plugins/" else ASI="$HERE/vendor/SUWSF-x64/SUWSF.asi" LOADER_SRC="$HERE/vendor/SUWSF-x64/dsound.dll" # Ultimate ASI Loader (adapts by filename) INI="$GAMES_DIR/$GAME/SUWSF.ini" for f in "$ASI" "$LOADER_SRC" "$INI"; do [ -f "$f" ] || { echo "[!] missing $f"; exit 1; } done cp -v "$ASI" "$GAMEDIR/SUWSF.asi" cp -v "$INI" "$GAMEDIR/SUWSF.ini" cp -v "$LOADER_SRC" "$GAMEDIR/$LOADER" fi apply_ini cat < right-click ${TITLE:-$GAME} > Properties > Launch Options: ${LAUNCH_OPTION:-WINEDLLOVERRIDES="${LOADER%.dll}=n,b" %command%} Then set the in-game resolution to your native ultrawide res, fullscreen. Tuning (METHOD=bepinex games): Config is generated on first launch at /BepInEx/config/*.cfg (edits apply live, ~2s, no restart) Plugin log (proves whether it applied): /BepInEx/LogOutput.log Troubleshooting: Different loader name: LOADER_NAME=version.dll ./install.sh $GAME Diagnose injection: add PROTON_LOG=1 to launch options, then ./tools/read_protonlog.sh $APPID Remove everything: ./install.sh $GAME uninstall EOF