From 1f977bb1f49bccaaa281c2545815e981dbe20c00 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 5 May 2026 02:22:36 -0400 Subject: [PATCH] Add UMU Proton backend for Shadow Line --- .includes/checkup.sh | 8 +++ .includes/help.sh | 30 +++++++++- .includes/proton.sh | 112 +++++++++++++++++++++++++++++++++++++ .install/Shadow Line.sh | 55 +++++++++++------- audiogame-manager.sh | 56 ++++++++++++++----- tests/umu_backend_tests.sh | 104 ++++++++++++++++++++++++++++++++++ 6 files changed, 329 insertions(+), 36 deletions(-) create mode 100644 .includes/proton.sh create mode 100644 tests/umu_backend_tests.sh diff --git a/.includes/checkup.sh b/.includes/checkup.sh index ed68e06..bc249b7 100755 --- a/.includes/checkup.sh +++ b/.includes/checkup.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + declare -a errorList declare -a packageList if [[ $# -eq 0 ]]; then @@ -10,6 +12,12 @@ else errorList+=("Critical: Wine is not installed. You will not be able to play any games.") fi packageList+=("wine") +if command -v umu-run &> /dev/null ; then + [[ $# -eq 0 ]] && echo "umu-launcher is installed." +else + errorList+=("Warning: umu-launcher is not installed. Games that require Proton/UMU will not install or launch.") +fi +packageList+=("umu-launcher") if command -v curl &> /dev/null ; then [[ $# -eq 0 ]] && echo "Curl is installed." else diff --git a/.includes/help.sh b/.includes/help.sh index a8b6fd4..854f9b2 100644 --- a/.includes/help.sh +++ b/.includes/help.sh @@ -1,3 +1,6 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2154 # Sourced by audiogame-manager with shared globals. + documentation() { if [[ "$2" == "Become a Patron" ]]; then return @@ -8,13 +11,36 @@ documentation() { # Extract architecture from first parameter (format: "win64|path") local wineArch="${1%%|*}" - get_bottle "$wineArch" + if [[ "$wineArch" == "umu" ]]; then + local launcherLine="" + local docFlag="" + local umuGameId="" + local -a documentationGame=() + launcherLine="$(grep -F -m1 "${1}|" "$configFile" 2> /dev/null || true)" + IFS='|' read -ra documentationGame <<< "$launcherLine" + for docFlag in "${documentationGame[@]:3}" ; do + if [[ "$docFlag" =~ ^export\ [a-zA-Z_][a-zA-Z0-9_]*=\'?.*\'?$ ]]; then + eval "$docFlag" + fi + done + if [[ -z "$umuGameId" ]]; then + echo "Unable to find UMU game id for documentation lookup." + return + fi + get_umu_bottle "$umuGameId" + else + get_bottle "$wineArch" + fi echo "Loading documentation, please wait..." # Try to find documentation based on common naming conventions. local gamePath - gamePath="$(winepath -u "$2" 2> /dev/null)" + if [[ "$wineArch" == "umu" ]]; then + gamePath="$(umu_windows_path_to_unix "$2")" + else + gamePath="$(winepath -u "$2" 2> /dev/null)" + fi gamePath="${gamePath%/*}" local gameDoc="" local isUrl="false" diff --git a/.includes/proton.sh b/.includes/proton.sh new file mode 100644 index 0000000..2c8c65d --- /dev/null +++ b/.includes/proton.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034,SC2154 # Sourced by audiogame-manager and installers with shared globals. + +require_umu() { + if command -v umu-run &> /dev/null; then + return 0 + fi + local message="This game requires umu-launcher. Please install umu-launcher and try again." + if declare -F agm_msgbox &> /dev/null; then + agm_msgbox "Audio Game Manager" "Audio Game Manager" "$message" + else + echo "$message" >&2 + fi + return 1 +} + +get_umu_bottle() { + local gameId="$1" + if [[ -z "$gameId" ]]; then + echo "get_umu_bottle requires a game id." >&2 + return 1 + fi + + export umuGameId="$gameId" + export WINEPREFIX="${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/protonBottles/${gameId}" + export GAMEID="$gameId" + export STORE="${umuStore:-none}" + export DISPLAY="${DISPLAY:-:0}" + mkdir -p "$WINEPREFIX" +} + +install_proton_bottle() { + local gameId="$1" + shift || true + require_umu || return 1 + get_umu_bottle "$gameId" || return 1 + + if [[ ! -f "${WINEPREFIX}/system.reg" ]]; then + umu-run "" + fi + + if [[ $# -gt 0 ]]; then + umu-run winetricks "$@" + fi +} + +umu_windows_path_to_unix() { + local windowsPath="$1" + local relativePath="" + if [[ "$windowsPath" =~ ^[cC]:\\ ]]; then + relativePath="${windowsPath:3}" + relativePath="${relativePath//\\//}" + printf '%s/drive_c/%s\n' "$WINEPREFIX" "$relativePath" + return 0 + fi + winepath -u "$windowsPath" +} + +run_umu_game() { + local windowsPath="$1" + local exePath="" + require_umu || return 1 + if [[ -z "${umuGameId:-}" ]]; then + echo "UMU game id is not set for ${game[2]:-selected game}." >&2 + return 1 + fi + get_umu_bottle "$umuGameId" || return 1 + exePath="$(umu_windows_path_to_unix "$windowsPath")" + if [[ ! -f "$exePath" ]]; then + echo "UMU executable not found: $exePath" >&2 + return 1 + fi + pushd "${exePath%/*}" > /dev/null || return 1 + umu-run "$exePath" + popd > /dev/null || return 1 +} + +add_umu_launcher() { + local gameId="$1" + local windowsPath="$2" + shift 2 + local launchSettings="umu|${windowsPath}|${game}|export umuGameId=${gameId}" + + while [[ $# -gt 0 ]]; do + launchSettings+="|$1" + shift + done + + if ! grep -F -q -x "$launchSettings" "$configFile" 2> /dev/null; then + echo "$launchSettings" >> "$configFile" + sort -t '|' -k3,3f -o "$configFile" "$configFile" + fi +} + +set_umu_reg_value() { + local key="$1" + local valueName="$2" + local valueData="$3" + wine reg add "$key" /v "$valueName" /t REG_SZ /d "$valueData" /f +} + +set_umu_app_winver() { + local exeName="$1" + local winVersion="$2" + set_umu_reg_value "HKCU\\Software\\Wine\\AppDefaults\\${exeName}" "Version" "$winVersion" +} + +stop_umu_bottle() { + if command -v wineserver &> /dev/null; then + wineserver -k 2> /dev/null || true + fi +} diff --git a/.install/Shadow Line.sh b/.install/Shadow Line.sh index 50449da..03db9d4 100644 --- a/.install/Shadow Line.sh +++ b/.install/Shadow Line.sh @@ -1,23 +1,38 @@ -# shellcheck shell=bash disable=SC2154 # cache and WINEPREFIX are set by audiogame-manager -#//Disable since it's not working -download "https://www.mm-galabo.com/sr/Download_files_srfv/shadowrine_fullvoice3.171.exe" "https://raw.githubusercontent.com/LordLuceus/sr-english-localization/master/language_en.dat" -export WINEARCH="win64" # Migrated to wine64 with WINETRICKS_FORCE=1 -export winVer="win8" -install_wine_bottle fakejapanese -# Add bcrypt DLL override required for Shadow Line to run -cat > /tmp/bcrypt_override.reg << 'EOF' -[HKEY_CURRENT_USER\Software\Wine\DllOverrides] -"bcryptprimitives"="native,builtin" -EOF -wine regedit /tmp/bcrypt_override.reg -rm /tmp/bcrypt_override.reg +# shellcheck shell=bash disable=SC2154 # cache, game, and helper functions are set by audiogame-manager. + +export game="Shadow Line" +shadowLineGameId="shadow-line" +shadowLinePath='c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe' + +download "https://www.mm-galabo.com/sr/Download_files_srfv/shadowrine_fullvoice3.171.exe" \ + "https://raw.githubusercontent.com/LordLuceus/sr-english-localization/master/language_en.dat" \ + "${nvdaControllerClient32Dll}" \ + "${nvdaControllerClient64Dll}" + +install_proton_bottle "$shadowLineGameId" fakejapanese +shadowLineInstallDir="${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice" + +set_umu_reg_value "HKCU\\Software\\Wine\\DllOverrides" "bcryptprimitives" "native,builtin" +set_umu_app_winver "play_sr.exe" "win8" + { echo "# Installing Shadow Line..." - timeout 300 wine "${cache}/shadowrine_fullvoice3.171.exe" /sp- /VERYSILENT /SUPPRESSMSGBOXES 2>&1 || true + timeout 300 umu-run "${cache}/shadowrine_fullvoice3.171.exe" /sp- /VERYSILENT /SUPPRESSMSGBOXES 2>&1 || true echo "# Installation complete" -} | agm_progressbox "Installing Game" "Installing Shadow Line (this may take a few minutes)..." -# Kill any auto-launched game processes (installer lacks skipifsilent flag) -wineserver -k 2>/dev/null || true -mv -v "${cache}/language_en.dat" "${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice/SystemData/language_en.dat" -add_launcher "c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe" -alert "Please set the language to English when the game opens.\nGo to options and press enter.\nPress down arrow 5 times and press enter.\nPress down arrow 1 time and press enter.\nPress up arrow 2 times and press enter.\nIf everything worked as expected you should be back on the game menu and speech should work." +} | agm_progressbox "Installing Game" "Installing Shadow Line with UMU/Proton (this may take a few minutes)..." + +stop_umu_bottle + +if [[ ! -f "${shadowLineInstallDir}/play_sr.exe" ]]; then + agm_msgbox "Shadow Line" "Shadow Line" "Shadow Line did not install to the expected location: ${shadowLineInstallDir}" + exit 1 +fi + +install -m 0644 "${cache}/language_en.dat" "${shadowLineInstallDir}/SystemData/language_en.dat" + +find "$shadowLineInstallDir" -type f -iname 'nvdaControllerClient32.dll' -exec cp -v "${cache}/nvdaControllerClient32.dll" "{}" \; +find "$shadowLineInstallDir" -type f -iname 'nvdaControllerClient64.dll' -exec cp -v "${cache}/nvdaControllerClient64.dll" "{}" \; +find "$shadowLineInstallDir" -type f -iname 'nvdaControllerClient.dll' -exec cp -v "${cache}/nvdaControllerClient32.dll" "{}" \; + +add_umu_launcher "$shadowLineGameId" "$shadowLinePath" +alert "Shadow Line" "Shadow Line" "Please set the language to English when the game opens.\nGo to options and press enter.\nPress down arrow 5 times and press enter.\nPress down arrow 1 time and press enter.\nPress up arrow 2 times and press enter.\nIf everything worked as expected you should be back on the game menu and speech should work." diff --git a/audiogame-manager.sh b/audiogame-manager.sh index e1187eb..9e89763 100755 --- a/audiogame-manager.sh +++ b/audiogame-manager.sh @@ -469,23 +469,36 @@ game_removal() { # With shared bottles, always remove only the game files, never the entire bottle create_game_array "$selectedGame" if [[ ${#game[@]} -gt 0 ]]; then - # Set up wine environment for this game - # shellcheck source=.includes/bottle.sh - source "${scriptDir}/.includes/bottle.sh" - get_bottle "${game[0]}" + process_launcher_flags + if [[ "${game[0]}" == "umu" ]]; then + get_umu_bottle "$umuGameId" + else + # Set up wine environment for this game + # shellcheck source=.includes/bottle.sh + source "${scriptDir}/.includes/bottle.sh" + get_bottle "${game[0]}" + fi if ! agm_yesno "Confirm Removal" "Audio Game Removal" "Are you sure you want to remove \"${game[2]}\"?"; then agm_msgbox "Audio Game Removal" "" "Removal cancelled." exit 0 fi - # kill any previous existing wineservers for this prefix in case they didn't shut down properly. - wineserver -k + # kill any previous existing servers for this prefix in case they didn't shut down properly. + if [[ "${game[0]}" == "umu" ]]; then + stop_umu_bottle + else + wineserver -k + fi # Remove only the game's installation directory if [[ -n "$winePath" ]]; then local gameDir - gameDir="$(winepath "$winePath")" + if [[ "${game[0]}" == "umu" ]]; then + gameDir="$(umu_windows_path_to_unix "$winePath")" + else + gameDir="$(winepath "$winePath")" + fi if [[ -d "$gameDir" ]]; then rm -rfv "$gameDir" | agm_progressbox "Removing Game" "Removing \"${game[2]}\" files..." else @@ -534,9 +547,16 @@ kill_game() { local wineExec="${game#*|}" wineExec="${wineExec%|*}" wineExec="${wineExec##*\\}" - # kill the wine server. - get_bottle "${game%|*}" - wineserver -k + create_game_array "$game" + if [[ "${game[0]}" == "umu" ]]; then + process_launcher_flags + get_umu_bottle "$umuGameId" + stop_umu_bottle + else + # kill the wine server. + get_bottle "${game[0]}" + wineserver -k + fi agm_msgbox "Audio Game Killer" "" "The selected game has been stopped." fi exit 0 @@ -790,14 +810,21 @@ game_launcher() { open_url "https://2mb.games/product/2mb-patron/" exit 0 fi + # Set default path/exec for custom launch handlers. + winePath="${game[1]%\\*.exe}" + wineExec="${game[1]##*\\}" + process_launcher_flags + if [[ "${game[0]}" == "umu" ]]; then + echo "launching with umu" + start_nvda2speechd + run_umu_game "${game[1]}" + exit 0 + fi get_bottle "${game[0]}" echo -n "launching " wine --version # kill any previous existing wineservers for this prefix in case they didn't shut down properly. wineserver -k - # Set default path/exec for custom launch handlers. - winePath="${game[1]%\\*.exe}" - wineExec="${game[1]##*\\}" # launch the game if command -v qjoypad &> /dev/null ; then mkdir -p ~/.qjoypad3 @@ -812,7 +839,6 @@ game_launcher() { fi fi fi - process_launcher_flags apply_executioners_rage_focus_workaround customLaunchHandled="false" custom_launch_parameters @@ -901,6 +927,8 @@ export ipfsGateway="${ipfsGateway:-https://ipfs.stormux.org}" # Source helper functions # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" # Also sourced in functions that need it +# shellcheck source=.includes/proton.sh +source "${scriptDir}/.includes/proton.sh" # shellcheck source=.includes/desktop.sh source "${scriptDir}/.includes/desktop.sh" # dialog-interface.sh already sourced earlier diff --git a/tests/umu_backend_tests.sh b/tests/umu_backend_tests.sh new file mode 100644 index 0000000..051d160 --- /dev/null +++ b/tests/umu_backend_tests.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -euo pipefail + +repoRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +testRoot="$(mktemp -d)" +trap 'rm -rf "$testRoot"' EXIT + +export HOME="${testRoot}/home" +export XDG_DATA_HOME="${HOME}/.local/share" +export XDG_CONFIG_HOME="${HOME}/.config" +export XDG_CACHE_HOME="${HOME}/.cache" +export DISPLAY="" +export cache="${XDG_CACHE_HOME}/audiogame-manager" +export configFile="${XDG_CONFIG_HOME}/storm-games/audiogame-manager/games.conf" +export game="Shadow Line" +export scriptDir="$repoRoot" +mkdir -p "$cache" "${configFile%/*}" "${testRoot}/bin" +touch "$configFile" + +cat > "${testRoot}/bin/umu-run" <<'STUB' +#!/usr/bin/env bash +printf '%s|%s|%s|%s\n' "$WINEPREFIX" "$GAMEID" "${STORE:-}" "$*" >> "$UMU_STUB_LOG" +if [[ "${1:-}" == "" ]]; then + mkdir -p "$WINEPREFIX/drive_c" +fi +STUB +chmod +x "${testRoot}/bin/umu-run" + +cat > "${testRoot}/bin/wine" <<'STUB' +#!/usr/bin/env bash +if [[ "${1:-}" == "winepath" || "${1:-}" == "winepath.exe" ]]; then + shift +fi +if [[ "${1:-}" == "-u" ]]; then + input="$2" + path="${input#c:\\}" + path="${path//\\//}" + printf '%s/drive_c/%s\n' "$WINEPREFIX" "$path" + exit 0 +fi +printf 'wine %s\n' "$*" >> "$WINE_STUB_LOG" +STUB +chmod +x "${testRoot}/bin/wine" + +cat > "${testRoot}/bin/wineserver" <<'STUB' +#!/usr/bin/env bash +printf 'wineserver %s\n' "$*" >> "$WINE_STUB_LOG" +STUB +chmod +x "${testRoot}/bin/wineserver" + +export PATH="${testRoot}/bin:$PATH" +export UMU_STUB_LOG="${testRoot}/umu.log" +export WINE_STUB_LOG="${testRoot}/wine.log" + +# shellcheck source=.includes/proton.sh +source "${repoRoot}/.includes/proton.sh" + +assert_equals() { + local expected="$1" + local actual="$2" + local message="$3" + if [[ "$expected" != "$actual" ]]; then + printf 'FAIL: %s\nexpected: %s\nactual: %s\n' "$message" "$expected" "$actual" >&2 + exit 1 + fi +} + +assert_file_contains() { + local file="$1" + local pattern="$2" + local message="$3" + if ! grep -Fq "$pattern" "$file"; then + printf 'FAIL: %s\nmissing pattern: %s\nfile contents:\n' "$message" "$pattern" >&2 + cat "$file" >&2 + exit 1 + fi +} + +test_get_umu_bottle_sets_environment() { + get_umu_bottle "shadow-line" + assert_equals "${XDG_DATA_HOME}/audiogame-manager/protonBottles/shadow-line" "$WINEPREFIX" "WINEPREFIX points at AGM proton bottle" + assert_equals "shadow-line" "$GAMEID" "GAMEID is exported" + assert_equals "none" "$STORE" "STORE defaults to none" + assert_equals ":0" "$DISPLAY" "DISPLAY defaults to :0" +} + +test_add_umu_launcher_records_backend_and_game_id() { + get_umu_bottle "shadow-line" + add_umu_launcher "shadow-line" 'c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe' + assert_file_contains "$configFile" 'umu|c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe|Shadow Line|export umuGameId=shadow-line' "UMU launcher entry is recorded" +} + +test_run_umu_game_uses_converted_path() { + get_umu_bottle "shadow-line" + mkdir -p "${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice" + touch "${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice/play_sr.exe" + run_umu_game 'c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe' + assert_file_contains "$UMU_STUB_LOG" "${WINEPREFIX}|shadow-line|none|${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice/play_sr.exe" "UMU launches converted exe path" +} + +test_get_umu_bottle_sets_environment +test_add_umu_launcher_records_backend_and_game_id +test_run_umu_game_uses_converted_path +printf 'UMU backend tests passed\n'