Files
UltraWidePatches/install.sh
T
ckochandClaude Opus 4.8 0833cf8495 Add Clair Obscur: Expedition 33 via new METHOD=external
Expedition 33 is UE5 with no DRM, but a mature MIT-licensed fix already exists:
ClairObscurFix by Lyall. Rather than reimplement it, install.sh gains a third
method that vendors a third-party fix unmodified and layers our own tuned
config on top, with the upstream license shipped in the release zip as MIT
requires. Upstream is archived, so vendoring also keeps it available.

Our only change vs upstream defaults is [Center HUD] Enabled = true, since at
5120px wide the edge-anchored HUD elements end up absurdly far apart.

Install/uninstall round-trip verified against a scratch game dir, including
that our tuned ini overrides the vendored default.

Credit to Lyall is recorded in THIRD_PARTY.md, the game README, and the repo
README's method table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:58:05 -04:00

193 lines
7.1 KiB
Bash
Executable File

#!/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 <Game> # nonstandard install
# PREFIX_INI_FILE=/path/to/Engine.ini ./install.sh <Game> # nonstandard prefix
# LOADER_NAME=version.dll ./install.sh <Game> # 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 <GameName> [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 <<EOF
[+] Installed ($METHOD): $(case "$METHOD" in
bepinex) echo "BepInEx + $PLUGIN_DLL + $LOADER" ;;
external) echo "$EXTERNAL_FILES" ;;
*) echo "SUWSF.asi, SUWSF.ini, $LOADER" ;;
esac)
NEXT (Proton) — REQUIRED, or the loader is ignored and the fix won't apply in gameplay.
Steam > 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
<game>/BepInEx/config/*.cfg (edits apply live, ~2s, no restart)
Plugin log (proves whether it applied):
<game>/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