18 Commits
Author SHA1 Message Date
Storm Dragon d4db802645 Merge branch 'testing' of ssh://git.stormux.org:1101/storm/audiogame-manager into testing 2026-08-29 03:19:48 -04:00
Storm Dragon 993f838404 Espeak now set as default voice for new installs. Install_espeak.sh helper script added in speech. 2026-08-29 03:19:40 -04:00
hjozwiak 7ff3f0b47d Updatr for Swamp 2.
For whatever reason, Wine is choking on the update when you try to go
and do it through the game's menu, so we'll just do it here for now.
2026-08-26 05:33:21 -04:00
Storm Dragon eee7b7e7c8 Gamemode support added. Will automatically use gamemode if installed, so be sure it is properly configured /etc/gamemode.ini. 2026-08-23 21:18:07 -04:00
Storm Dragon 4b312099b4 Dependency installer for Fedora added. 2026-08-20 22:30:37 -04:00
Storm Dragon 7ec9ef3b87 Removed ripgrep call to cut down on dependencies. 2026-08-20 22:15:55 -04:00
hjozwiak b87245fa69 Fix mist world installer to use the official download link from the game instead of the one from Google Drive.
Also, put it in x86 since it's a 32-bit game.
2026-08-09 08:52:47 -04:00
hjozwiak 05ea1502ce Add an alert to the installer to tell you how to get the game talking.
I should have done that last night but I wanted to kill zombies and forgot.
2026-08-05 00:34:25 -04:00
hjozwiak c033a21211 Add the new swamp 2 beta.
There be rough edges and possibly dragons herein.
2026-08-04 03:36:47 -04:00
Storm Dragon 8943688a43 I forgot to push changes, and it's been a while. I'm pretty sure this is game fixes. 2026-07-27 01:41:47 -04:00
Storm Dragon 92745d8af8 Fix Shadow Line translation line endings 2026-05-05 02:58:12 -04:00
Storm Dragon 1f977bb1f4 Add UMU Proton backend for Shadow Line 2026-05-05 02:58:12 -04:00
Storm Dragon 53d8c10645 Plan UMU Proton backend implementation 2026-05-05 02:58:12 -04:00
Storm Dragon b58681964c Document UMU Proton backend design 2026-05-05 02:58:12 -04:00
lilmike 2abf445637 Fix error installing mtga if a previous game was using win xp. 2026-05-02 16:57:54 -07:00
lilmike c0e6c37f1a Add Magic: the Gathering Arena 2026-05-02 13:03:40 -07:00
Storm Dragon 856415c22f Forgot the shellcheck statement. 2026-04-23 13:34:38 -04:00
Storm Dragon 7ddd7cbac4 Judgement Day download link updated. 2026-04-23 13:32:37 -04:00
25 changed files with 1769 additions and 419 deletions
+71
View File
@@ -69,6 +69,77 @@ winetricks() {
fi
}
install_winespeak() {
local installerName="winespeak.exe"
local installerPath="${cache}/${installerName}"
local installerSha256="187d4db69f3af7c1bca1c100a04489b76732048be7c38449781da360ed7b2d66"
local wrapperPath="${WINEPREFIX}/drive_c/Program Files (x86)/espeak-ng-sapi/EspeakSAPI.dll"
local wrapperSha256="8009750dad82ca6665871814033dcee6844bb424f033130c109f47e188c5364b"
local cscriptPath="${WINEPREFIX}/drive_c/windows/syswow64/cscript.exe"
local verifyScriptPath="${WINEPREFIX}/drive_c/windows/temp/verify_winespeak.vbs"
local actualSha256
local registryOutput
local voiceOutput
local defaultToken='HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\TokenEnums\eSpeak-NG\English (America)'
if [[ -z "${wineSpeakInstaller:-}" ]]; then
echo "WineSpeak download URL is not configured."
return 1
fi
download "$wineSpeakInstaller"
actualSha256="$(sha256sum "$installerPath" | awk '{print $1}')"
if [[ "$actualSha256" != "$installerSha256" ]]; then
echo "WineSpeak installer failed its SHA-256 check."
rm -f "$installerPath"
return 1
fi
if ! wine "$installerPath" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-; then
echo "WineSpeak installer failed."
return 1
fi
if [[ ! -f "$wrapperPath" ]]; then
echo "WineSpeak did not install its 32-bit SAPI wrapper."
return 1
fi
actualSha256="$(sha256sum "$wrapperPath" | awk '{print $1}')"
if [[ "$actualSha256" != "$wrapperSha256" ]]; then
echo "WineSpeak installed an unexpected 32-bit SAPI wrapper."
return 1
fi
registryOutput="$(wine reg query 'HKCU\Software\Microsoft\Speech\Voices' /v DefaultTokenId 2>/dev/null)"
if ! grep -Fq "$defaultToken" <<< "$registryOutput"; then
echo "WineSpeak did not become the default SAPI voice."
return 1
fi
if [[ ! -f "$cscriptPath" ]]; then
echo "Wine's 32-bit cscript.exe was not found."
return 1
fi
mkdir -p "${verifyScriptPath%/*}"
cat > "$verifyScriptPath" <<'EOF'
dim speechobject
set speechobject=createobject("sapi.spvoice")
wscript.echo speechobject.voice.getdescription
EOF
if ! voiceOutput="$(wine "$cscriptPath" //nologo 'c:\windows\temp\verify_winespeak.vbs' 2>&1)"; then
echo "WineSpeak's 32-bit SAPI voice could not be created."
echo "$voiceOutput"
return 1
fi
if ! grep -Fq "English (America)" <<< "$voiceOutput"; then
echo "WineSpeak's 32-bit SAPI verification returned an unexpected voice."
echo "$voiceOutput"
return 1
fi
echo "WineSpeak English (America) is the default SAPI voice."
}
install_rhvoice() {
if [[ -d "${WINEPREFIX}/drive_c/Program Files/Olga Yakovleva/" ]]; then
return
+8
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
run_with_optional_gamemode() {
if command -v gamemoderun &> /dev/null; then
gamemoderun "$@"
else
"$@"
fi
}
+28 -2
View File
@@ -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"
+4
View File
@@ -0,0 +1,4 @@
# Replacement map for scripts/check-ipfs-links.sh --fix.
# Format: MATCH<TAB>REPLACEMENT
# MATCH can be the ipfs array key, such as BG 15 Puzzle, or the old CID.
# REPLACEMENT can be a bare CID or a full /ipfs/ URL. Bare CIDs keep the existing query string.
1 # Replacement map for scripts/check-ipfs-links.sh --fix.
2 # Format: MATCH<TAB>REPLACEMENT
3 # MATCH can be the ipfs array key, such as BG 15 Puzzle, or the old CID.
4 # REPLACEMENT can be a bare CID or a full /ipfs/ URL. Bare CIDs keep the existing query string.
+1
View File
@@ -7,6 +7,7 @@ declare -Ag ipfs=(
[nvdaControllerClient32]="${ipfsGateway}/ipfs/QmTrRrT4QFKSkZ8ivfUawA6iJ6adEyyogccE3nLDTfSK8u?filename=nvdaControllerClient32.dll"
[nvdaControllerClient64]="${ipfsGateway}/ipfs/QmaYE7RFDtwHCiXCVLcuA3esfFx6E7koidtvrck9AwPuuN?filename=nvdaControllerClient64.dll"
[nvda2speechd]="${ipfsGateway}/ipfs/QmPxhoNsoFoJC7bCfioBBCcK8tEoSoYpm342z6u7KjFsVz?filename=nvda2speechd"
[winespeak]="https://stormgames.wolfe.casa/downloads/winespeak.exe"
# Games (alphabetical order)
[BG 15 Puzzle]="${ipfsGateway}/ipfs/QmQiocMpMXoxejDftKKvmrR5xxpj1qcWcgkhBBwTcyijXg?filename=FPB32Setup10a.exe"
+145
View File
@@ -0,0 +1,145 @@
#!/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
}
install_proton_winetricks_verb() {
local verbName="$1"
local output=""
local status=0
local hadErrexit="false"
require_umu || return 1
if [[ $- == *e* ]]; then
hadErrexit="true"
fi
set +e
output="$(umu-run winetricks -q "$verbName" 2>&1)"
status=$?
if [[ "$hadErrexit" == "true" ]]; then
set -e
fi
printf '%s\n' "$output"
if [[ "$status" -ne 0 ]]; then
if grep -F -q "winetricks verb '${verbName}' is already installed" <<< "$output"; then
return 0
fi
return "$status"
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
run_with_optional_gamemode 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"
}
install_crlf_file() {
local sourceFile="$1"
local destFile="$2"
mkdir -p "${destFile%/*}"
perl -pe 's/\r?\n/\r\n/' "$sourceFile" > "$destFile"
}
stop_umu_bottle() {
if command -v wineserver &> /dev/null; then
wineserver -k 2> /dev/null || true
fi
}
+41 -9
View File
@@ -1,9 +1,41 @@
download "https://blindgamers.com/downloads/a-heros-call-freeware.zip" "https://stormgames.wolfe.casa/downloads/nvdaControllerClient32.dll"
export winVer="win7"
export winetricksSettings="vd=1024x768"
install_wine_bottle
# Dotnet is evil. That is all.
LC_ALL=C DISPLAY="" winetricks -q dotnet462 xna40
wineserver -k # Really!
install_with_progress unzip "Extracting game files..." -d "$WINEPREFIX/drive_c/Program Files" "${cache}/a-heros-call-freeware.zip"
add_launcher "c:\Program Files\a-heros-call\A Hero's Call.exe"
# shellcheck shell=bash disable=SC2154 # cache, game, and helper functions are set by audiogame-manager.
export game="A Hero's Call"
herosCallGameId="a-heros-call"
herosCallPath="c:\\Program Files\\a-heros-call\\A Hero's Call.exe"
download "https://blindgamers.com/downloads/a-heros-call-freeware.zip" \
"https://stormgames.wolfe.casa/downloads/nvdaControllerClient32.dll"
export PROTON_USE_XALIA=0
install_proton_bottle "$herosCallGameId"
alert "A Hero's Call" "A Hero's Call" "If you hear a window open during installation, or if the installer seems to be taking a long time, try pressing enter to see if it will continue."
{
echo "# Installing A Hero's Call dependencies..."
WINETRICKS_FORCE=1 install_proton_winetricks_verb speechsdk
install_proton_winetricks_verb corefonts
install_proton_winetricks_verb dotnet462
install_proton_winetricks_verb xna40
install_proton_winetricks_verb win7
echo "# Extracting game files..."
mkdir -p "${WINEPREFIX}/drive_c/Program Files"
unzip -oq "${cache}/a-heros-call-freeware.zip" -d "${WINEPREFIX}/drive_c/Program Files"
echo "# Installation complete"
} | agm_progressbox "Installing Game" "Installing A Hero's Call with UMU/Proton (this may take several minutes)..."
stop_umu_bottle
herosCallInstallDir="${WINEPREFIX}/drive_c/Program Files/a-heros-call"
if [[ ! -f "${herosCallInstallDir}/A Hero's Call.exe" ]]; then
agm_msgbox "A Hero's Call" "A Hero's Call" "A Hero's Call did not install to the expected location."
exit 1
fi
cp "${cache}/nvdaControllerClient32.dll" "${herosCallInstallDir}/nvdaControllerClient32.dll"
mkdir -p "${herosCallInstallDir}/lib/Tolk"
cp "${cache}/nvdaControllerClient32.dll" "${herosCallInstallDir}/lib/Tolk/nvdaControllerClient32.dll"
add_umu_launcher "$herosCallGameId" "$herosCallPath" "export PROTON_USE_XALIA=0"
+92 -26
View File
@@ -1,28 +1,94 @@
#// Borken, candidate for removal
export WINEARCH="win64" # Migrated to wine64 with WINETRICKS_FORCE=1 - complex .NET dependencies, test carefully
download "http://blind-games.com/newentombed/EntombedSetup.exe" "https://download.microsoft.com/download/E/C/1/EC1B2340-67A0-4B87-85F0-74D987A27160/SSCERuntime-ENU.exe" "https://stormgames.wolfe.casa/downloads/Entombed.exe.config" "https://stormgames.wolfe.casa/downloads/mfplat.dll"
export winVer="win7"
install_wine_bottle sapi msvcrt40 gdiplus ie7 wmp11 mf
# Ok, more dotnet.
LC_ALL=C DISPLAY="" winetricks -q dotnet40 xna40
wineserver -k # Sigh.
mkdir -p "${WINEPREFIX}/drive_c/temp"
pushd "${WINEPREFIX}/drive_c/temp"
install_with_progress 7z "Extracting game files..." x "${cache}/SSCERuntime-ENU.exe"
wine msiexec /i "${WINEPREFIX}/drive_c/temp/SSCERuntime_x86-ENU.msi" /q
rm *
popd
pushd "${WINEPREFIX}/drive_c/Program Files/Microsoft SQL Server Compact Edition/v3.5"
wine regsvr32 sqlceoledb35.dll
wine regsvr32 sqlceca35.dll
popd
wine "${cache}/EntombedSetup.exe" /silent
pushd "${WINEPREFIX}/drive_c/Program Files/Entombed"
cp ../Microsoft\ SQL\ Server\ Compact\ Edition/v3.5/Private/System.Data.SqlServerCe.Entity.dll ../Microsoft\ SQL\ Server\ Compact\ Edition/v3.5/Private/System.Data.SqlServerCe.dll .
cp ../Microsoft\ SQL\ Server\ Compact\ Edition/v3.5/sql* .
cp "${cache}/Entombed.exe.config" .
popd
if [ ! -f "${WINEPREFIX}/drive_c/windows/system32/mfplat.dll" ] ; then
# shellcheck shell=bash disable=SC2154 # cache, game, and helper functions are set by audiogame-manager.
export game="Entombed"
entombedGameId="entombed"
entombedPath='c:\Program Files (x86)\Entombed\Entombed.exe'
download "http://blind-games.com/newentombed/EntombedSetup.exe" \
"https://download.microsoft.com/download/E/C/1/EC1B2340-67A0-4B87-85F0-74D987A27160/SSCERuntime-ENU.exe" \
"https://stormgames.wolfe.casa/downloads/Entombed.exe.config" \
"https://stormgames.wolfe.casa/downloads/mfplat.dll"
export PROTON_USE_XALIA=0
install_proton_bottle "$entombedGameId"
entombedProfileSid="$(sed -n 's/^;; All keys relative to REGISTRY\\\\User\\\\//p' "${WINEPREFIX}/user.reg" | head -n 1)"
if [[ -n "$entombedProfileSid" ]]; then
umu-run reg add "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\${entombedProfileSid}" /v ProfileImagePath /t REG_SZ /d "C:\\users\\steamuser" /f
fi
mkdir -p \
"${WINEPREFIX}/drive_c/users/steamuser/AppData/Local/IsolatedStorage" \
"${WINEPREFIX}/drive_c/users/steamuser/AppData/Roaming/IsolatedStorage"
alert "Entombed" "Entombed" "If you hear a window open during installation, or if the installer seems to be taking a long time, try pressing enter to see if it will continue."
{
echo "# Installing Entombed dependencies..."
WINETRICKS_FORCE=1 install_proton_winetricks_verb speechsdk
install_proton_winetricks_verb msvcrt40
install_proton_winetricks_verb gdiplus
install_proton_winetricks_verb wmp11
install_proton_winetricks_verb mf
install_proton_winetricks_verb dotnet40
install_proton_winetricks_verb xna40
install_proton_winetricks_verb win7
echo "# Extracting SQL Server Compact runtime..."
entombedTempDir="${WINEPREFIX}/drive_c/temp"
mkdir -p "$entombedTempDir"
7z x -y "-o${entombedTempDir}" "${cache}/SSCERuntime-ENU.exe"
echo "# Installing SQL Server Compact runtime..."
umu-run msiexec /i "c:\\temp\\SSCERuntime_x86-ENU.msi" /q
umu-run regsvr32 "c:\\Program Files\\Microsoft SQL Server Compact Edition\\v3.5\\sqlceoledb35.dll"
umu-run regsvr32 "c:\\Program Files\\Microsoft SQL Server Compact Edition\\v3.5\\sqlceca35.dll"
echo "# Installing Entombed..."
umu-run "${cache}/EntombedSetup.exe" /silent
find "$entombedTempDir" -mindepth 1 -maxdepth 1 -exec rm -rf "{}" +
echo "# Installation complete"
} | agm_progressbox "Installing Game" "Installing Entombed with UMU/Proton (this may take several minutes)..."
stop_umu_bottle
entombedInstallDir=""
for entombedCandidateDir in \
"${WINEPREFIX}/drive_c/Program Files (x86)/Entombed" \
"${WINEPREFIX}/drive_c/Program Files/Entombed"; do
if [[ -f "${entombedCandidateDir}/Entombed.exe" ]]; then
entombedInstallDir="$entombedCandidateDir"
break
fi
done
if [[ -z "$entombedInstallDir" ]]; then
agm_msgbox "Entombed" "Entombed" "Entombed did not install to the expected location."
exit 1
fi
entombedSqlDir=""
for entombedCandidateSqlDir in \
"${WINEPREFIX}/drive_c/Program Files (x86)/Microsoft SQL Server Compact Edition/v3.5" \
"${WINEPREFIX}/drive_c/Program Files/Microsoft SQL Server Compact Edition/v3.5"; do
if [[ -d "$entombedCandidateSqlDir" ]]; then
entombedSqlDir="$entombedCandidateSqlDir"
break
fi
done
if [[ -z "$entombedSqlDir" ]]; then
agm_msgbox "Entombed" "Entombed" "SQL Server Compact v3.5 did not install to the expected location."
exit 1
fi
cp "${entombedSqlDir}/Private/System.Data.SqlServerCe.Entity.dll" "$entombedInstallDir/"
cp "${entombedSqlDir}/Private/System.Data.SqlServerCe.dll" "$entombedInstallDir/"
cp "${entombedSqlDir}"/sql* "$entombedInstallDir/"
cp "${cache}/Entombed.exe.config" "${entombedInstallDir}/Entombed.exe.config"
if [[ ! -f "${WINEPREFIX}/drive_c/windows/system32/mfplat.dll" ]]; then
cp "${cache}/mfplat.dll" "${WINEPREFIX}/drive_c/windows/system32/"
fi
add_launcher "c:\Program Files (x86)\Entombed\Entombed.exe"
add_umu_launcher "$entombedGameId" "$entombedPath" "export PROTON_USE_XALIA=0"
+2 -1
View File
@@ -1,4 +1,5 @@
download "http://files.l-works.net/judgmentdayfullsetup.exe"
# shellcheck shell=bash disable=SC2154 # cache is set by audiogame-manager
download "https://www.l-works.net/files/judgmentdayfullsetup.exe"
install_wine_bottle vb6run dx8vb quartz
wine "${cache}/judgmentdayfullsetup.exe" /silent
cat << EOF > /tmp/judgementday.reg
+95
View File
@@ -0,0 +1,95 @@
export WINEARCH=win64
# shellcheck disable=SC2154 # installer is sourced by audiogame-manager with shared globals
export winVer="win10"
game="${game:-Magic: The Gathering Arena}"
mtgaVersionUrl="https://mtgarena.downloads.wizards.com/Live/Windows32/version"
accessibleArenaDllUrl="https://github.com/JeanStiletto/AccessibleArena/releases/latest/download/AccessibleArena.dll"
melonLoaderZipUrl="https://github.com/LavaGang/MelonLoader/releases/latest/download/MelonLoader.x64.zip"
tolkDllUrl="https://stormgames.wolfe.casa/downloads/Tolk.dll"
get_mtga_installer_url() {
local versionJson=""
local installerUrl=""
if ! versionJson="$(curl -fsSL "$mtgaVersionUrl")"; then
alert "Magic: The Gathering Arena" "Magic: The Gathering Arena" "Could not fetch the current MTG Arena installer URL."
exit 1
fi
installerUrl="$(sed -n 's/.*"CurrentInstallerURL"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' <<< "$versionJson" | head -n1)"
if [[ -z "$installerUrl" ]]; then
alert "Magic: The Gathering Arena" "Magic: The Gathering Arena" "Could not parse the current MTG Arena installer URL."
exit 1
fi
printf '%s\n' "$installerUrl"
}
download_mtga_installer() {
local installerUrl="$1"
local installerFile="${installerUrl##*/}"
installerFile="${installerFile%%\?*}"
[[ -n "$installerFile" ]] || installerFile="MTGAInstaller.msi"
# shellcheck disable=SC2154 # cache is set by audiogame-manager before installers are sourced
if [[ "${redownload:-}" == "true" ]] || [[ ! -s "${cache}/${installerFile}" ]]; then
if ! curl -L4 -C - --retry 10 --output "${cache}/${installerFile}" "$installerUrl" 2>&1 | agm_progressbox "Magic: The Gathering Arena" "Downloading MTG Arena installer..."; then
alert "Magic: The Gathering Arena" "Magic: The Gathering Arena" "Could not download the MTG Arena installer."
exit 1
fi
fi
mtgaInstallerPath="${cache}/${installerFile}"
}
configure_accessible_arena_loader() {
local mtgaRoot="$1"
local userDataPath="${mtgaRoot}/UserData"
local loaderConfig="${userDataPath}/Loader.cfg"
mkdir -p "$userDataPath"
if [[ -f "$loaderConfig" ]]; then
if grep -Fq "hide_console = false" "$loaderConfig"; then
sed -i 's/hide_console = false/hide_console = true/g' "$loaderConfig"
elif ! grep -Fq "hide_console" "$loaderConfig"; then
printf '\n[console]\nhide_console = true\n' >> "$loaderConfig"
fi
else
printf '[console]\nhide_console = true\n' > "$loaderConfig"
fi
}
install_accessible_arena_support() {
local mtgaRoot="$1"
if [[ -z "${nvdaControllerClient64Dll:-}" ]]; then
# shellcheck disable=SC2154 # ipfs is sourced through audiogame-manager helpers
nvdaControllerClient64Dll="${ipfs[nvdaControllerClient64]}"
fi
download "$accessibleArenaDllUrl" "$melonLoaderZipUrl" "$tolkDllUrl" "$nvdaControllerClient64Dll"
install_with_progress unzip "Installing MelonLoader..." -d "$mtgaRoot" "${cache}/MelonLoader.x64.zip"
mkdir -p "${mtgaRoot}/Mods"
install_with_progress cp "Installing Accessible Arena..." "${cache}/AccessibleArena.dll" "${mtgaRoot}/Mods/AccessibleArena.dll"
install_with_progress cp "Installing screen reader support DLLs..." "${cache}/Tolk.dll" "${cache}/nvdaControllerClient64.dll" "$mtgaRoot"
configure_accessible_arena_loader "$mtgaRoot"
}
install_wine_bottle dxvk
winetricks -q $winVer
mtgaRoot="${WINEPREFIX}/drive_c/Program Files (x86)/Wizards of the Coast/MTGA"
mtgaInstallerUrl="$(get_mtga_installer_url)"
download_mtga_installer "$mtgaInstallerUrl"
wine msiexec /i "$mtgaInstallerPath" /q
if [[ ! -f "${mtgaRoot}/MTGA.exe" ]]; then
alert "Magic: The Gathering Arena" "Magic: The Gathering Arena" "MTG Arena installation did not finish at the expected location: ${mtgaRoot}"
exit 1
fi
install_accessible_arena_support "$mtgaRoot"
add_launcher 'c:\Program Files (x86)\Wizards of the Coast\MTGA\MTGA.exe' 'export WINEDLLOVERRIDES=version=n,b'
+7 -7
View File
@@ -1,12 +1,12 @@
# shellcheck shell=bash disable=SC2154 # cache and WINEPREFIX are set by audiogame-manager
export WINEARCH="win64" # Migrated to wine64
export winVer="win7"
get_installer "Mist World_Setup.exe" "https://drive.google.com/uc?export=download&id=12YeUqorkkMT46ZSR5pcfWxSY8DHOLxZ-"
get_installer "MistWorld_Setup_260323.exe" "https://download.mwgame.net/MistWorld_Setup_260323.exe"
install_wine_bottle
install_with_progress 7z "Extracting game files..." x -o"$WINEPREFIX/drive_c/Program Files/Mist World" "$cache/Mist World_Setup.exe"
sed -i 's/1024m/768m/g' "$WINEPREFIX/drive_c/Program Files/Mist World/mw.exe.vmoptions"
cp "$WINEPREFIX/drive_c/Program Files/Mist World/"{mw.exe.vmoptions,update.exe.vmoptions}
mkdir "$WINEPREFIX/drive_c/Program Files/Mist World/"{user,users}
add_launcher 'c:\Program Files\Mist World\mw.exe'
install_with_progress 7z "Extracting game files..." x -o"$WINEPREFIX/drive_c/Program Files (x86)/Mist World" "$cache/MistWorld_Setup_260323.exe"
sed -i 's/1024m/768m/g' "$WINEPREFIX/drive_c/Program Files (x86)/Mist World/mw.exe.vmoptions"
cp "$WINEPREFIX/drive_c/Program Files (x86)/Mist World/"{mw.exe.vmoptions,update.exe.vmoptions}
mkdir "$WINEPREFIX/drive_c/Program Files (x86)/Mist World/"{user,users}
add_launcher 'c:\Program Files (x86)\Mist World\mw.exe'
alert "If you do not have an account, There is a script in game-scripts to help.\nLaunch the game, press enter on create account, then drop into a console so the game window does not lose focus.\nChange to the game-scripts directory and run\n./mist_world_account_creator.sh and follow the prompts.\n\nTo login, type your email address, press tab, and type your password.\nIf you want to enable automatic login, press tab two times followed by space, then tab and enter.\nIf you do not want to auto login, you can just press enter after typing your password."
+35 -20
View File
@@ -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_crlf_file "${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."
+8
View File
@@ -0,0 +1,8 @@
export winetricksSettings="vd=1024x768"
install_wine_bottle
download "https://www.kaldobsky.com/audiogames/swamp2.zip"
install_with_progress unzip "Extracting game files..." -d "${WINEPREFIX}/drive_c/Program Files/swamp2" "${cache}/swamp2.zip"
add_launcher "c:\Program Files\swamp2\swamp2.exe"
alert "This game has native support for NVDA, but it is not the default.\nTo set it up so that it will talk to you, you need to press the tab key immediately after you launch the game."
+105
View File
@@ -0,0 +1,105 @@
# AGENTS.md
## Project Purpose
Audiogame Manager installs and launches Windows audio games under Wine or UMU/Proton on Linux. Contributions must preserve keyboard and screen-reader accessibility in both console `dialog` and graphical `yad` modes.
## Repository Map
- `audiogame-manager.sh`: main entry point, game menu, launcher, removal flow, and shared runtime state.
- `.install/`: one sourced Bash installer per game. The filename, minus `.sh`, is the game name shown in the install menu.
- `.includes/`: shared bottle, UMU, download, dialog, desktop, help, update, and URL helpers.
- `.includes/ipfs.sh`: centralized IPFS URLs for core files and games.
- `game-scripts/`: scripts installed or used by particular games after installation.
- `speech/`: speech-related helper and setup scripts.
- `wine/`: distribution-specific dependency installers and Wine utilities.
- `tests/`: isolated shell regression tests with mocked external programs.
## Runtime Architecture
- This is a Bash project, not a POSIX `sh` project. Arrays, associative arrays, `mapfile`, `[[ ... ]]`, and Bash parameter expansion are used intentionally.
- Installer files in `.install/` are sourced by the main process. They share functions and exported state from `audiogame-manager.sh` and `.includes/`; they are not independent programs.
- Common installer state includes `game`, `cache`, `WINEPREFIX`, `WINEARCH`, `winetricksSettings`, and helper functions such as `download`, `install_wine_bottle`, `install_proton_bottle`, `install_with_progress`, `add_launcher`, and `add_umu_launcher`.
- The launcher configuration is pipe-delimited. Keep its field order compatible with `create_game_array()` and `process_launcher_flags()`.
- Native Wine and UMU/Proton are separate backends. Use the helpers for the selected backend; do not mix their bottle paths, environment setup, launcher functions, or shutdown functions.
- The main script performs dependency checks, bottle setup, update checks, and other startup work before command dispatch. Do not source it casually in tests. Source the smallest `.includes/` file needed and mock its external commands.
## Game Installer Conventions
- Name a new installer `.install/Game Name.sh`; that filename becomes the menu label.
- A first line beginning with `#//` hides an installer from the menu. Preserve this convention when editing disabled installers.
- Quote paths and expansions, especially game names and Windows paths containing spaces.
- Use `download` so caching, progress reporting, retries, and validation remain consistent.
- Use `install_with_progress` for archive extraction or copies that could otherwise prompt invisibly. Extraction must be non-interactive and safe to repeat.
- Use `install_wine_bottle` plus `add_launcher` for the Wine backend.
- Use `install_proton_bottle`, the UMU helpers, and `add_umu_launcher` for the UMU backend.
- After installation, verify the expected executable exists before recording a launcher when failure would otherwise produce a broken menu entry.
- Set game-specific environment or winetricks values in the installer rather than changing global defaults for one game.
- Prefer idempotent installation steps. Re-running an installer should not hang on overwrite prompts or silently corrupt an existing bottle.
- Do not delete a shared Wine or Proton bottle to remove one game. Removal code must target only the selected game's files and launcher entry.
## Portability and Dependencies
- Contributors and coding agents may use any locally installed tools, including ripgrep (`rg`), while searching, reviewing, testing, or editing the repository. This restriction applies only to commands invoked by scripts shipped to users.
- Keep commands used by shipped scripts portable. Do not make runtime code depend on ripgrep or other modern command-line tools that are not commonly installed by default when classic Unix tools can provide the required behavior.
- Prefer broadly available classic Unix tools such as `grep`, `sed`, `awk`, and `find` when they provide the required behavior.
- A nonstandard runtime dependency is acceptable when there is no practical portable alternative, but it must be declared and checked rather than assumed.
- Add every new runtime dependency to `.includes/checkup.sh`, including its `packageList` entry so `audiogame-manager.sh -P` reports it. Update the relevant distribution-specific dependency scripts under `wine/` when they manage packages for that platform.
- If Audiogame Manager cannot perform its basic startup or core functions without a dependency, also add it to the startup checks in `check_requirements()` alongside critical commands such as `sox` and `dialog`.
- Do not assume a developer's interactive shell aliases, local utilities, desktop session, or current working directory are available.
- Resolve repository files relative to `scriptDir` or `BASH_SOURCE`, as appropriate.
- Do not add compatibility fallbacks or legacy paths unless they are an explicit requirement.
## Shell Style
- Follow the surrounding file's style and keep edits narrowly scoped.
- For new code, use camelCase variables and snake_case functions. Use PascalCase only for class-like concepts if any are introduced.
- Quote variable expansions unless intentional splitting or pattern matching is required.
- Prefer arrays for argument lists; do not construct commands in strings and evaluate them.
- Treat sourced shared globals deliberately. Add a focused ShellCheck suppression with a reason when a value is populated by the caller; do not broadly silence actionable warnings.
- Logging timestamps follow the message: `message [date]`.
- Do not add colored output unless requested.
## Accessibility and Interaction
- Screen-reader and keyboard users are first-class users.
- Use the `agm_*` wrappers from `.includes/dialog-interface.sh` instead of invoking `dialog` or `yad` directly. Changes must continue to work in both interfaces.
- Keep every workflow operable without a mouse. Do not introduce keyboard traps or communicate state only through color, sound, or visual layout.
- Do not use `spd-say` or direct Speech Dispatcher calls in graphical interfaces. Expose information through accessible controls and the existing dialog wrappers.
- Avoid commands that can stop at an invisible prompt behind a progress box. Supply non-interactive flags and handle failures explicitly.
- When changing accessibility behavior, verify the exact affected console and GUI workflow when those environments are available. Automated shell checks do not prove live screen-reader behavior.
## Downloads, URLs, and External State
- Treat remote URLs, archive layouts, executable names, and installer behavior as changeable external state. Verify them when working on a download or installer rather than relying on an old report.
- Keep reusable IPFS references centralized in `.includes/ipfs.sh` and preserve their explicit `filename` query when the cache filename depends on it.
- Never include credentials, private tokens, personal usernames, or live user paths in code, fixtures, logs, or examples.
- Do not run destructive installer or removal tests against the real home directory, Wine prefixes, or game data.
## Testing and Verification
- For every edited Bash or `.sh` file, run:
```bash
bash -n path/to/file.sh
shellcheck path/to/file.sh
```
- Fix real ShellCheck findings. A narrow suppression is acceptable for intentionally sourced globals or dynamic source paths when it includes a reason.
- Run the smallest relevant test under `tests/`. Tests must use a temporary directory, replace external programs with stubs, and avoid network, GUI, Wine, and real user-state changes.
- For UMU helper changes, run:
```bash
bash tests/umu_backend_tests.sh
```
- For changes spanning many shell files, syntax-check every changed shell file rather than assuming one successful check covers sourced code.
- Before handing work back, run `git diff --check` and inspect `git status --short --untracked-files=all` plus the final diff.
- Distinguish automated verification from live acceptance. Installer, Wine, audio, focus, controller, and screen-reader behavior may still require a real installation or launch test.
## Repository Hygiene
- Preserve unrelated tracked and untracked work. Do not reset, clean, overwrite, or incorporate files outside the requested scope.
- Do not edit generated caches, Wine prefixes, downloaded game data, or logs as source changes.
- Keep contributor changes reviewable; avoid unrelated formatting or refactors in installer fixes.
- Do not commit, merge, push, or alter remote state unless explicitly requested.
-265
View File
@@ -1,265 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
**Critical**: Be sure to keep this file up to date with new changes.
## Project Overview
**Audiogame Manager** is a comprehensive bash-based installer and launcher system for Windows audio games running under Wine on Linux. The project focuses on accessibility, providing speech synthesis support and screen reader compatibility for audio games designed for blind and visually impaired users.
## Architecture
### Core Components
- **Main Script**: `audiogame-manager.sh` - Entry point providing interactive menus for game installation, launching, and management with Wine32/64 bottle support
- **Includes Directory**: `.includes/` - Modular helper scripts containing core functionality
- `functions.sh` - Core utilities (download, cache management, validation, requirements checking)
- `dialog-interface.sh` - UI abstraction layer supporting both console (dialog) and GUI (yad) modes
- `bottle.sh` - Wine bottle management, RHVoice installation, and prefix management
- `checkup.sh` - System dependency validation
- `help.sh` - Documentation system
- `desktop.sh` - Desktop integration
- `update.sh` - Auto-update mechanisms
- **Game Scripts**: `game-scripts/` - Individual game update/launch scripts
- **Install Scripts**: `.install/` - Game installation scripts (100+ games supported)
- **Speech Integration**: `speech/` - TTS and accessibility tools
- `set-voice.sh` - Voice configuration with test functionality
- Supports multiple TTS engines (RHVoice, SAPI, Cepstral)
- Per-bottle voice configuration system
- NVDA screen reader compatibility through custom DLL
- **Wine Integration**: `wine/` - Wine setup utilities including bottle creation and dependency installation
- `mkwine.sh` - Wine bottle creation
- Distribution-specific dependency installers
### Wine Architecture
The project uses Wine with a unified architecture:
- **Wine64 Only**: All games use wine64+WOW64 exclusively - it runs both 32-bit and 64-bit applications efficiently
- **Wine32 ELIMINATED**: As of 2025-12-06, wine32 bottle is no longer created or used
- **Unified bottle**: Single wine64 prefix (stored in `~/.local/wine64`) for ALL games, including SAPI games
- **Custom bottles**: Can be stored in `~/.local/share/audiogame-manager/wineBottles/` with per-bottle configurations in `~/.config/audiogame-manager/`
- **IMPORTANT**: Never create game-specific wine directories like `~/.local/winegamename` - use the standard bottle system
#### SAPI and Speech SDK Support (WINETRICKS_FORCE=1)
**Discovery (2025-12-06)**: Setting `WINETRICKS_FORCE=1` enables reliable speechsdk installation in wine64+WOW64 bottles, eliminating the need for wine32 for most SAPI-dependent games.
**Implementation:**
- `audiogame-manager.sh:96` - wine64 bottle creation includes speechsdk with `WINETRICKS_FORCE=1`
- `audiogame-manager.sh:99` - Restores win10 after speechsdk (see caveat below)
- `.includes/bottle.sh:176-181` - Only speechsdk installations use `WINETRICKS_FORCE=1` (other deps use regular winetricks)
- Both bottles install Microsoft Mike as the default SAPI voice automatically
**CAVEAT**: The `speechsdk` winetricks verb sets `w_set_winver winxp` at the end, trampling any previously set Windows version. Always call `winetricks win10` AFTER installing speechsdk.
**Migrated SAPI Games (wine64):**
- Bloodshed - Tested and confirmed working
- Kitchensinc Games - Tested and confirmed working (VB6)
- Oh Shit - Migrated, requires testing
- Dog Who Hates Toast - Migrated, requires testing (VB6)
- Lunimals - Migrated, requires testing (VB6)
- VIP Mud - Migrated, requires testing (VB6)
- Entombed - Migrated, requires testing (complex .NET dependencies)
- Skateboarder Pro - Already configured for wine64, now functional
- Three D velocity - Already configured for wine64, now functional
**Migrated Self-Voicing Games (wine64):**
- Shadow Line - Migrated, requires Windows 8 via per-app versioning
- Villains From Beyond - Tested and confirmed working
#### Per-Application Windows Version
**Problem (2025-12-08)**: Some games require specific Windows versions (e.g., Shadow Line needs win8) but with a unified bottle, `winetricks winXX` changes the version globally for ALL games.
**Solution**: Use Wine's per-application settings via registry keys:
```
[HKEY_CURRENT_USER\Software\Wine\AppDefaults\executable.exe]
"Version"="win8"
```
**Implementation:**
- Game installers set `export winVer="win8"` (or win7, win10)
- `install_wine_bottle()` stores this in `gameWinVer` (doesn't apply globally)
- `add_launcher()` calls `set_app_winver()` which writes the per-exe registry key
- Located in `.includes/bottle.sh:186-209` (`set_app_winver` function)
**Architecture Selection Logic:**
- Games explicitly setting `WINEARCH=win64` will use wine64 (new behavior)
- Games passing `sapi` dependency use wine64 with speechsdk pre-installed
- Legacy games passing `speechsdk` dependency still use wine32 for compatibility
- Default architecture is wine64 for all new games
### Interface System
Dialog interface automatically detects environment:
- Console mode: Uses `dialog` for accessibility
- GUI mode: Uses `yad` when DISPLAY is available
- All UI functions are prefixed with `agm_` (audiogame manager)
## Development Commands
### System Check
```bash
./audiogame-manager.sh -c # Check system requirements and dependencies
bash -n audiogame-manager.sh # Basic syntax check
```
### Installation and Usage
```bash
./audiogame-manager.sh # Launch installed games menu
./audiogame-manager.sh -i # Install games (interactive menu)
./audiogame-manager.sh -I "Game Name" # Install a game noninteractively
./audiogame-manager.sh -h # Show help
```
### Wine Bottle Management
```bash
./wine/mkwine.sh [bottle_name] [architecture] # Create a new Wine bottle
```
### Voice and Speech Testing
```bash
./speech/set-voice.sh # Configure and test voice settings
./speech/set-voice.sh [bottle_name] # Configure voice for a specific bottle
```
### Dependency Management
```bash
# Check all required packages
./.includes/checkup.sh packages
# Install Wine dependencies for different distros
./wine/install-dependencies-arch.sh
./wine/install-dependencies-debian.sh
```
### Testing All Include Files
```bash
for f in .includes/*.sh; do bash -n "$f"; done
```
## Key Patterns and Conventions
### Coding Style - **EXTREMELY IMPORTANT - MUST BE FOLLOWED**
- **Variables**: Use camelCase for variable names (e.g., `gameTitle`, `wineBottle`, `installPath`)
- **CRITICAL**: ALL variables must use camelCase - no exceptions
- Examples: `selectedGame`, `wineArch`, `installPath`, `downloadUrl`
- Never use: `selected_game`, `wine_arch`, `install_path`, `download_url`
- **Functions**: Use snake_case for function names (e.g., `install_game`, `create_wine_bottle`, `check_dependencies`)
- **CRITICAL**: ALL functions must use snake_case - no exceptions
- Examples: `get_wine_bottle()`, `process_launcher_flags()`, `download_file()`
- Never use: `getWineBottle()`, `processLauncherFlags()`, `downloadFile()`
- **Shebang**: Use `#!/bin/bash` for all bash scripts
- **CRITICAL**: .sh files in .install/ are game installers, not typical bash scripts If they contain # on the first line they are disabled and will not show up in audiogame-manager
- **Sourcing pattern**:
- **Main script** (`audiogame-manager.sh`): Use `source "${scriptDir}/.includes/file.sh"` (scriptDir is defined at line 4)
- **Subdirectory scripts** (game-scripts/, wine/, speech/): Use `source "${0%/*}/../.includes/file.sh"`
- **CRITICAL**: Never use relative paths like `source .includes/file.sh` - these fail when the current working directory differs from the script location
- **Indentation**: Use consistent indentation (tabs or spaces, follow existing file patterns)
- When fixing code, correct any indentation inconsistencies to match the established style
### Error Handling
- Functions return 0 for success, non-zero for failure
- Use consistent exit codes throughout scripts
- Provide clear error messages with context
- Critical errors vs warnings are clearly distinguished in checkup system
- Progress feedback is provided for all long-running operations
- Always clean up temporary files on exit
- Log important operations for debugging
### File Structure
- Game installers follow naming convention: `.install/Game Name.sh`
- **IMPORTANT**: Games starting with hash (#) are commented out even if it's a comment
- First line of a game installer may not start with # unless we're excluding it
- Cache directory: `~/.local/share/audiogame-manager/cache/`
- Wine bottles: `~/.local/wine32/` and `~/.local/wine64/`
- Custom bottles: `~/.local/share/audiogame-manager/wineBottles/`
- Configuration: `~/.config/audiogame-manager/`
### UI Functions
All dialog functions in `.includes/dialog-interface.sh` follow pattern:
- `agm_menu()` - Selection menus
- `agm_msgbox()` - Message display
- `agm_yesno()` - Confirmation dialogs
- `agm_progressbox()` - Progress display
- Functions automatically adapt to console/GUI environment
### Game Integration
- Each game has both an installer (`.install/`) and optional update script (`game-scripts/`)
- Games are categorized by engine type and requirements
- SAPI games automatically use Wine32, others use Wine64
- Games are tracked in configuration files with Wine bottle associations
- Each game can use different Wine bottles with specific configurations
### Game Installation Pattern
1. Check system requirements and dependencies
2. Create or verify Wine bottle
3. Install Wine dependencies via winetricks
4. Download and validate game files (use checksums when available)
5. Configure speech synthesis (typically RHVoice)
6. Add game entry to launcher configuration
### Accessibility Features
- Sound alerts using `sox` for important notifications
- Screen reader compatibility through proper dialog usage
- Keyboard navigation support throughout interface
- Voice testing integrated into setup process
## Dependencies
### Critical (Required)
- wine, curl, dialog, sox
- Archive tools: 7z, cabextract, unzip, xz
- winetricks (for Wine component management)
### Optional (Warnings if missing)
- gawk (for game removal)
- ocrdesktop (installer debugging)
- qjoypad (gamepad support)
- translate-shell, sqlite3, perl (translation features)
- w3m (documentation viewing)
- xclip, xdotool (X11 integration)
### Platform Support
- x86_64 Linux (primary)
- aarch64 with FEX-Emu (alternative Wine implementation)
## Testing
- System requirements: `./audiogame-manager.sh -c`
- Voice functionality: Use built-in voice test in `set-voice.sh`
- Game installation: Test with simple games first before complex ones
- Wine bottle integrity: Check `~/.local/wine32/system.reg` and `~/.local/wine64/system.reg` exist
- Test with different Wine versions when modifying bottle creation
- Verify speech synthesis functionality after TTS changes
- Test game installation scripts in clean environments
- Check both 32-bit and 64-bit compatibility where applicable
## Recent Refactor Notes (2025)
### Major Refactor Status
The project has undergone a significant refactor to modularize functionality. **Status: Largely Complete**
### Key Fixes Applied
1. **Function naming**: Fixed `process_launcher-flags()``process_launcher_flags()` in `audiogame-manager.sh:270`
2. **Variable scope**: Added `export game` in main script so `.includes/bottle.sh` functions can access it
3. **Installation logic**: Completed the `-I` option implementation for noninteractive game installation
4. **Code deduplication**: Removed duplicate `check_news` and launcher logic from `update.sh`
5. **Include sourcing**: Fixed all relative path sourcing (e.g., `source .includes/bottle.sh`) to use `${scriptDir}` to ensure desktop launchers and execution from any working directory works correctly
### Critical Variable Handling
- **`$game` variable**: Must be exported when set (line 489 in main script) for bottle.sh functions to work
- **`agmNoLaunch` variable**: Used to prevent main script execution when sourced by other scripts
- **Bottle names**: Derived from game names, converted to lowercase with spaces replaced by hyphens
### Important Patterns
- **Noninteractive installation**: The `-I` option sources the appropriate `.install/GameName.sh` script
- **Variable scope**: Include files rely on exported variables from main script
- **Wine bottle logic**: Game-specific wine versions are handled in `bottle.sh:get_bottle()`
### Common Issues to Watch For
1. **Variable exports**: Ensure variables are exported when needed by include files
2. **Function naming**: Use snake_case for functions, camelCase for variables
3. **Path quoting**: Always quote paths that might contain spaces
4. **Duplicate logic**: Check main script vs includes to avoid redundant code
+73 -46
View File
@@ -33,7 +33,7 @@ start_nvda2speechd() {
if [[ "$nvda2speechdStarted" == "true" ]]; then
return
fi
if ! ss -ltnp | rg 3457 | grep -q 'cthulhu'; then
if ! ss -ltnp | grep 3457 | grep -q 'cthulhu'; then
if [[ -x "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" ]]; then
local translateSetting="${TRANSLATE:-unset}"
local translateFromSetting="${TRANSLATE_FROM:-unset}"
@@ -285,11 +285,14 @@ check_wine32() {
# Ensure wine64 bottle exists with all dependencies (wine32 no longer needed!)
ensure_wine_bottles() {
local wine64Bottle="$HOME/.local/wine64"
local setupMarker="${wine64Bottle}/.agm-winespeak-setup-pending"
# Create wine64 bottle if missing - now includes SAPI support via WINETRICKS_FORCE=1
if [[ ! -d "$wine64Bottle" ]] || [[ ! -f "$wine64Bottle/system.reg" ]]; then
if [[ ! -d "$wine64Bottle" ]] || [[ ! -f "$wine64Bottle/system.reg" ]] || [[ -f "$setupMarker" ]]; then
{
echo "# Creating wine64 bottle for modern games..."
mkdir -p "$wine64Bottle"
touch "$setupMarker"
# Set up environment for wine64
export WINEPREFIX="$wine64Bottle"
@@ -329,22 +332,15 @@ ensure_wine_bottles() {
# Restore win10 after speechsdk (speechsdk sets winxp)
winetricks -q win10
# Initialize SAPI and set Microsoft Mike as default voice
echo "# Setting Microsoft Mike as default voice..."
mkdir -p "${WINEPREFIX}/drive_c/windows/temp"
cat << "EOF" > "${WINEPREFIX}/drive_c/windows/temp/init_sapi.vbs"
dim speechobject
set speechobject=createobject("sapi.spvoice")
speechobject.speak ""
EOF
wine cscript "c:\\windows\\temp\\init_sapi.vbs"
wine reg add "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTokenId" /t REG_SZ /d "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\MSMike" /f
wine reg add "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTTSRate" /t REG_DWORD /d "7" /f
echo "Set Microsoft Mike as default voice for wine64"
# Install and verify the default 32-bit SAPI voice.
echo "# Installing WineSpeak English (America)..."
if ! install_winespeak; then
echo "# WineSpeak installation or SAPI verification failed."
return 1
fi
# Setup nvda2speechd for accessibility if needed
if ! ss -ltnp | rg 3457 | grep -q 'cthulhu'; then
if ! ss -ltnp | grep 3457 | grep -q 'cthulhu'; then
echo "# Setting up accessibility support..."
download "${nvda2speechdBinary}"
if [[ ! -f "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" ]]; then
@@ -353,6 +349,7 @@ EOF
fi
fi
rm -f "$setupMarker"
echo "# Wine64 bottle creation complete."
} > >(agm_progressbox "Wine Bottle Setup" "Creating unified wine64 bottle with SAPI support (this may take several minutes)...")
fi
@@ -469,23 +466,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 +544,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
@@ -550,14 +567,14 @@ custom_launch_parameters() {
fi
start_nvda2speechd
pushd "$(winepath "$winePath")" || exit 1
wine "$wineExec"
run_with_optional_gamemode wine "$wineExec"
popd || exit 1
customLaunchHandled="true"
return 0
fi
if [[ "${game[2]}" == "Dreamland" ]]; then
start_nvda2speechd
"$WINE" "${game[1]}" &> /dev/null
run_with_optional_gamemode "$WINE" "${game[1]}" &> /dev/null
customLaunchHandled="true"
return 0
fi
@@ -617,20 +634,20 @@ custom_launch_parameters() {
# sketchbook: DLL replacement now handled by update_nvda_dlls()
if [[ "${game[2]}" == "Audiodisc" ]]; then
start_nvda2speechd
wine "$winePath\\$wineExec"
run_with_optional_gamemode wine "$winePath\\$wineExec"
customLaunchHandled="true"
return 0
fi
if [[ "${game[2]}" == "Audioquake" ]]; then
start_nvda2speechd
wine "$winePath\\$wineExec"
run_with_optional_gamemode wine "$winePath\\$wineExec"
customLaunchHandled="true"
return 0
fi
if [[ "${game[2]}" == "Screaming Strike 2" ]]; then
start_nvda2speechd
pushd "$(winepath "$winePath")" || exit 1
wine "$wineExec"
run_with_optional_gamemode wine "$wineExec"
popd || exit 1
customLaunchHandled="true"
return 0
@@ -638,7 +655,7 @@ custom_launch_parameters() {
if [[ "${game[2]}" == "Warsim" ]]; then
start_nvda2speechd
pushd "$(winepath "${game[1]%\\*}")" || exit 1
wine "${game[1]##*\\}"
run_with_optional_gamemode wine "${game[1]##*\\}"
popd || exit 1
customLaunchHandled="true"
return 0
@@ -646,7 +663,7 @@ custom_launch_parameters() {
if [[ "${game[2]}" == "Interceptor" ]]; then
start_nvda2speechd
pushd "$(winepath "$winePath")" || exit 1
wine "$wineExec"
run_with_optional_gamemode wine "$wineExec"
popd || exit 1
customLaunchHandled="true"
return 0
@@ -667,12 +684,11 @@ create_game_array() {
# Game array 0 bottle, 1 path, 2 title, 3+ flags
local selectedGame="$1"
for i in "${lines[@]}" ; do
# Only compare the launcher section
local j="${selectedGame#*|}"
local k="${i#*|}"
k="${k%%|*}"
if [[ "$j" == "$k" ]]; then
IFS='|' read -ra game <<< "$i"
local selectedLauncher="${selectedGame#*|}"
local candidate=()
IFS='|' read -ra candidate <<< "$i"
if [[ "$selectedLauncher" == "${candidate[1]}" || "$selectedGame" == "${candidate[2]}" ]]; then
game=("${candidate[@]}")
break
fi
done
@@ -790,14 +806,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 +835,6 @@ game_launcher() {
fi
fi
fi
process_launcher_flags
apply_executioners_rage_focus_workaround
customLaunchHandled="false"
custom_launch_parameters
@@ -824,11 +846,11 @@ game_launcher() {
# Change to game directory before launching (required for proper game operation)
pushd "$(winepath "${game[1]%\\*}")" > /dev/null || exit 1
if [[ "$debugGdb" == "1" ]]; then
winedbg --gdb "${game[1]##*\\}"
run_with_optional_gamemode winedbg --gdb "${game[1]##*\\}"
else
# Use direct wine execution instead of 'wine start' to ensure clipboard works
# See: https://bugs.winehq.org/show_bug.cgi?id=50598
wine "${game[1]##*\\}"
run_with_optional_gamemode wine "${game[1]##*\\}"
fi
popd > /dev/null || exit 1
restore_executioners_rage_focus_workaround
@@ -901,6 +923,10 @@ 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/gamemode.sh
source "${scriptDir}/.includes/gamemode.sh"
# 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
@@ -916,13 +942,14 @@ source "${scriptDir}/.includes/update.sh"
export nvdaControllerClient32Dll="${ipfs[nvdaControllerClient32]}"
export nvdaControllerClient64Dll="${ipfs[nvdaControllerClient64]}"
export nvda2speechdBinary="${ipfs[nvda2speechd]}"
export wineSpeakInstaller="${ipfs[winespeak]}"
# Check minimum requirements
check_requirements || exit 1
# Wine32 no longer needed - all games use wine64 with SAPI support via WINETRICKS_FORCE=1
# check_wine32 # Disabled - wine32 eliminated 2025-12-06
# Ensure wine bottles exist with dependencies
ensure_wine_bottles
ensure_wine_bottles || exit 1
# Check for updates
update
# Get latest news if available
@@ -0,0 +1,525 @@
# UMU Proton Backend Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a reusable UMU/Proton backend and migrate Shadow Line to it with English localization and NVDA controller DLL replacement.
**Architecture:** Keep the existing Wine backend intact and add UMU as a new launcher backend value in `games.conf`. Put generic UMU behavior in a focused include file, keep Shadow Line-specific install work in `.install/Shadow Line.sh`, and route launch/removal/kill behavior through backend-aware branches.
**Tech Stack:** Bash, `umu-run`, Wine/Proton prefixes, existing AGM dialog/progress helpers, `shellcheck`.
---
## File Structure
- Create: `.includes/proton.sh`
- Owns UMU dependency checks, prefix selection, UMU launch environment, Windows path conversion, UMU installer/launch helpers, and UMU launcher registration.
- Create: `tests/umu_backend_tests.sh`
- Self-contained shell tests with temporary HOME/config/cache paths and stubbed external commands.
- Modify: `audiogame-manager.sh`
- Source `.includes/proton.sh`, skip Wine-only DLL scanning for UMU prefixes, launch UMU entries through `run_umu_game`, and handle UMU kill/remove paths safely.
- Modify: `.includes/checkup.sh`
- Report `umu-run` as required for Proton-backed games and include `umu-launcher` in package output.
- Modify: `.install/Shadow Line.sh`
- Use `install_proton_bottle`, run installer via UMU, apply registry settings, copy English language file, replace NVDA controller DLLs, register with `add_umu_launcher`, and show first-run English instructions.
## Task 1: Add UMU Backend Tests
**Files:**
- Create: `tests/umu_backend_tests.sh`
- [ ] **Step 1: Write failing shell tests**
Create `tests/umu_backend_tests.sh`:
```bash
#!/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 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'
```
- [ ] **Step 2: Run tests and verify they fail because `.includes/proton.sh` is missing**
Run: `bash tests/umu_backend_tests.sh`
Expected: FAIL with a message that `.includes/proton.sh` cannot be sourced.
## Task 2: Implement Generic UMU Helpers
**Files:**
- Create: `.includes/proton.sh`
- Test: `tests/umu_backend_tests.sh`
- [ ] **Step 1: Implement `.includes/proton.sh`**
Create `.includes/proton.sh`:
```bash
#!/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}"
export UMU_RUNTIME_UPDATE="${UMU_RUNTIME_UPDATE:-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"
wine 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
}
```
- [ ] **Step 2: Run UMU backend tests and verify they pass**
Run: `bash tests/umu_backend_tests.sh`
Expected: PASS with `UMU backend tests passed`.
- [ ] **Step 3: Run shellcheck on new files**
Run: `shellcheck .includes/proton.sh tests/umu_backend_tests.sh`
Expected: no output and exit code 0.
## Task 3: Wire UMU Backend Into AGM Launch, Kill, and Removal
**Files:**
- Modify: `audiogame-manager.sh`
- Test: `tests/umu_backend_tests.sh`
- [ ] **Step 1: Source proton helpers**
In `audiogame-manager.sh`, after sourcing `.includes/bottle.sh`, add:
```bash
# shellcheck source=.includes/proton.sh
source "${scriptDir}/.includes/proton.sh"
```
- [ ] **Step 2: Make removal backend-aware**
In `remove_game`, after `create_game_array "$selectedGame"` and before Wine-only setup, branch on `game[0]`:
```bash
if [[ "${game[0]}" == "umu" ]]; then
process_launcher_flags
get_umu_bottle "$umuGameId"
else
source "${scriptDir}/.includes/bottle.sh"
get_bottle "${game[0]}"
fi
```
For directory conversion, replace the Wine-only `winepath` call with:
```bash
if [[ "${game[0]}" == "umu" ]]; then
gameDir="$(umu_windows_path_to_unix "$winePath")"
else
gameDir="$(winepath "$winePath")"
fi
```
For stopping processes, use:
```bash
if [[ "${game[0]}" == "umu" ]]; then
stop_umu_bottle
else
wineserver -k
fi
```
- [ ] **Step 3: Make kill backend-aware**
In `kill_game`, after `create_game_array` or parsing the selected line, ensure UMU entries process launcher flags and call `stop_umu_bottle`:
```bash
if [[ "${game[0]}" == "umu" ]]; then
process_launcher_flags
get_umu_bottle "$umuGameId"
stop_umu_bottle
else
get_bottle "${game%|*}"
wineserver -k
fi
```
- [ ] **Step 4: Launch UMU entries through `run_umu_game`**
In `game_launcher`, after `process_launcher_flags` and before Wine-only qjoypad/path work is used, add:
```bash
if [[ "${game[0]}" == "umu" ]]; then
echo "launching with umu"
start_nvda2speechd
run_umu_game "${game[1]}"
exit 0
fi
```
Keep Wine launch behavior unchanged for non-UMU entries.
- [ ] **Step 5: Run syntax checks**
Run: `bash -n audiogame-manager.sh`
Expected: no output and exit code 0.
- [ ] **Step 6: Run shellcheck on touched shell files**
Run: `shellcheck audiogame-manager.sh .includes/proton.sh tests/umu_backend_tests.sh`
Expected: no new actionable errors. Existing intentional warnings may be suppressed locally only if they are not real bugs.
## Task 4: Update Dependency Reporting
**Files:**
- Modify: `.includes/checkup.sh`
- [ ] **Step 1: Add UMU check to `.includes/checkup.sh`**
After the Wine check, add:
```bash
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")
```
- [ ] **Step 2: Run syntax and shellcheck**
Run: `bash -n .includes/checkup.sh`
Expected: no output and exit code 0.
Run: `shellcheck .includes/checkup.sh`
Expected: no output and exit code 0, or only pre-existing intentional warnings.
## Task 5: Migrate Shadow Line Installer to UMU
**Files:**
- Modify: `.install/Shadow Line.sh`
- [ ] **Step 1: Replace installer body**
Update `.install/Shadow Line.sh` to:
```bash
# shellcheck shell=bash disable=SC2154 # cache, game, and helper functions are set by audiogame-manager
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}"
export game="Shadow Line"
shadowLineGameId="shadow-line"
shadowLinePath='c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe'
shadowLineInstallDir="${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice"
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 umu-run "${cache}/shadowrine_fullvoice3.171.exe" /sp- /VERYSILENT /SUPPRESSMSGBOXES 2>&1 || true
echo "# Installation complete"
} | 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."
```
- [ ] **Step 2: Run syntax check**
Run: `bash -n ".install/Shadow Line.sh"`
Expected: no output and exit code 0.
- [ ] **Step 3: Run shellcheck on Shadow Line installer**
Run: `shellcheck ".install/Shadow Line.sh"`
Expected: no output and exit code 0, or only intentional sourced-global warnings suppressed by the file header.
## Task 6: End-to-End Verification
**Files:**
- Modify as needed based on verification findings.
- [ ] **Step 1: Run focused unit tests**
Run: `bash tests/umu_backend_tests.sh`
Expected: PASS with `UMU backend tests passed`.
- [ ] **Step 2: Run syntax checks for all touched scripts**
Run:
```bash
bash -n audiogame-manager.sh
bash -n .includes/proton.sh
bash -n .includes/checkup.sh
bash -n ".install/Shadow Line.sh"
bash -n tests/umu_backend_tests.sh
```
Expected: all commands exit 0.
- [ ] **Step 3: Run shellcheck for all touched Bash files**
Run:
```bash
shellcheck audiogame-manager.sh .includes/proton.sh .includes/checkup.sh ".install/Shadow Line.sh" tests/umu_backend_tests.sh
```
Expected: no actionable warnings.
- [ ] **Step 4: Optional live install verification**
If the user approves running the live installer, run:
```bash
DISPLAY=:0 ./audiogame-manager.sh -I "Shadow Line"
```
Expected:
- Shadow Line installs into `${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/protonBottles/shadow-line`.
- `games.conf` contains `umu|...|Shadow Line|export umuGameId=shadow-line`.
- `play_sr.exe` exists under the Shadow Line Proton prefix.
- `SystemData/language_en.dat` exists.
- `user.reg` contains `bcryptprimitives` and `AppDefaults\\play_sr.exe` with `Version=win8`.
- [ ] **Step 5: Commit implementation**
Run:
```bash
git add .includes/proton.sh .includes/checkup.sh ".install/Shadow Line.sh" audiogame-manager.sh tests/umu_backend_tests.sh docs/superpowers/plans/2026-05-05-umu-proton-backend.md
git commit -m "Add UMU Proton backend for Shadow Line"
```
Expected: commit succeeds and unrelated pre-existing dirty files remain unstaged.
## Self-Review
- Spec coverage: generic UMU helpers, Shadow Line migration, dependency checks, error handling, and verification are each covered by tasks.
- Placeholder scan: no TODO/TBD placeholders remain.
- Type consistency: helper names are consistent across tests, implementation, installer, and launcher tasks.
@@ -0,0 +1,64 @@
# UMU Proton Backend Design
## Goal
Add a general UMU/Proton backend to audiogame-manager so selected games can install and launch through `umu-run` with AGM-managed Proton prefixes. Shadow Line is the first supported game and should be installable, launchable, translated to English, and speech-capable without changing the existing Wine backend for other games.
## Scope
This change introduces generic UMU helpers and wires Shadow Line to use them. It does not migrate existing Wine games, add proton-voices, or reimplement `umu-launcher`. AGM depends on `umu-run` for Proton/runtime management because UMU handles Proton selection, Steam Runtime setup, prefix creation, protonfixes, and Proton winetricks routing.
## Architecture
Launcher entries gain a new backend value in the first `games.conf` field:
```text
umu|c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe|Shadow Line|...
```
The existing Wine values (`win64`, `win32`) continue to behave as they do now. When the backend is `umu`, launch setup uses a dedicated Proton prefix under:
```text
${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/protonBottles/<game-id>
```
The game id is supplied by the installer and stored in launcher flags, for example `export umuGameId=shadow-line`. Helpers will centralize backend behavior:
- `install_proton_bottle <game-id> [winetricks verbs...]`: create and select a UMU prefix, initialize it with `umu-run ""`, and install optional Proton winetricks verbs with `umu-run winetricks`.
- `get_umu_bottle <game-id>`: export `WINEPREFIX`, `GAMEID`, `STORE`, and `DISPLAY`.
- `run_umu_game <windows-path>`: convert the Windows path to a Unix executable path in the UMU prefix and run it through `umu-run`.
- `add_umu_launcher <game-id> <windows-path> [flags...]`: append a `umu|...` launcher line.
## Shadow Line
Shadow Line uses the generic UMU backend with `umuGameId=shadow-line`. Its installer will:
1. Require `umu-run`.
2. Download `shadowrine_fullvoice3.171.exe` and `language_en.dat`.
3. Create/select the Shadow Line UMU prefix.
4. Apply `bcryptprimitives=native,builtin`.
5. Apply per-app `win8` for `play_sr.exe`.
6. Run the installer silently with UMU.
7. Stop any leftover Wine/Proton processes for the prefix.
8. Copy `language_en.dat` into `SystemData`.
9. Copy AGM's `nvdaControllerClient32.dll` and/or `nvdaControllerClient64.dll` over any matching game DLLs found in the Shadow Line install.
10. Register the UMU launcher and show the first-run instructions for switching the game language to English.
Clipboard translation will not be used for Shadow Line in this implementation.
## Dependency Checks
`check_requirements` remains focused on core AGM dependencies, but UMU installers call a targeted `require_umu` helper so the error is clear at install time. The `-c` checkup report will also list `umu-run`: missing UMU is reported as required for Proton-backed games, not as a blocker for every Wine game.
## Error Handling
UMU helper functions fail fast with clear messages when `umu-run` is missing, prefix initialization fails, the installer does not produce the expected executable, or the translation file cannot be copied. Shadow Line should not be added to `games.conf` unless the expected executable exists.
## Verification
Verification will include:
- `bash -n` on changed shell scripts.
- `shellcheck` on changed Bash files.
- A focused install-path dry run where possible without launching the interactive game.
- Confirmation that Shadow Line creates a `umu|...|Shadow Line` launcher entry and that the UMU prefix contains `play_sr.exe`, `language_en.dat`, registry settings, and replacement NVDA DLLs when matching DLLs exist.
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# shellcheck disable=SC1091,SC2034,SC2154
export game="Magic: The Gathering Arena"
export WINEARCH=win64
export winVer="win10"
export DIALOGOPTS='--no-lines --visit-items'
export ipfsGateway="${ipfsGateway:-https://ipfs.stormux.org}"
cache="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager"
winetricksPath="${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager"
mkdir -p "$cache" "$winetricksPath"
if [[ -z "$DISPLAY" ]]; then
dialogType="dialog"
export DISPLAY=":0"
elif command -v yad &> /dev/null; then
dialogType="yad"
else
dialogType="dialog"
fi
source "${0%/*}/../.includes/dialog-interface.sh"
source "${0%/*}/../.includes/functions.sh"
source "${0%/*}/../.includes/bottle.sh"
export nvdaControllerClient64Dll="${ipfs[nvdaControllerClient64]}"
mtgaVersionUrl="https://mtgarena.downloads.wizards.com/Live/Windows32/version"
accessibleArenaDllUrl="https://github.com/JeanStiletto/AccessibleArena/releases/latest/download/AccessibleArena.dll"
melonLoaderZipUrl="https://github.com/LavaGang/MelonLoader/releases/latest/download/MelonLoader.x64.zip"
tolkDllUrl="https://stormgames.wolfe.casa/downloads/Tolk.dll"
get_mtga_installer_url() {
local versionJson=""
local installerUrl=""
if ! versionJson="$(curl -fsSL "$mtgaVersionUrl")"; then
alert "Magic: The Gathering Arena" "Magic: The Gathering Arena" "Could not fetch the current MTG Arena installer URL."
exit 1
fi
installerUrl="$(sed -n 's/.*"CurrentInstallerURL"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' <<< "$versionJson" | head -n1)"
if [[ -z "$installerUrl" ]]; then
alert "Magic: The Gathering Arena" "Magic: The Gathering Arena" "Could not parse the current MTG Arena installer URL."
exit 1
fi
printf '%s\n' "$installerUrl"
}
download_mtga_installer() {
local installerUrl="$1"
local installerFile="${installerUrl##*/}"
installerFile="${installerFile%%\?*}"
[[ -n "$installerFile" ]] || installerFile="MTGAInstaller.msi"
rm -f "${cache}/${installerFile}" 2> /dev/null
if ! curl -L4 -C - --retry 10 --output "${cache}/${installerFile}" "$installerUrl" 2>&1 | agm_progressbox "Magic: The Gathering Arena Update" "Downloading MTG Arena update..."; then
alert "Magic: The Gathering Arena" "Magic: The Gathering Arena" "Could not download the MTG Arena update."
exit 1
fi
mtgaInstallerPath="${cache}/${installerFile}"
}
configure_accessible_arena_loader() {
local mtgaRoot="$1"
local userDataPath="${mtgaRoot}/UserData"
local loaderConfig="${userDataPath}/Loader.cfg"
mkdir -p "$userDataPath"
if [[ -f "$loaderConfig" ]]; then
if grep -Fq "hide_console = false" "$loaderConfig"; then
sed -i 's/hide_console = false/hide_console = true/g' "$loaderConfig"
elif ! grep -Fq "hide_console" "$loaderConfig"; then
printf '\n[console]\nhide_console = true\n' >> "$loaderConfig"
fi
else
printf '[console]\nhide_console = true\n' > "$loaderConfig"
fi
}
install_accessible_arena_support() {
local mtgaRoot="$1"
download "$accessibleArenaDllUrl" "$melonLoaderZipUrl" "$tolkDllUrl" "$nvdaControllerClient64Dll"
install_with_progress unzip "Reinstalling MelonLoader..." -d "$mtgaRoot" "${cache}/MelonLoader.x64.zip"
mkdir -p "${mtgaRoot}/Mods"
install_with_progress cp "Installing Accessible Arena..." "${cache}/AccessibleArena.dll" "${mtgaRoot}/Mods/AccessibleArena.dll"
install_with_progress cp "Installing screen reader support DLLs..." "${cache}/Tolk.dll" "${cache}/nvdaControllerClient64.dll" "$mtgaRoot"
configure_accessible_arena_loader "$mtgaRoot"
}
check_requirements || exit 1
install_wine_bottle dxvk
winetricks -q $winVer
mtgaRoot="${WINEPREFIX}/drive_c/Program Files (x86)/Wizards of the Coast/MTGA"
mtgaInstallerUrl="$(get_mtga_installer_url)"
download_mtga_installer "$mtgaInstallerUrl"
wine msiexec /i "$mtgaInstallerPath" /q
if [[ ! -f "${mtgaRoot}/MTGA.exe" ]]; then
alert "Magic: The Gathering Arena" "Magic: The Gathering Arena" "MTG Arena was not found at the expected location: ${mtgaRoot}"
exit 1
fi
install_accessible_arena_support "$mtgaRoot"
agm_msgbox "Magic: The Gathering Arena Update" "Magic: The Gathering Arena Update" "Magic: The Gathering Arena and Accessible Arena have been updated."
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Updater script for Swamp 2.
# For whatever reason, the updates are partially applied and I'm too lazy to figure out why Wine is barfing, so we'll just do it manually.
cache="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager"
. ../.includes/dialog-interface.sh
. ../.includes/functions.sh
swamphouse=~/.local/wine64/drive_c/Program\ Files/swamp2
myver=$(cat "$swamphouse/myversion.txt")
newver=$(curl -s https://kaldobsky.com/audiogames/swamp2version.txt)
if [[ $newver -gt $myver ]]; then
agm_msgbox "Our version: ${myver}, with the newer version being ${newver}"
download "https://kaldobsky.com/audiogames/swamp2_update.zip"
install_with_progress unzip "Extracting game files..." -d "${swamphouse}" "${cache}/swamp2_update.zip"
rm "${cache}/swamp2_update.zip"
fi
+36 -43
View File
@@ -7,23 +7,30 @@
# set -Eeuo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/#:~:text=set%20%2Du,is%20often%20highly%20desirable%20behavior.
shopt -s expand_aliases
# Debug logging
#DEBUG_LOG="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager/translator-debug.log"
#exec 2>>"$DEBUG_LOG"
#echo "=== Translator started at $(date) ===" >&2
#echo "Args: $@" >&2
#set -x
if [[ $# -ne 2 ]]; then
echo "Usage: $0 \"application name\" \"file name\"."
exit 1
fi
scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
pythonScript="${scriptDir}/translate_clipboard.py"
if [[ -z "${AGM_TRANSLATOR_FORCE_SHELL:-}" ]] && command -v python3 &> /dev/null && [[ -f "$pythonScript" ]]; then
exec python3 "$pythonScript" "$@"
fi
is_app_running() {
local appPattern="$1"
local selfPid="$$"
pgrep -af -u "$USER" "$appPattern" \
| awk -v selfPid="$selfPid" '$1 != selfPid && $0 !~ /clipboard_translator.sh/ { found=1 } END { exit !found }'
}
# Wait for the application to start
while ! pgrep -f -u "$USER" "$1" &> /dev/null ; do
while ! is_app_running "$1"; do
sleep 0.05
done
fileName="${2,,}"
fileName="${fileName//[[:space:]]/-}.sqlite"
translationFile="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager/${fileName}"
@@ -44,26 +51,8 @@ CREATE INDEX translations_text_idx ON translations (text);
EOF
fi
# Define a function to safely query the database
query_database() {
local dbFile="$1"
local sqlQuery="$2"
sqlite3 -line "$dbFile" "$sqlQuery"
}
# Define a function to safely insert into the database
insert_database() {
local dbFile="$1"
local text="$2"
local translation="$3"
# Use sqlite3 .import feature which is more robust for special characters
echo "$text|$translation" | sqlite3 -separator "|" "$dbFile" ".import /dev/stdin temp_import"
sqlite3 "$dbFile" "INSERT OR IGNORE INTO translations SELECT * FROM temp_import; DROP TABLE IF EXISTS temp_import;"
}
# Read so long as the application is running
while pgrep -f -u "$USER" "$1" &> /dev/null ; do
while is_app_running "$1"; do
sleep 0.05
text="$(xclip -d "${DISPLAY:-:0}" -selection clipboard -o 2> /dev/null)"
if [[ -f ~/.agmsilent ]]; then
@@ -80,46 +69,50 @@ while pgrep -f -u "$USER" "$1" &> /dev/null ; do
# https://en.wikipedia.org/wiki/Unicode_equivalence#Combining_and_precomposed_characters
# https://www.effectiveperlprogramming.com/2011/09/normalize-your-perl-source/
alias nfc="perl -MUnicode::Normalize -CS -ne 'print NFC(\$_)'" # composed characters
nfc() {
perl -MUnicode::Normalize -CS -ne 'print NFC($_)'
}
# Normalize different unicode space characters to the same space
# https://stackoverflow.com/a/43640405
alias normalizeSpaces="perl -CSDA -plE 's/[^\\S\\t]/ /g'"
alias normalizeUnicode="normalizeSpaces | nfc"
normalize_spaces() {
perl -CSDA -plE 's/[^\S\t]/ /g'
}
# Remove zero-width spaces (U+200B) that some games insert between characters
strip_zwsp() {
perl -CSDA -plE 's/\x{200B}//g'
}
normalize_unicode() {
strip_zwsp | normalize_spaces | nfc
}
# Normalize text
normalizedText="$(echo "$text" | normalizeUnicode)"
normalizedText="$(echo "$text" | normalize_unicode)"
# Create a temporary database for import
sqlite3 "$translationFile" "CREATE TABLE IF NOT EXISTS temp_import(text TEXT, translation TEXT);"
# Check if we already have a translation
translated=$(sqlite3 "$translationFile" "SELECT translation FROM translations WHERE text = '$normalizedText' LIMIT 1;" 2>/dev/null)
echo "DEBUG: Database lookup result: '$translated'" >&2
if [[ -z "$translated" ]]; then
echo "DEBUG: No cached translation, calling trans command" >&2
# Get translation from the trans utility
translated="$(trans -no-autocorrect -no-warn -brief "$normalizedText" | head -1 | sed 's/\s*$//' | normalizeUnicode)"
echo "DEBUG: trans returned: '$translated'" >&2
translated="$(trans -no-autocorrect -no-warn -brief "$normalizedText" | head -1 | sed 's/\s*$//' | normalize_unicode)"
if [[ -n "$translated" ]]; then
# Insert using echo piping to avoid escaping issues
echo "$normalizedText|$translated" | sqlite3 -separator "|" "$translationFile" ".import /dev/stdin temp_import"
sqlite3 "$translationFile" "INSERT OR IGNORE INTO translations SELECT * FROM temp_import; DELETE FROM temp_import;"
echo "DEBUG: Saved to database" >&2
fi
fi
# If we got a translation, speak it
if [[ -n "$translated" ]]; then
echo "DEBUG: Speaking translation: '$translated'" >&2
spd-say -- "$translated"
else
echo "DEBUG: No translation available, speaking original: '$text'" >&2
spd-say -- "$text"
spd-say -- "$normalizedText"
fi
# Clear clipboard
echo "" | xclip -d "${DISPLAY:-:0}" -selection clipboard 2> /dev/null
done
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail
export WINEPREFIX="${WINEPREFIX:-${HOME}/.local/wine64}"
export WINEDEBUG="${WINEDEBUG:--all}"
installerUrl="https://stormgames.wolfe.casa/downloads/winespeak.exe"
installerName="winespeak.exe"
installerSha256="187d4db69f3af7c1bca1c100a04489b76732048be7c38449781da360ed7b2d66"
cacheDir="${XDG_CACHE_HOME:-${HOME}/.cache}/audiogame-manager"
installerPath="${cacheDir}/${installerName}"
partialPath="${installerPath}.part"
wrapperPath="${WINEPREFIX}/drive_c/Program Files (x86)/espeak-ng-sapi/EspeakSAPI.dll"
wrapperSha256="8009750dad82ca6665871814033dcee6844bb424f033130c109f47e188c5364b"
voiceToken="HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\TokenEnums\\eSpeak-NG\\English (America)"
fail() {
printf 'Error: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" > /dev/null 2>&1 || fail "Required command not found: $1"
}
verify_sha256() {
local file="$1"
local expected="$2"
local actual
actual="$(sha256sum "$file" | awk '{print $1}')"
[[ "$actual" == "$expected" ]]
}
download_installer() {
mkdir -p "$cacheDir"
if [[ -s "$installerPath" ]] && verify_sha256 "$installerPath" "$installerSha256"; then
printf 'Using cached WineSpeak installer.\n'
return
fi
rm -f "$installerPath" "$partialPath"
printf 'Downloading WineSpeak...\n'
if ! curl -L4 --fail --retry 3 --output "$partialPath" "$installerUrl"; then
rm -f "$partialPath"
fail 'Could not download WineSpeak.'
fi
if ! verify_sha256 "$partialPath" "$installerSha256"; then
rm -f "$partialPath"
fail 'The downloaded WineSpeak installer failed its SHA-256 check.'
fi
mv "$partialPath" "$installerPath"
}
install_winespeak() {
local registryOutput
download_installer
printf 'Installing WineSpeak into %s...\n' "$WINEPREFIX"
if ! wine "$installerPath" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-; then
fail 'WineSpeak installation failed.'
fi
if [[ ! -f "$wrapperPath" ]]; then
fail 'WineSpeak did not install its 32-bit SAPI wrapper.'
fi
if ! verify_sha256 "$wrapperPath" "$wrapperSha256"; then
fail 'WineSpeak installed an unexpected 32-bit SAPI wrapper.'
fi
printf 'Setting English (America) as the default SAPI voice...\n'
if ! wine reg add 'HKCU\Software\Microsoft\Speech\Voices' \
/v DefaultTokenId /t REG_SZ /d "$voiceToken" /f > /dev/null; then
fail 'Could not set the default SAPI voice.'
fi
registryOutput="$(wine reg query 'HKCU\Software\Microsoft\Speech\Voices' /v DefaultTokenId 2>/dev/null)"
if ! grep -Fq "$voiceToken" <<< "$registryOutput"; then
fail 'Could not verify the default SAPI voice.'
fi
printf 'WineSpeak is installed and English (America) is the default SAPI voice.\n'
}
for commandName in curl wine sha256sum awk grep; do
require_command "$commandName"
done
install_winespeak
+150
View File
@@ -0,0 +1,150 @@
#!/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
if [[ "${1:-}" == "winetricks" && "${3:-}" == "already-installed" ]]; then
printf "winetricks verb 'already-installed' is already installed\n" >&2
exit 1
fi
STUB
chmod +x "${testRoot}/bin/umu-run"
cat > "${testRoot}/bin/gamemoderun" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$GAMEMODE_STUB_LOG"
exec "$@"
STUB
chmod +x "${testRoot}/bin/gamemoderun"
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"
export GAMEMODE_STUB_LOG="${testRoot}/gamemode.log"
# shellcheck source=.includes/gamemode.sh
source "${repoRoot}/.includes/gamemode.sh"
# 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_add_umu_launcher_records_extra_environment_flags() {
get_umu_bottle "entombed"
game="Entombed"
add_umu_launcher "entombed" 'c:\Program Files (x86)\Entombed\Entombed.exe' "export PROTON_USE_XALIA=0"
assert_file_contains "$configFile" 'umu|c:\Program Files (x86)\Entombed\Entombed.exe|Entombed|export umuGameId=entombed|export PROTON_USE_XALIA=0' "UMU launcher entry records extra environment flags"
}
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 "$GAMEMODE_STUB_LOG" "umu-run ${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice/play_sr.exe" "UMU launches through GameMode"
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_install_crlf_file_normalizes_line_endings() {
local sourceFile="${testRoot}/language_en.dat"
local destFile="${testRoot}/installed/language_en.dat"
local expectedFile="${testRoot}/expected-language_en.dat"
printf 'line one\nline two\n' > "$sourceFile"
printf 'line one\r\nline two\r\n' > "$expectedFile"
install_crlf_file "$sourceFile" "$destFile"
if ! cmp -s "$expectedFile" "$destFile"; then
printf 'FAIL: Translation file is installed with CRLF line endings\n' >&2
exit 1
fi
}
test_install_proton_winetricks_verb_ignores_already_installed() {
get_umu_bottle "shadow-line"
install_proton_winetricks_verb "already-installed"
}
test_get_umu_bottle_sets_environment
test_add_umu_launcher_records_backend_and_game_id
test_add_umu_launcher_records_extra_environment_flags
test_run_umu_game_uses_converted_path
test_install_crlf_file_normalizes_line_endings
test_install_proton_winetricks_verb_ignores_already_installed
printf 'UMU backend tests passed\n'
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Immediately exit if errors are encountered.
set -e
# Wine dependencies installer for Fedora Linux
# If this fails on your system, please contact storm_dragon@stormux.org
configure_fedora() {
packageList=(
7zip
alsa-lib
alsa-plugins-pulseaudio
cabextract
curl
dialog
dos2unix
gawk
gnutls
gstreamer1-plugin-libav
gstreamer1-plugins-bad-free
gstreamer1-plugins-good
gstreamer1-plugins-ugly-free
libjpeg-turbo
libpng
libwbclient
mesa-dri-drivers
mpg123
ncurses
openal-soft
pulseaudio-libs
sdl2-compat
sox
sqlite
translate-shell
unzip
w3m
wine
winetricks
xdg-utils
xz
)
sudo dnf install --assumeyes "${packageList[@]}"
}
configure_fedora
exit 0