Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62e47a41ae | ||
|
|
8e33ef63d0 | ||
|
|
7709ec83d8 | ||
|
|
d5b688314c | ||
|
|
4886578fc5 | ||
|
|
105672be13 | ||
|
|
77b77ef1ff | ||
|
|
fd72a85003 | ||
|
|
82edcc66b8 | ||
|
|
2ead52888d | ||
|
|
ab2c6d267a | ||
|
|
8e4f764847 | ||
|
|
17aed35d5b | ||
|
|
c1c5363c63 | ||
|
|
83dfa99aa3 | ||
|
|
edf8e8e104 | ||
|
|
38a9b2cd39 | ||
|
|
4112aa3bda | ||
|
|
4f11130369 | ||
|
|
850cd37364 | ||
|
|
d4db802645 | ||
|
|
993f838404 | ||
|
|
7ff3f0b47d | ||
|
|
eee7b7e7c8 | ||
|
|
4b312099b4 | ||
|
|
7ec9ef3b87 | ||
|
|
b87245fa69 | ||
|
|
05ea1502ce | ||
|
|
c033a21211 | ||
|
|
8943688a43 | ||
|
|
92745d8af8 | ||
|
|
1f977bb1f4 | ||
|
|
53d8c10645 | ||
|
|
b58681964c |
No files matched your search
@@ -1,3 +1,4 @@
|
||||
*.x.c
|
||||
*.x
|
||||
music/
|
||||
/game.log
|
||||
+154
-12
@@ -22,6 +22,35 @@ get_bottle() {
|
||||
fi
|
||||
}
|
||||
|
||||
apply_swamp_quartz_override() {
|
||||
local exePath="$1"
|
||||
local wineVersion="$2"
|
||||
local exeName="${exePath##*\\}"
|
||||
local wineMajor
|
||||
local wineMinor
|
||||
local quartzOverride
|
||||
|
||||
exeName="${exeName##*/}"
|
||||
if [[ "${exeName,,}" != "swamp.exe" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! "$wineVersion" =~ wine-([0-9]+)\.([0-9]+) ]]; then
|
||||
echo 'Could not determine the Wine version.' >&2
|
||||
return
|
||||
fi
|
||||
|
||||
wineMajor="${BASH_REMATCH[1]}"
|
||||
wineMinor="${BASH_REMATCH[2]}"
|
||||
if (( wineMajor > 11 || (wineMajor == 11 && wineMinor >= 17) )); then
|
||||
quartzOverride="b"
|
||||
else
|
||||
quartzOverride="n,b"
|
||||
fi
|
||||
|
||||
export WINEDLLOVERRIDES="${WINEDLLOVERRIDES:+${WINEDLLOVERRIDES};}quartz=${quartzOverride}"
|
||||
}
|
||||
|
||||
# Set wine environment for installation (called from install_wine_bottle)
|
||||
set_wine_env() {
|
||||
local architecture="$1"
|
||||
@@ -40,15 +69,41 @@ set_wine_env() {
|
||||
|
||||
# Note: install_wine function removed - we now use system wine with simplified bottle management
|
||||
|
||||
enable_winetricks_usage_reporting() {
|
||||
local winetricksCache="${XDG_CACHE_HOME:-$HOME/.cache}/winetricks"
|
||||
|
||||
mkdir -p "$winetricksCache"
|
||||
if [[ ! -e "${winetricksCache}/track_usage" ]]; then
|
||||
printf '1\n' > "${winetricksCache}/track_usage"
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_speechsdk_cache() {
|
||||
local winetricksCache="${XDG_CACHE_HOME:-$HOME/.cache}/winetricks"
|
||||
local speechsdkPayload="${winetricksCache}/speechsdk/SpeechSDK51.exe"
|
||||
|
||||
enable_winetricks_usage_reporting
|
||||
if [[ -f "$speechsdkPayload" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Downloading Speech SDK for the Winetricks cache..."
|
||||
# ipfs and cache are populated by the main script before bottle setup.
|
||||
# shellcheck disable=SC2154
|
||||
download "${ipfs[speechsdk]}"
|
||||
if ! unzip -oq "${cache}/speechsdk.zip" -d "$winetricksCache"; then
|
||||
echo "Could not extract Speech SDK into the Winetricks cache."
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$speechsdkPayload" ]]; then
|
||||
echo "The Speech SDK archive did not contain speechsdk/SpeechSDK51.exe."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
winetricks() {
|
||||
# Report used packages to the winetricks maintainer so he knows they are being used.
|
||||
if ! [[ -e "${XDG_CACHE_HOME:-$HOME/.cache}/winetricks/track_usage" ]]; then
|
||||
mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}/winetricks/"
|
||||
echo "1" > "${XDG_CACHE_HOME:-$HOME/.cache}/winetricks/track_usage"
|
||||
fi
|
||||
# Temporary work around for winetricks git bugs. Requires winetricks be installed from package manager.
|
||||
/usr/bin/winetricks "$@"
|
||||
return
|
||||
enable_winetricks_usage_reporting
|
||||
# Download or update agm's copy of winetricks
|
||||
if [[ ! -e "${winetricksPath}/winetricks" ]]; then
|
||||
checkWinetricksUpdate="true"
|
||||
@@ -58,17 +113,104 @@ winetricks() {
|
||||
else
|
||||
if [[ "$checkWinetricksUpdate" != "true" ]]; then
|
||||
checkWinetricksUpdate="true"
|
||||
${winetricksPath}/winetricks --self-update
|
||||
"${winetricksPath}/winetricks" --self-update
|
||||
fi
|
||||
fi
|
||||
# Run the requested winetricks parameters
|
||||
if command -v FEXLoader &> /dev/null ; then
|
||||
WINE="" FEXLoader -- ${winetricksPath}/winetricks "$@"
|
||||
WINE="" FEXLoader -- "${winetricksPath}/winetricks" "$@"
|
||||
else
|
||||
${winetricksPath}/winetricks "$@"
|
||||
"${winetricksPath}/winetricks" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
install_winespeak() {
|
||||
local installerName="winespeak.exe"
|
||||
local installerPath="${cache}/${installerName}"
|
||||
local installerSha256="8007004cd024b391e8f1962455ffe2a8b241cfdf52641a2bc1aa2274bc80f236"
|
||||
local wrapperPath="${WINEPREFIX}/drive_c/Program Files (x86)/espeak-ng-sapi/EspeakSAPI.dll"
|
||||
local wrapperSha256="2282da4a23f18b40e6601a4b9a880f484196bf2f5138881e76428be10d8dbe76"
|
||||
local cscriptPath="${WINEPREFIX}/drive_c/windows/syswow64/cscript.exe"
|
||||
local verifyScriptPath="${WINEPREFIX}/drive_c/windows/temp/verify_winespeak.vbs"
|
||||
local actualSha256
|
||||
local verificationAttempt
|
||||
local registryOutput
|
||||
local voiceOutput
|
||||
local voiceStatus
|
||||
local defaultToken='HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens\Espeak-ng US English'
|
||||
|
||||
if [[ -z "${wineSpeakInstaller:-}" ]]; then
|
||||
echo "WineSpeak download URL is not configured."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for ((verificationAttempt = 1; verificationAttempt <= 3; verificationAttempt++)); do
|
||||
download "$wineSpeakInstaller"
|
||||
actualSha256="$(sha256sum "$installerPath" | awk '{print $1}')"
|
||||
if [[ "$actualSha256" == "$installerSha256" ]]; then
|
||||
break
|
||||
fi
|
||||
echo "WineSpeak installer failed its SHA-256 check (attempt ${verificationAttempt} of 3)."
|
||||
rm -f "$installerPath"
|
||||
if ((verificationAttempt == 3)); then
|
||||
return 1
|
||||
fi
|
||||
echo "Downloading WineSpeak again."
|
||||
done
|
||||
|
||||
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
|
||||
dim voiceDescription
|
||||
set speechobject=createobject("sapi.spvoice")
|
||||
voiceDescription=speechobject.voice.getdescription
|
||||
if instr(1, voiceDescription, "Espeak-ng US English", 1)=0 then
|
||||
wscript.echo voiceDescription
|
||||
wscript.quit 2
|
||||
end if
|
||||
EOF
|
||||
voiceStatus=0
|
||||
voiceOutput="$(wine "$cscriptPath" //nologo 'c:\windows\temp\verify_winespeak.vbs' 2>&1)" || voiceStatus=$?
|
||||
if [[ "$voiceStatus" -ne 0 ]]; then
|
||||
if [[ "$voiceStatus" -eq 2 ]]; then
|
||||
echo "WineSpeak's 32-bit SAPI verification returned an unexpected voice."
|
||||
else
|
||||
echo "WineSpeak's 32-bit SAPI voice could not be created."
|
||||
fi
|
||||
if [[ -n "$voiceOutput" ]]; then
|
||||
echo "$voiceOutput"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "WineSpeak US English is the default SAPI voice."
|
||||
}
|
||||
|
||||
install_rhvoice() {
|
||||
if [[ -d "${WINEPREFIX}/drive_c/Program Files/Olga Yakovleva/" ]]; then
|
||||
return
|
||||
@@ -168,7 +310,7 @@ install_wine_bottle() {
|
||||
if [[ ${#regularDeps[@]} -gt 0 ]]; then
|
||||
echo "Installing additional dependencies: ${regularDeps[*]}"
|
||||
{
|
||||
env WINE="$WINE" WINESERVER="$WINESERVER" DISPLAY="${DISPLAY:-:0}" winetricks -q isolate_home "${regularDeps[@]}" ${winetricksSettings}
|
||||
WINE="$WINE" WINESERVER="$WINESERVER" DISPLAY="${DISPLAY:-:0}" winetricks -q isolate_home "${regularDeps[@]}" ${winetricksSettings}
|
||||
} | agm_progressbox "Wine Setup" "Installing additional dependencies..."
|
||||
fi
|
||||
|
||||
@@ -176,7 +318,7 @@ install_wine_bottle() {
|
||||
if [[ "$needsSpeechsdk" == "true" ]]; then
|
||||
echo "Installing speechsdk with WINETRICKS_FORCE=1"
|
||||
{
|
||||
env WINE="$WINE" WINESERVER="$WINESERVER" DISPLAY="${DISPLAY:-:0}" WINETRICKS_FORCE=1 winetricks -q speechsdk
|
||||
WINE="$WINE" WINESERVER="$WINESERVER" DISPLAY="${DISPLAY:-:0}" WINETRICKS_FORCE=1 winetricks -q speechsdk
|
||||
} | agm_progressbox "Wine Setup" "Installing Speech SDK..."
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -251,3 +251,166 @@ install_with_progress() {
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Return the latest GitHub release tag and one matching asset URL, one per line.
|
||||
github_latest_release() {
|
||||
local repo="$1"
|
||||
local assetFilter="$2"
|
||||
local response tag
|
||||
local -a assetUrls=()
|
||||
|
||||
if [[ -z "$repo" || "$repo" != */* ]]; then
|
||||
echo "Expected a GitHub repository in owner/repo format." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! response="$(curl -fsSL --retry 3 "https://api.github.com/repos/${repo}/releases/latest")"; then
|
||||
echo "Could not fetch the latest release for ${repo}." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
tag="$(sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' <<< "$response" | head -n1)"
|
||||
mapfile -t assetUrls < <(
|
||||
sed -n 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' <<< "$response"
|
||||
)
|
||||
if [[ -n "$assetFilter" ]]; then
|
||||
mapfile -t assetUrls < <(
|
||||
printf '%s\n' "${assetUrls[@]}" | grep -iF -- "$assetFilter"
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ -z "$tag" || ${#assetUrls[@]} -ne 1 ]]; then
|
||||
echo "Could not identify one matching release asset for ${repo}." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n%s\n' "$tag" "${assetUrls[0]}"
|
||||
}
|
||||
|
||||
# Install or replace a game from its latest GitHub release.
|
||||
# Arguments: repository, asset filter, destination, expected executable, and
|
||||
# zero or more relative paths or files to preserve from the old installation.
|
||||
update_github_release_game() {
|
||||
local repo="$1"
|
||||
local assetFilter="$2"
|
||||
local destination="$3"
|
||||
local expectedExecutable="$4"
|
||||
shift 4
|
||||
local -a preservePaths=("$@")
|
||||
local -a releaseInfo=() executablePaths=() pathParts=()
|
||||
local releaseInfoText installedVersion latestTag latestUrl archiveFile
|
||||
local stageRoot archiveRoot newRoot executablePath preservePath sourcePath targetPath pathPart
|
||||
local updateFailed="false"
|
||||
|
||||
githubReleaseGameStatus="checking"
|
||||
destination="${destination%/}"
|
||||
if [[ -z "$destination" || "$destination" != /* ]]; then
|
||||
echo "The game destination must be an absolute path." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
for preservePath in "${preservePaths[@]}"; do
|
||||
if [[ "$preservePath" == /* ]]; then
|
||||
echo "Preserve paths must be relative to the game directory: ${preservePath}" >&2
|
||||
return 1
|
||||
fi
|
||||
IFS='/' read -r -a pathParts <<< "$preservePath"
|
||||
for pathPart in "${pathParts[@]}"; do
|
||||
if [[ -z "$pathPart" || "$pathPart" == "." || "$pathPart" == ".." ]]; then
|
||||
echo "Invalid preserve path: ${preservePath}" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if ! releaseInfoText="$(github_latest_release "$repo" "$assetFilter")"; then
|
||||
return 1
|
||||
fi
|
||||
mapfile -t releaseInfo <<< "$releaseInfoText"
|
||||
latestTag="${releaseInfo[0]}"
|
||||
latestUrl="${releaseInfo[1]}"
|
||||
|
||||
if [[ -f "${destination}/.agm-version" ]]; then
|
||||
installedVersion="$(<"${destination}/.agm-version")"
|
||||
if [[ "$installedVersion" == "$latestTag" && -f "${destination}/${expectedExecutable}" ]]; then
|
||||
githubReleaseGameStatus="up-to-date"
|
||||
echo "${game:-The game} is already at ${latestTag}."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
archiveFile="${latestUrl##*/}"
|
||||
archiveFile="${archiveFile//%20/ }"
|
||||
archiveFile="${archiveFile#*\?filename=}"
|
||||
archiveFile="${archiveFile%\?*}"
|
||||
rm -f "${cache}/${archiveFile}"
|
||||
download "$latestUrl"
|
||||
|
||||
mkdir -p "$(dirname "$destination")"
|
||||
stageRoot="${destination}.agm-update.$$"
|
||||
archiveRoot="${stageRoot}/archive"
|
||||
mkdir -p "$archiveRoot"
|
||||
|
||||
if ! unzip -q "${cache}/${archiveFile}" -d "$archiveRoot"; then
|
||||
echo "Could not extract ${archiveFile}." >&2
|
||||
rm -rf "$stageRoot"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mapfile -t executablePaths < <(find "$archiveRoot" -type f -name "$expectedExecutable" -print)
|
||||
if [[ ${#executablePaths[@]} -ne 1 ]]; then
|
||||
echo "The release did not contain exactly one ${expectedExecutable}." >&2
|
||||
rm -rf "$stageRoot"
|
||||
return 1
|
||||
fi
|
||||
executablePath="${executablePaths[0]}"
|
||||
newRoot="${executablePath%/*}"
|
||||
|
||||
for preservePath in "${preservePaths[@]}"; do
|
||||
sourcePath="${destination}/${preservePath}"
|
||||
[[ -e "$sourcePath" ]] || continue
|
||||
targetPath="${newRoot}/${preservePath}"
|
||||
if [[ -d "$sourcePath" ]]; then
|
||||
mkdir -p "$targetPath"
|
||||
if ! cp -a "${sourcePath}/." "$targetPath/"; then
|
||||
echo "Could not preserve ${preservePath}." >&2
|
||||
updateFailed="true"
|
||||
break
|
||||
fi
|
||||
else
|
||||
mkdir -p "${targetPath%/*}"
|
||||
if ! cp -a "$sourcePath" "$targetPath"; then
|
||||
echo "Could not preserve ${preservePath}." >&2
|
||||
updateFailed="true"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$updateFailed" == "true" ]]; then
|
||||
rm -rf "$stageRoot"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$latestTag" > "${newRoot}/.agm-version"
|
||||
if [[ -e "$destination" ]]; then
|
||||
if ! mv "$destination" "${stageRoot}/old"; then
|
||||
echo "Could not move the old installation aside." >&2
|
||||
rm -rf "$stageRoot"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! mv "$newRoot" "$destination"; then
|
||||
echo "Could not install the new version." >&2
|
||||
if [[ -e "${stageRoot}/old" ]]; then
|
||||
mv "${stageRoot}/old" "$destination"
|
||||
fi
|
||||
rm -rf "$stageRoot"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -rf "$stageRoot"
|
||||
githubReleaseGameStatus="updated"
|
||||
echo "${game:-The game} has been updated to ${latestTag}."
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
run_with_optional_gamemode() {
|
||||
if command -v gamemoderun &> /dev/null; then
|
||||
gamemoderun "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
+29
-2
@@ -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"
|
||||
@@ -116,6 +142,7 @@ help() {
|
||||
echo "noqjoypad=\"true\" # Do not launch qjoypad."
|
||||
echo "norh=\"true\" # Do not install RHVoice."
|
||||
echo "redownload=\"true\" # Redownload sources, do not use the version stored in cache."
|
||||
echo "skipDllReplace=\"true\" # Skip automatic NVDA controller DLL replacement when launching games."
|
||||
echo "voiceName=\"voicename\" # Select the voice to be installed (default Bdl)."
|
||||
echo "defaultVoice=\"voicename\" # Select the default voice to use for the bottle, e.g. MSMike or RHVoice."
|
||||
echo "defaultRate=\"Default voice rate for the bottle, default 7, may not work in all games. Values 1-9 or A."
|
||||
|
||||
@@ -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.
|
||||
|
@@ -7,6 +7,8 @@ declare -Ag ipfs=(
|
||||
[nvdaControllerClient32]="${ipfsGateway}/ipfs/QmTrRrT4QFKSkZ8ivfUawA6iJ6adEyyogccE3nLDTfSK8u?filename=nvdaControllerClient32.dll"
|
||||
[nvdaControllerClient64]="${ipfsGateway}/ipfs/QmaYE7RFDtwHCiXCVLcuA3esfFx6E7koidtvrck9AwPuuN?filename=nvdaControllerClient64.dll"
|
||||
[nvda2speechd]="${ipfsGateway}/ipfs/QmPxhoNsoFoJC7bCfioBBCcK8tEoSoYpm342z6u7KjFsVz?filename=nvda2speechd"
|
||||
[speechsdk]="${ipfsGateway}/ipfs/QmXKp7xKoaLmpLJ8GoGJbQcGXnfMkfoqq5PDpyWHWnL9DB?filename=speechsdk.zip"
|
||||
[winespeak]="https://stormgames.wolfe.casa/downloads/winespeak.exe"
|
||||
|
||||
# Games (alphabetical order)
|
||||
[BG 15 Puzzle]="${ipfsGateway}/ipfs/QmQiocMpMXoxejDftKKvmrR5xxpj1qcWcgkhBBwTcyijXg?filename=FPB32Setup10a.exe"
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Run setup in this shell so WINEDLLPATH remains available to the launcher.
|
||||
prepare_prism_speech() {
|
||||
local title="$1"
|
||||
shift
|
||||
local logDir="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager/prism-wine"
|
||||
local logFile details
|
||||
if ! mkdir -p "$logDir" || ! logFile="$(mktemp "${logDir}/setup.XXXXXX.log")"; then
|
||||
agm_msgbox "$title" "$title" "Could not prepare Prism Wine speech support: unable to create a diagnostic log in ${logDir}. Check directory permissions and free disk space."
|
||||
return 1
|
||||
fi
|
||||
if "$@" > "$logFile" 2>&1; then
|
||||
cat "$logFile"
|
||||
rm -f "$logFile"
|
||||
return 0
|
||||
fi
|
||||
cat "$logFile" >&2
|
||||
details="$(tail -n 15 "$logFile")"
|
||||
agm_msgbox "$title" "$title" "Could not prepare Prism Wine speech support.
|
||||
|
||||
${details}
|
||||
|
||||
Full diagnostic log: ${logFile}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Build the host bridge from the same release as the application's Prism DLL.
|
||||
# Wine does not implement RpcMgmtInqIfIds, which Prism uses to detect NVDA.
|
||||
build_prism_speech_bridge() (
|
||||
local prismVersion="$1"
|
||||
local bridgeDir="$2"
|
||||
local buildRoot sourceRoot tool
|
||||
local -a missingTools=()
|
||||
|
||||
for tool in curl tar cmake make winegcc wineg++ winebuild pkg-config; do
|
||||
command -v "$tool" >/dev/null || missingTools+=("$tool")
|
||||
done
|
||||
if [[ ${#missingTools[@]} -gt 0 ]]; then
|
||||
echo "Missing Prism Wine speech build tools: ${missingTools[*]}. Install these tools using your distribution's package manager, then launch the game again." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! pkg-config --exists speech-dispatcher; then
|
||||
echo "Missing Speech Dispatcher development files (libspeechd). Install your distribution's Speech Dispatcher development package, then launch the game again." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! mkdir -p "${bridgeDir%/*}" || ! buildRoot="$(mktemp -d "${bridgeDir%/*}/.build.XXXXXX")"; then
|
||||
echo "Could not create the Prism build directory. Check cache directory permissions and free disk space." >&2
|
||||
return 1
|
||||
fi
|
||||
trap 'rm -rf "$buildRoot"' EXIT
|
||||
echo "Preparing Prism ${prismVersion} Wine speech support..."
|
||||
if ! curl -fsSL --retry 3 "https://codeload.github.com/ethindp/prism/tar.gz/refs/tags/v${prismVersion}" -o "${buildRoot}/source.tar.gz"; then
|
||||
echo "Could not download Prism ${prismVersion}. Check your internet connection and whether this Prism release is available, then try again." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! tar -xzf "${buildRoot}/source.tar.gz" -C "$buildRoot"; then
|
||||
echo "Could not extract Prism ${prismVersion}. Check free disk space and the archive error above." >&2
|
||||
return 1
|
||||
fi
|
||||
sourceRoot="${buildRoot}/prism-${prismVersion}"
|
||||
if ! cmake -S "${sourceRoot}/winelibs" -B "${buildRoot}/build" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="${sourceRoot}/cmake/winelib-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release; then
|
||||
echo "Could not configure Prism ${prismVersion} Wine speech support. See the CMake error above." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! cmake --build "${buildRoot}/build" --target \
|
||||
prism_speech_dispatcher_bridge prism_speech_dispatcher_bridge_placeholder -j2; then
|
||||
echo "Could not compile Prism ${prismVersion} Wine speech support. See the compiler error above." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! mkdir -p "$bridgeDir" || ! cp "${buildRoot}/build/speechd/prism_speech_dispatcher_bridge.dll.so" \
|
||||
"${buildRoot}/build/speechd/placeholder/prism_speech_dispatcher_bridge.dll" \
|
||||
"$bridgeDir/"; then
|
||||
echo "Could not save the Prism speech bridge to the cache. Check directory permissions and free disk space." >&2
|
||||
return 1
|
||||
fi
|
||||
)
|
||||
|
||||
prepare_arcadelux_prism_bridge() {
|
||||
local gameDir="$1"
|
||||
local prismVersion
|
||||
local -a metadataFiles=("${gameDir}"/_internal/prismatoid-*.dist-info/METADATA)
|
||||
if [[ ${#metadataFiles[@]} -ne 1 || ! -f "${metadataFiles[0]}" ]]; then
|
||||
echo "Could not identify Arcadelux's bundled Prism version: expected one _internal/prismatoid-*.dist-info/METADATA file. Update or reinstall Arcadelux, then try again." >&2
|
||||
return 1
|
||||
fi
|
||||
prismVersion="$(sed -n 's/^Version: //p' "${metadataFiles[0]}" | tr -d '\r')"
|
||||
if [[ ! "$prismVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Unsupported Prism version: ${prismVersion}" >&2
|
||||
return 1
|
||||
fi
|
||||
prepare_prism_bridge "$gameDir" "$prismVersion" "Arcadelux.exe"
|
||||
}
|
||||
|
||||
# App wrappers identify their bundled Prism release before sharing bridge setup.
|
||||
prepare_prism_bridge() {
|
||||
local gameDir="$1"
|
||||
local prismVersion="$2"
|
||||
local gameExecutable="$3"
|
||||
local bridgeDir
|
||||
if [[ ! "$prismVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Unsupported Prism version: ${prismVersion}" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ "$(uname -m)" != "x86_64" ]] || ! file -b "${gameDir}/${gameExecutable}" | grep -q 'PE32+'; then
|
||||
echo "Prism Wine speech requires an x86_64 host and 64-bit game." >&2
|
||||
return 1
|
||||
fi
|
||||
bridgeDir="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager/prism-wine/${prismVersion}-x86_64"
|
||||
if [[ ! -f "${bridgeDir}/prism_speech_dispatcher_bridge.dll" ||
|
||||
! -f "${bridgeDir}/prism_speech_dispatcher_bridge.dll.so" ]]; then
|
||||
build_prism_speech_bridge "$prismVersion" "$bridgeDir" || return 1
|
||||
fi
|
||||
# Updates replace the game directory, so restore the placeholder each launch.
|
||||
if ! cp "${bridgeDir}/prism_speech_dispatcher_bridge.dll" "$gameDir/"; then
|
||||
echo "Could not copy the Prism speech bridge into the game directory. Check directory permissions and free disk space." >&2
|
||||
return 1
|
||||
fi
|
||||
export WINEDLLPATH="${bridgeDir}${WINEDLLPATH:+:$WINEDLLPATH}"
|
||||
}
|
||||
|
||||
prepare_tekken3_prism_bridge() {
|
||||
local gameDir="$1"
|
||||
local prismHash
|
||||
# Only this Prism 0.18.3 DLL is supported by the Tekken integration.
|
||||
prismHash="$(sha256sum "${gameDir}/T3Access/prism.dll")" || return 1
|
||||
if [[ "${prismHash%% *}" != "7c7d09c8c7306e8e1a0c46d603ccb2a391c194c77843400c11b746e7dd56a467" ]]; then
|
||||
echo "Tekken 3 Access has an unrecognized Prism DLL; update its AGM integration before launching." >&2
|
||||
return 1
|
||||
fi
|
||||
prepare_prism_bridge "$gameDir" "0.18.3" "Tekken3.exe"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,9 @@
|
||||
# shellcheck shell=bash disable=SC2154 # cache, game, and WINEPREFIX are set by audiogame-manager
|
||||
export WINEARCH=win64
|
||||
export winVer="win10"
|
||||
install_wine_bottle
|
||||
if ! update_github_release_game "riadassoum/Arcadelux" "arcadelux" "${WINEPREFIX}/drive_c/Program Files/${game}" "Arcadelux.exe" "savedata"; then
|
||||
echo "Could not install the latest Arcadelux release." >&2
|
||||
exit 1
|
||||
fi
|
||||
add_launcher "c:\Program Files\\${game}\\Arcadelux.exe"
|
||||
+122
-27
@@ -1,28 +1,123 @@
|
||||
#// 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
|
||||
cp "${cache}/mfplat.dll" "${WINEPREFIX}/drive_c/windows/system32/"
|
||||
# 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"
|
||||
|
||||
# Use the same verified package as Winetricks, including on repeat installs
|
||||
# where wmp11 is already recorded but its download cache has been cleared.
|
||||
entombedWmpCache="${W_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/winetricks}/wmp11"
|
||||
entombedWmpInstaller="${entombedWmpCache}/wmp11-windowsxp-x64-enu.exe"
|
||||
mkdir -p "$entombedWmpCache"
|
||||
cache="$entombedWmpCache" download "https://web.archive.org/web/20190512112704/https://download.microsoft.com/download/3/0/8/3080C52C-2517-43DE-BDB4-B7EAFD88F084/wmp11-windowsxp-x64-enu.exe"
|
||||
if ! sha256sum -c <<< "5af407cf336849aff435044ec28f066dd523bbdc22d1ce7aaddb5263084f5526 ${entombedWmpInstaller}"; then
|
||||
agm_msgbox "Entombed" "Entombed" "The cached WMP11 installer failed verification. Remove ${entombedWmpInstaller} and try again."
|
||||
exit 1
|
||||
fi
|
||||
add_launcher "c:\Program Files (x86)\Entombed\Entombed.exe"
|
||||
|
||||
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"
|
||||
|
||||
# Proton prefix setup can replace these native WMP11 DLLs with builtins, while
|
||||
# wmp11's native-only overrides remain. Restore the matching 32-bit pair after
|
||||
# the final UMU invocation so XNA can initialize and play Entombed's WMA music.
|
||||
entombedWmpTempDir="$(mktemp -d)" || exit 1
|
||||
if ! install_with_progress cabextract "Extracting Entombed's WMP11 runtime..." \
|
||||
-q -F wmfdist11-64.exe -d "$entombedWmpTempDir" "$entombedWmpInstaller" ||
|
||||
[[ ! -s "${entombedWmpTempDir}/wmfdist11-64.exe" ]] ||
|
||||
! install_with_progress cabextract "Extracting Entombed's music dependencies..." \
|
||||
-q -d "$entombedWmpTempDir" "${entombedWmpTempDir}/wmfdist11-64.exe" ||
|
||||
[[ ! -s "${entombedWmpTempDir}/wmasf.dll" || ! -s "${entombedWmpTempDir}/mfplat.dll" ]]; then
|
||||
rm -rf "$entombedWmpTempDir"
|
||||
agm_msgbox "Entombed" "Entombed" "Could not extract Entombed's WMP11 music dependencies."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! cp "${entombedWmpTempDir}/wmasf.dll" "${entombedWmpTempDir}/mfplat.dll" \
|
||||
"${WINEPREFIX}/drive_c/windows/syswow64/"; then
|
||||
rm -rf "$entombedWmpTempDir"
|
||||
agm_msgbox "Entombed" "Entombed" "Could not restore Entombed's WMP11 music dependencies."
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$entombedWmpTempDir"
|
||||
|
||||
add_umu_launcher "$entombedGameId" "$entombedPath" "export PROTON_USE_XALIA=0"
|
||||
@@ -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
@@ -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."
|
||||
@@ -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."
|
||||
@@ -0,0 +1,45 @@
|
||||
# shellcheck shell=bash disable=SC2154 # Shared AGM installer state.
|
||||
export WINEARCH=win64
|
||||
export winVer="win10"
|
||||
|
||||
if ! command -v python3 >/dev/null; then
|
||||
alert "Tekken 3" "Install native Linux Python 3, then run this installer again. Windows Python is not required."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(uname -m)" != "x86_64" ]]; then
|
||||
alert "Tekken 3" "This installer currently requires an x86_64 Linux host for the Prism Wine speech bridge."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tekkenDiscDir="${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/tekken-3-disc"
|
||||
if ! mkdir -p "$tekkenDiscDir"; then
|
||||
alert "Tekken 3" "Could not create the disc-image folder: ${tekkenDiscDir}"
|
||||
exit 1
|
||||
fi
|
||||
alert "Tekken 3 disc image" "Use your own Tekken 3 (USA) PlayStation disc dump. Place these four files together in:\n${tekkenDiscDir}\n\nTekken 3 (USA).cue\nTekken 3 (USA) (Track 1).bin\nTekken 3 (USA) (Track 2).bin\nTekken 3 (USA) (Track 3).bin\n\nThis requires the USA three-track BIN/CUE layout. ISO, CHD, other regions, and a single combined BIN are not supported. Keep the filenames and CUE references intact.\n\nPress OK after placing the files. Missing or incompatible files stop installation. AGM reads the originals without moving them. This folder is outside the download cache." || exit 0
|
||||
|
||||
if ! tekkenSetupOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" validate-disc "$tekkenDiscDir" 2>&1)"; then
|
||||
alert "Tekken 3 disc image" "$tekkenSetupOutput"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Keep the base executable pinned while resolving the latest stable access mod.
|
||||
tekkenPortCommit="b6e941378ad03dbd834fef340bd8d90bae6e430b"
|
||||
if ! tekkenSetupOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" latest-release 2>&1)"; then
|
||||
alert "Tekken 3 release" "$tekkenSetupOutput"
|
||||
exit 1
|
||||
fi
|
||||
IFS=$'\t' read -r tekkenModVersion tekkenModUrl tekkenModHash <<< "$tekkenSetupOutput"
|
||||
if ! download "https://github.com/Muqaddas12/Tekken3-PC/archive/${tekkenPortCommit}.zip" "$tekkenModUrl"; then
|
||||
alert "Tekken 3 installation" "Could not download the Tekken 3 port or Access ${tekkenModVersion}."
|
||||
exit 1
|
||||
fi
|
||||
install_wine_bottle
|
||||
if ! tekkenSetupOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" install \
|
||||
"$tekkenDiscDir" "${cache}/${tekkenPortCommit}.zip" "${cache}/${tekkenModUrl##*/}" \
|
||||
"${WINEPREFIX}/drive_c/Program Files/Tekken 3 Access" \
|
||||
--mod-sha256 "$tekkenModHash" --mod-version "$tekkenModVersion" 2>&1)"; then
|
||||
alert "Tekken 3 installation" "$tekkenSetupOutput"
|
||||
exit 1
|
||||
fi
|
||||
add_launcher 'c:\Program Files\Tekken 3 Access\Play Tekken 3 Access.exe'
|
||||
@@ -0,0 +1,6 @@
|
||||
install_wine_bottle
|
||||
download "https://terraformers.nu/terraformers-install1.03.exe"
|
||||
download "https://terraformers.nu/pinTFLicKey.txt.zip"
|
||||
wine "${cache}/terraformers-install1.03.exe" /silent
|
||||
install_with_progress unzip "Extracting game license key..." -d "${WINEPREFIX}/drive_c/Program Files (x86)/Terraformers/Prefs" "${cache}/pinTFLicKey.txt.zip"
|
||||
add_launcher "c:\Program Files (x86)\Terraformers\Game.exe"
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -19,6 +19,20 @@ Select from a list of games to install.
|
||||
|
||||
With no arguments, the list of installed games will be displayed. In all the menus, both installer and launcher, use the up and down arrow keys to find the game you want. Press enter on the game to perform the action. For more detailed help, try the -h flag.
|
||||
|
||||
Use `./audiogame-manager.sh -s` to open the launcher without automatically
|
||||
replacing NVDA controller DLLs with the nvda2speechd versions. This also works
|
||||
with launch options, for example `./audiogame-manager.sh -s -l bottle-name`.
|
||||
Uppercase `-S` remains the speech-rate option.
|
||||
|
||||
To skip replacement on every launch, add `skipDllReplace="true"` to
|
||||
`${XDG_CONFIG_HOME:-$HOME/.config}/storm-games/audiogame-manager/settings.conf`.
|
||||
Replacement is also skipped when `wine-access.exe` is running under the current
|
||||
user, regardless of which screen reader started it. Wine Access starts on demand,
|
||||
so use the flag or setting when it has not started yet. These controls leave
|
||||
already-replaced DLLs as they are and apply to launch-time changes; individual
|
||||
game installers and update scripts still install their speech support, including
|
||||
Tolk DLLs, as before.
|
||||
|
||||
|
||||
## Language and Keyboard
|
||||
|
||||
@@ -46,3 +60,115 @@ Voice names used must exactly match the way they appear in the set-voice.sh menu
|
||||
- MSMike
|
||||
- MSSam
|
||||
- RHVoice
|
||||
|
||||
## Arcadelux speech under Wine
|
||||
|
||||
Arcadelux's Prism NVDA backend uses an RPC discovery function that Wine does
|
||||
not implement. Audio Game Manager prepares Prism's native Speech Dispatcher
|
||||
Wine bridge on the first launch, using the same Prism release bundled with the
|
||||
game. This requires an x86_64 host, CMake, Make, a C/C++ compiler, Wine development
|
||||
tools (`winegcc`, `wineg++`, and `winebuild`), `pkg-config`, and Speech Dispatcher
|
||||
development files. No administrator privileges are used by the launcher.
|
||||
|
||||
The bridge is cached under `${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager/prism-wine/`.
|
||||
A game update that changes Prism's version triggers a matching bridge build at
|
||||
next launch. The launcher restores the Windows placeholder beside Arcadelux.exe
|
||||
and adds the matching host module directory to `WINEDLLPATH`, preserving any
|
||||
existing entries. Other games keep their usual launch behavior.
|
||||
|
||||
## Tekken 3
|
||||
|
||||
Choose **Tekken 3** from `./audiogame-manager.sh -i`. This is a Windows
|
||||
game running under Wine and belongs in audiogame-manager. It uses the normal
|
||||
shared `~/.local/wine64` bottle, not a separate emulator or native Linux game.
|
||||
|
||||
You must supply your own **Tekken 3 (USA), three-track BIN/CUE disc dump**.
|
||||
The installer displays an alert with the location and expected filenames.
|
||||
Place these four files together in
|
||||
`${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/tekken-3-disc/`:
|
||||
|
||||
- `Tekken 3 (USA).cue`
|
||||
- `Tekken 3 (USA) (Track 1).bin`
|
||||
- `Tekken 3 (USA) (Track 2).bin`
|
||||
- `Tekken 3 (USA) (Track 3).bin`
|
||||
|
||||
The CUE must refer to those names. Other regions, ISO, CHD, and combined BIN
|
||||
layouts are not supported. Missing files, unexpected track sizes, and a disc
|
||||
executable that differs from the tested USA release stop installation before
|
||||
downloads. The disc files are read without modification, outside AGM's download
|
||||
cache. After installation, the extracted game data is sufficient for play.
|
||||
|
||||
The installer downloads the tested PC-port commit
|
||||
`b6e941378ad03dbd834fef340bd8d90bae6e430b` and the latest stable Access release
|
||||
from GitHub. It verifies the mod archive against GitHub's published SHA-256
|
||||
checksum, checks the supported mod layout and Prism DLL, verifies the pinned
|
||||
port executable/extractor, and uses native Linux Python 3 to extract game data.
|
||||
Allow approximately 1 GB for installed files, in addition to your original dump.
|
||||
Python 3 is an additional installer dependency.
|
||||
|
||||
Speech uses the same Prism 0.18.3 Speech Dispatcher Wine bridge as Arcadelux,
|
||||
with the build requirements listed above. The bridge is prepared on launch and
|
||||
cached for reuse. No NVDA replacement DLL or Windows Python is needed. The base
|
||||
port stays pinned because the access mod hooks its executable. Mod releases are
|
||||
resolved automatically; a changed Prism DLL or unsupported archive layout stops
|
||||
installation or updating until AGM's integration is updated.
|
||||
|
||||
The game installs in `~/.local/wine64/drive_c/Program Files/Tekken 3 Access/`.
|
||||
Select **Tekken 3** in AGM to launch the access executable. It starts in a
|
||||
window with `p1_device = "auto"`, using the first connected controller. Connect
|
||||
your controller before launching. Options/Start starts the game, the D-pad
|
||||
moves, and Cross confirms menus on a PlayStation controller.
|
||||
|
||||
Controller mode disables ordinary game keyboard input, even without a connected
|
||||
controller; the mod's function keys still work. To play with the keyboard, set
|
||||
`p1_device = "keyboard"` in `settings.toml` and restart. Keyboard controls are
|
||||
Enter for Start, arrows to move, and Z/A/X/S for Square/Triangle/Cross/Circle.
|
||||
The documentation button opens the bundled manual, including the mod keys.
|
||||
|
||||
To update an existing AGM installation, close Tekken 3 and run from this checkout:
|
||||
|
||||
```bash
|
||||
bash game-scripts/tekken3-update.sh
|
||||
```
|
||||
|
||||
This resolves the latest stable Access release on each run, without the original
|
||||
disc dump or another game-data extraction. It checks the installed port executable,
|
||||
preserves settings, controller selection, memory cards, logs and mod preferences,
|
||||
and refreshes the manual. Replaced files are backed up beside the game folder;
|
||||
the completion message gives the version and backup path. GitHub API failures,
|
||||
missing release assets or checksums, and incompatible archives stop the update;
|
||||
there is no fallback to a stale release. Routine mod releases need no version
|
||||
or checksum edits in AGM. A published checksum verifies the download's integrity,
|
||||
but does not establish that a future mod release works correctly under Wine.
|
||||
|
||||
In 1.0.1, Shift+F1 raises the distance hum volume and Control+F1 lowers it, with
|
||||
ten steps and zero for off. The chosen level is remembered. Fight sounds go
|
||||
quiet while the pause menu or command list is open and resume with the fight.
|
||||
|
||||
In 1.0.2, Shift+F2 raises music volume and Control+F2 lowers it. Level ten is
|
||||
the normal game volume; zero turns music off. The level is remembered and also
|
||||
applies to movie audio. Fighters' voices, hits, menu sounds and mod sounds keep
|
||||
their existing volume.
|
||||
|
||||
Before submitting changes, test installation with missing or incorrect disc
|
||||
files and with a valid USA dump. In the installed game, check the loaded
|
||||
announcement, menu and character selection, F1 distance/F2 health, fight sounds,
|
||||
pause/resume, controller input if available, and saving/relaunching. Also check
|
||||
Arcadelux speech if modifying the shared Prism helper. Logs are
|
||||
`T3Access/T3Access.log` and `T3Access/launch.log` in the game folder; copy them
|
||||
before another launch overwrites them.
|
||||
After updating, check both hum-volume shortcuts, level zero, and persistence
|
||||
after restarting. Check sound suppression and resumption in a normal fight,
|
||||
Tekken Force and Tekken Ball, including the pause command list. Confirm your
|
||||
controller/keyboard selection and existing memory-card saves still work.
|
||||
Check both music-volume shortcuts, levels zero and ten, and persistence after
|
||||
restarting. Check music in fights and movies, and confirm voices, hits, menu
|
||||
sounds and mod sounds are unaffected.
|
||||
|
||||
Run the offline release, installer and updater checks with
|
||||
`python3 tests/tekken3_setup_tests.py`. These use temporary fixtures and do not
|
||||
access the network, start Wine or change installed games.
|
||||
|
||||
## AI-assisted contributions
|
||||
|
||||
AI-assisted contributions are welcome. The person submitting the contribution remains responsible for reviewing and testing the changes before submission. Please review the generated diff, run the relevant tests, and describe what changed and how it was tested.
|
||||
+131
-54
@@ -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"
|
||||
@@ -300,6 +303,9 @@ ensure_wine_bottles() {
|
||||
# Initialize wine64 bottle
|
||||
echo "# Initializing wine64 environment..."
|
||||
DISPLAY="" wine wineboot -u
|
||||
|
||||
# Seed Winetricks with the Speech SDK payload before installing dependencies.
|
||||
prepare_speechsdk_cache || return 1
|
||||
|
||||
# Install mono and gecko
|
||||
echo "# Installing .NET Framework..."
|
||||
@@ -324,27 +330,20 @@ ensure_wine_bottles() {
|
||||
|
||||
# Install Speech SDK for SAPI compatibility (experimental - requires WINETRICKS_FORCE=1)
|
||||
echo "# Installing Speech SDK for wine64 SAPI support..."
|
||||
env WINE="$WINE" WINESERVER="$WINESERVER" DISPLAY="${DISPLAY:-:0}" WINETRICKS_FORCE=1 winetricks -q speechsdk
|
||||
WINE="$WINE" WINESERVER="$WINESERVER" DISPLAY="${DISPLAY:-:0}" WINETRICKS_FORCE=1 winetricks -q speechsdk
|
||||
|
||||
# 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 US English..."
|
||||
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 +352,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 +469,38 @@ 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
|
||||
# process_launcher_flags populates umuGameId from launcher settings.
|
||||
# shellcheck disable=SC2154
|
||||
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 +549,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 +572,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
|
||||
@@ -580,12 +602,16 @@ custom_launch_parameters() {
|
||||
if [[ ! -d "${dictPath}/dict" ]]; then
|
||||
cp "${cache}/bk3-dict.dat" "${dictPath}/dict.dat"
|
||||
fi
|
||||
if [[ -r "${cache}/nvdaControllerClient32.dll" ]]; then
|
||||
cp "${cache}/nvdaControllerClient32.dll" "${dataDir}/nvdaControllerClient32.dll"
|
||||
fi
|
||||
if ! should_skip_dll_replace; then
|
||||
if [[ -r "${cache}/bk3-dict.dat" ]]; then
|
||||
if [[ -r "${cache}/nvdaControllerClient32.dll" ]]; then
|
||||
cp "${cache}/nvdaControllerClient32.dll" "${dataDir}/nvdaControllerClient32.dll"
|
||||
fi
|
||||
else
|
||||
rm -f "${dataDir}/nvdaControllerClient32.dll"
|
||||
find "${WINEPREFIX}/drive_c/nyanchangame/bk3" -type f -name 'nvdaControllerClient.dll' -exec rm -v "{}" \;
|
||||
fi
|
||||
else
|
||||
rm -f "${dataDir}/nvdaControllerClient32.dll"
|
||||
find "${WINEPREFIX}/drive_c/nyanchangame/bk3" -type f -name 'nvdaControllerClient.dll' -exec rm -v "{}" \;
|
||||
fi
|
||||
# DLL replacement now handled by update_nvda_dlls()
|
||||
if [[ ! -d "${dictPath}/dict" ]]; then
|
||||
@@ -617,20 +643,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 +664,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 +672,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,19 +693,27 @@ 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
|
||||
}
|
||||
|
||||
should_skip_dll_replace() {
|
||||
[[ "${skipDllReplace:-false}" == "true" ]] ||
|
||||
pgrep -u "$UID" -x 'wine-access[.]exe' &> /dev/null
|
||||
}
|
||||
|
||||
# Update NVDA controller client DLLs in wine bottles
|
||||
update_nvda_dlls() {
|
||||
if should_skip_dll_replace; then
|
||||
log_msg "Skipping NVDA DLL replacement (skipDllReplace enabled or Wine Access running)."
|
||||
return 0
|
||||
fi
|
||||
# Ensure we have the replacement DLLs
|
||||
# shellcheck source=.includes/functions.sh
|
||||
source "${scriptDir}/.includes/functions.sh"
|
||||
@@ -790,14 +824,23 @@ game_launcher() {
|
||||
open_url "https://2mb.games/product/2mb-patron/"
|
||||
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]##*\\}"
|
||||
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]}"
|
||||
local wineVersion
|
||||
wineVersion="$(wine --version)"
|
||||
echo "launching $wineVersion"
|
||||
apply_swamp_quartz_override "${game[1]}" "$wineVersion"
|
||||
# kill any previous existing wineservers for this prefix in case they didn't shut down properly.
|
||||
wineserver -k
|
||||
# launch the game
|
||||
if command -v qjoypad &> /dev/null ; then
|
||||
mkdir -p ~/.qjoypad3
|
||||
@@ -812,7 +855,6 @@ game_launcher() {
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
process_launcher_flags
|
||||
apply_executioners_rage_focus_workaround
|
||||
customLaunchHandled="false"
|
||||
custom_launch_parameters
|
||||
@@ -823,12 +865,26 @@ game_launcher() {
|
||||
start_nvda2speechd
|
||||
# Change to game directory before launching (required for proper game operation)
|
||||
pushd "$(winepath "${game[1]%\\*}")" > /dev/null || exit 1
|
||||
if [[ "${wineExec,,}" == "arcadelux.exe" ]]; then
|
||||
# shellcheck source=.includes/prism-wine.sh
|
||||
source "${scriptDir}/.includes/prism-wine.sh"
|
||||
if ! prepare_prism_speech "Arcadelux Speech" prepare_arcadelux_prism_bridge "$PWD"; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [[ "${wineExec,,}" == "play tekken 3 access.exe" ]]; then
|
||||
# shellcheck source=.includes/prism-wine.sh
|
||||
source "${scriptDir}/.includes/prism-wine.sh"
|
||||
if ! prepare_prism_speech "Tekken 3 Access Speech" prepare_tekken3_prism_bridge "$PWD"; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
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 +957,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 +976,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
|
||||
@@ -953,6 +1014,7 @@ declare -A command=(
|
||||
[R]="Redownload. Removes old versions of packages from cache before installing."
|
||||
[r]="Remove a game. This will delete all game data."
|
||||
[S:]="Speech rate. Requires -V voice name, the default is 7. Values 0-9 or A."
|
||||
[s]="Skip automatic NVDA controller DLL replacement when launching games."
|
||||
[t]="Total games. Show how many games are currently available."
|
||||
[v:]="Select the voice to be installed, default is Bdl. Options are alan, bdl, clb, or slt."
|
||||
[V:]="Select the default voice for a bottle."
|
||||
@@ -961,6 +1023,15 @@ declare -A command=(
|
||||
# Convert the keys of the associative array to a format usable by getopts
|
||||
args="${!command[*]}"
|
||||
args="${args//[[:space:]]/}"
|
||||
# Read this modifier before options such as -l dispatch the launcher immediately.
|
||||
# Use getopts so -s inside another option's argument is not treated as a flag.
|
||||
savedOptind="$OPTIND"
|
||||
while getopts ":${args}" i; do
|
||||
case "$i" in
|
||||
s) skipDllReplace="true";;
|
||||
esac
|
||||
done
|
||||
OPTIND="$savedOptind"
|
||||
while getopts "${args}" i ; do
|
||||
case "$i" in
|
||||
c) "${scriptDir}/.includes/checkup.sh";;
|
||||
@@ -1003,6 +1074,12 @@ while getopts "${args}" i ; do
|
||||
# shellcheck disable=SC2034
|
||||
defaultRate="${OPTARG}"
|
||||
;;
|
||||
s)
|
||||
skipDllReplace="true"
|
||||
if [[ $# -eq 1 ]]; then
|
||||
game_launcher
|
||||
fi
|
||||
;;
|
||||
t)
|
||||
gameCount=$(find "${scriptDir}/.install" -type f -iname "*.sh" | wc -l)
|
||||
agm_infobox "Linux Game Manager" "Linux Game Manager" "There are currently ${gameCount} games 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.
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC1091,SC2154
|
||||
|
||||
scriptDir="$(readlink -f "$0")"
|
||||
scriptDir="${scriptDir%/*}/.."
|
||||
source "${scriptDir}/.includes/dialog-interface.sh"
|
||||
source "${scriptDir}/.includes/functions.sh"
|
||||
|
||||
export game="Arcadelux"
|
||||
cache="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager"
|
||||
mkdir -p "$cache"
|
||||
WINEPREFIX="$HOME/.local/wine64"
|
||||
gamePath="${WINEPREFIX}/drive_c/Program Files/${game}"
|
||||
|
||||
if ! update_github_release_game "riadassoum/Arcadelux" "arcadelux" "$gamePath" "Arcadelux.exe" "savedata"; then
|
||||
alert "Arcadelux Update" "Arcadelux Update" "Could not update Arcadelux."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$githubReleaseGameStatus" == "up-to-date" ]]; then
|
||||
agm_msgbox "Arcadelux Update" "Arcadelux Update" "Arcadelux is up to date."
|
||||
else
|
||||
agm_msgbox "Arcadelux Update" "Arcadelux Update" "Arcadelux has been updated."
|
||||
fi
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/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"
|
||||
else
|
||||
agm_msgbox "Nothing to do; your game is up to date."
|
||||
fi
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC1091 # AGM includes are resolved relative to this script.
|
||||
|
||||
scriptDir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.."
|
||||
# shellcheck source=.includes/dialog-interface.sh
|
||||
source "${scriptDir}/.includes/dialog-interface.sh"
|
||||
# shellcheck source=.includes/functions.sh
|
||||
source "${scriptDir}/.includes/functions.sh"
|
||||
|
||||
export game="Tekken 3"
|
||||
cache="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager"
|
||||
gamePath="$HOME/.local/wine64/drive_c/Program Files/Tekken 3 Access"
|
||||
|
||||
if ! command -v python3 >/dev/null; then
|
||||
alert "Tekken 3 Update" "Install native Linux Python 3, then run this updater again."
|
||||
exit 1
|
||||
fi
|
||||
if ! updateOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" validate-installation "$gamePath" 2>&1)"; then
|
||||
alert "Tekken 3 Update" "$updateOutput"
|
||||
exit 1
|
||||
fi
|
||||
if ! mkdir -p "$cache"; then
|
||||
alert "Tekken 3 Update" "Could not create the download cache: ${cache}"
|
||||
exit 1
|
||||
fi
|
||||
if ! updateOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" latest-release 2>&1)"; then
|
||||
alert "Tekken 3 Update" "$updateOutput"
|
||||
exit 1
|
||||
fi
|
||||
IFS=$'\t' read -r modVersion modUrl modHash <<< "$updateOutput"
|
||||
if ! download "$modUrl"; then
|
||||
alert "Tekken 3 Update" "Could not download Tekken 3 Access ${modVersion}."
|
||||
exit 1
|
||||
fi
|
||||
if ! updateOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" update \
|
||||
"${cache}/${modUrl##*/}" "$gamePath" --mod-sha256 "$modHash" --mod-version "$modVersion" 2>&1)"; then
|
||||
alert "Tekken 3 Update" "$updateOutput"
|
||||
exit 1
|
||||
fi
|
||||
agm_msgbox "Tekken 3 Update" "Tekken 3 Update" "$updateOutput"
|
||||
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare the tested Tekken 3 port and accessibility mod using local disc data."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from urllib.request import Request, urlopen
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
|
||||
|
||||
PORT_COMMIT = "b6e941378ad03dbd834fef340bd8d90bae6e430b"
|
||||
PORT_EXE_HASH = "25d038f55d7a1c02a0e5bbcc97fe9b1738c8292198e7fa0d1b171b57129d18c0"
|
||||
SETUP_HASH = "c1e0430498b2f4212d7e04187e3ba822d3ac273de487cd35d93aa2ef823ac5aa"
|
||||
RELEASE_API = "https://api.github.com/repos/BlindChallengerX/Tekken3-Access/releases/latest"
|
||||
PRISM_HASH = "7c7d09c8c7306e8e1a0c46d603ccb2a391c194c77843400c11b746e7dd56a467"
|
||||
USA_EXE_HASH = "fbda8b68e5799dbef4af39a161783bc670c15b0aa0e87dce65e210717da19b8c"
|
||||
TRACK_SIZES = (632532768, 27701856, 28042896)
|
||||
TRACK_NAMES = tuple(f"Tekken 3 (USA) (Track {trackNumber}).bin" for trackNumber in range(1, 4))
|
||||
CUE_NAME = "Tekken 3 (USA).cue"
|
||||
SECTOR_SIZE = 2352
|
||||
|
||||
|
||||
def parse_release(releaseInfo):
|
||||
if not isinstance(releaseInfo, dict) or releaseInfo.get("draft") is not False or releaseInfo.get("prerelease") is not False:
|
||||
raise ValueError("GitHub did not return a published stable Tekken 3 Access release.")
|
||||
versionMatch = re.fullmatch(r"v?(\d+(?:\.\d+)+)", str(releaseInfo.get("tag_name", "")))
|
||||
if not versionMatch:
|
||||
raise ValueError("The latest Tekken 3 Access release has an unsupported version tag.")
|
||||
version = versionMatch.group(1)
|
||||
archiveName = f"Tekken3Access-{version}.zip"
|
||||
assets = releaseInfo.get("assets")
|
||||
if not isinstance(assets, list):
|
||||
raise ValueError("The latest release has no download assets.")
|
||||
matchingAssets = [asset for asset in assets if isinstance(asset, dict) and asset.get("name") == archiveName]
|
||||
if len(matchingAssets) != 1:
|
||||
raise ValueError(f"The latest release must contain exactly one {archiveName} download.")
|
||||
asset = matchingAssets[0]
|
||||
downloadUrl = f"https://github.com/BlindChallengerX/Tekken3-Access/releases/download/{releaseInfo['tag_name']}/{archiveName}"
|
||||
if asset.get("browser_download_url") != downloadUrl:
|
||||
raise ValueError("The latest release has an unexpected download URL.")
|
||||
digestMatch = re.fullmatch(r"sha256:([0-9a-f]{64})", str(asset.get("digest", "")))
|
||||
if not digestMatch:
|
||||
raise ValueError("GitHub did not supply a SHA-256 checksum for the latest release. Nothing was installed.")
|
||||
return version, downloadUrl, digestMatch.group(1)
|
||||
|
||||
|
||||
def latest_release():
|
||||
request = Request(RELEASE_API, headers={"Accept": "application/vnd.github+json", "User-Agent": "audiogame-manager"})
|
||||
with urlopen(request, timeout=30) as response:
|
||||
return parse_release(json.load(response))
|
||||
|
||||
|
||||
def validate_disc(discDir):
|
||||
missingFiles = [name for name in (CUE_NAME, *TRACK_NAMES) if not (discDir / name).is_file()]
|
||||
if missingFiles:
|
||||
raise ValueError("Missing disc files in " + str(discDir) + ":\n" + "\n".join(missingFiles))
|
||||
cueText = (discDir / CUE_NAME).read_text(encoding="utf-8-sig")
|
||||
cueFiles = re.findall(r'^\s*FILE\s+"([^"]+)"\s+BINARY\s*$', cueText, re.MULTILINE | re.IGNORECASE)
|
||||
cueTracks = re.findall(r"^\s*TRACK\s+(\d+)\s+(\S+)\s*$", cueText, re.MULTILINE | re.IGNORECASE)
|
||||
if tuple(cueFiles) != TRACK_NAMES or [(int(number), mode.upper()) for number, mode in cueTracks] != [(1, "MODE2/2352"), (2, "AUDIO"), (3, "AUDIO")]:
|
||||
raise ValueError("Expected a three-track Tekken 3 (USA) BIN/CUE dump with the listed filenames. Other layouts are not supported.")
|
||||
for trackName, expectedSize in zip(TRACK_NAMES, TRACK_SIZES):
|
||||
if (discDir / trackName).stat().st_size != expectedSize:
|
||||
raise ValueError(f"{trackName} has an unexpected size. Check that the USA three-track dump is complete.")
|
||||
with (discDir / TRACK_NAMES[0]).open("rb") as trackFile:
|
||||
trackFile.seek(25 * SECTOR_SIZE)
|
||||
rawExe = trackFile.read(579 * SECTOR_SIZE)
|
||||
gameExe = b"".join(rawExe[offset + 24:offset + 24 + 2048] for offset in range(0, len(rawExe), SECTOR_SIZE))[:1185792]
|
||||
if hashlib.sha256(gameExe).hexdigest() != USA_EXE_HASH:
|
||||
raise ValueError("The disc executable does not match the tested Tekken 3 (USA) release. Check the region and integrity of the dump.")
|
||||
|
||||
|
||||
def extract_archive(archivePath, destination, stripRoot=None):
|
||||
"""Normalize Windows ZIP separators and reject paths outside the destination."""
|
||||
with ZipFile(archivePath) as archive:
|
||||
for member in archive.infolist():
|
||||
name = member.filename.replace("\\", "/")
|
||||
relativePath = PurePosixPath(name)
|
||||
if relativePath.is_absolute() or ".." in relativePath.parts or ":" in name:
|
||||
raise ValueError(f"Unsafe archive path: {member.filename}")
|
||||
if stat.S_ISLNK(member.external_attr >> 16):
|
||||
raise ValueError(f"Archive links are not supported: {member.filename}")
|
||||
if stripRoot:
|
||||
if not relativePath.parts or relativePath.parts[0] != stripRoot:
|
||||
raise ValueError("The port archive has an unexpected top-level directory.")
|
||||
relativePath = PurePosixPath(*relativePath.parts[1:])
|
||||
if not relativePath.parts:
|
||||
continue
|
||||
outputPath = destination.joinpath(*relativePath.parts)
|
||||
if member.is_dir() or name.endswith("/"):
|
||||
outputPath.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
outputPath.parent.mkdir(parents=True, exist_ok=True)
|
||||
with archive.open(member) as sourceFile, outputPath.open("wb") as outputFile:
|
||||
shutil.copyfileobj(sourceFile, outputFile)
|
||||
|
||||
|
||||
def verify_hash(path, expectedHash):
|
||||
if hashlib.sha256(path.read_bytes()).hexdigest() != expectedHash:
|
||||
raise ValueError(f"{path.name} does not match the expected checksum. Remove the cached download and retry.")
|
||||
|
||||
|
||||
def validate_mod(gameRoot):
|
||||
"""Accept the supported mod layout without replacing game data or user state."""
|
||||
modFiles = {
|
||||
"INSTALL.txt", "Play Tekken 3 Access.exe", "tekken 3 pc link.txt",
|
||||
"Use controller.bat", "Use keyboard.bat", "T3Access/T3Access.dll",
|
||||
"T3Access/prism.dll", "T3Access/README.txt", "T3Access/movies.txt",
|
||||
}
|
||||
for path in gameRoot.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
relativePath = path.relative_to(gameRoot)
|
||||
if relativePath.as_posix() not in modFiles and not (relativePath.parent == Path("T3Access/ad") and relativePath.suffix == ".wav"):
|
||||
raise ValueError(f"Unsupported release file: {relativePath}. Update its AGM integration before installing this release.")
|
||||
for name in ("Play Tekken 3 Access.exe", "T3Access/T3Access.dll", "T3Access/README.txt"):
|
||||
if not (gameRoot / name).is_file():
|
||||
raise ValueError(f"The release is missing {name}.")
|
||||
verify_hash(gameRoot / "T3Access/prism.dll", PRISM_HASH)
|
||||
|
||||
|
||||
def write_manual(gameRoot):
|
||||
manualText = (
|
||||
"Tekken 3 Access under Wine\n\n"
|
||||
"Keyboard: Enter starts; arrows move; Z/A/X/S are Square/Triangle/Cross/Circle.\n"
|
||||
'Controller mode is enabled by default (p1_device = "auto"); connect your controller before launch.\n'
|
||||
'To play with the keyboard, set p1_device = "keyboard" in settings.toml and restart.\n'
|
||||
"The mod function keys work in either mode.\n"
|
||||
"Alt+F4 exits. Settings, memory cards and logs are in this game folder.\n"
|
||||
"AGM uses Prism's native Speech Dispatcher bridge for speech.\n"
|
||||
"The upstream manual follows.\n\n"
|
||||
)
|
||||
(gameRoot / "manual.txt").write_text(manualText + (gameRoot / "T3Access/README.txt").read_text())
|
||||
|
||||
|
||||
def check_game_stopped():
|
||||
for processPath in Path("/proc").iterdir():
|
||||
if not processPath.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
if processPath.stat().st_uid != os.getuid():
|
||||
continue
|
||||
arguments = (processPath / "cmdline").read_bytes().split(b"\0")
|
||||
except (FileNotFoundError, ProcessLookupError, PermissionError):
|
||||
continue
|
||||
if any(argument.replace(b"\\", b"/").rsplit(b"/", 1)[-1].lower()
|
||||
in (b"tekken3.exe", b"play tekken 3 access.exe") for argument in arguments):
|
||||
raise ValueError("Close Tekken 3 and its access launcher before updating.")
|
||||
|
||||
|
||||
def validate_installation(destination):
|
||||
check_game_stopped()
|
||||
if destination.is_symlink():
|
||||
raise ValueError("The game directory must not be a symbolic link.")
|
||||
verify_hash(destination / "Tekken3.exe", PORT_EXE_HASH)
|
||||
for name in ("Play Tekken 3 Access.exe", "T3Access/T3Access.dll", "game_data/disc.cue", "settings.toml"):
|
||||
if not (destination / name).is_file():
|
||||
raise ValueError(f"The existing installation is missing {name}. Install Tekken 3 through AGM first.")
|
||||
|
||||
|
||||
def update_game(modArchive, destination, modHash, modVersion):
|
||||
validate_installation(destination)
|
||||
verify_hash(modArchive, modHash)
|
||||
with tempfile.TemporaryDirectory(prefix=".tekken3-update-", dir=destination.parent) as temporaryDir:
|
||||
gameRoot = Path(temporaryDir) / "mod"
|
||||
gameRoot.mkdir()
|
||||
extract_archive(modArchive, gameRoot)
|
||||
validate_mod(gameRoot)
|
||||
write_manual(gameRoot)
|
||||
relativeFiles = sorted(path.relative_to(gameRoot) for path in gameRoot.rglob("*") if path.is_file())
|
||||
for relativePath in relativeFiles:
|
||||
targetPath = destination / relativePath
|
||||
for path in (targetPath, *targetPath.parents):
|
||||
if path == destination.parent:
|
||||
break
|
||||
if path.is_symlink():
|
||||
raise ValueError(f"Update target must not be a symbolic link: {path}")
|
||||
if targetPath.exists() and not targetPath.is_file():
|
||||
raise ValueError(f"Update target is not a file: {targetPath}")
|
||||
for parentPath in targetPath.parents:
|
||||
if parentPath == destination:
|
||||
break
|
||||
if parentPath.exists() and not parentPath.is_dir():
|
||||
raise ValueError(f"Update target parent is not a directory: {parentPath}")
|
||||
backupRoot = Path(tempfile.mkdtemp(prefix=".tekken3-access-backup-", dir=destination.parent))
|
||||
for relativePath in relativeFiles:
|
||||
sourcePath = destination / relativePath
|
||||
if sourcePath.exists():
|
||||
backupPath = backupRoot / relativePath
|
||||
backupPath.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(sourcePath, backupPath)
|
||||
check_game_stopped()
|
||||
appliedFiles = []
|
||||
try:
|
||||
for relativePath in relativeFiles:
|
||||
targetPath = destination / relativePath
|
||||
targetPath.parent.mkdir(parents=True, exist_ok=True)
|
||||
(gameRoot / relativePath).replace(targetPath)
|
||||
appliedFiles.append(relativePath)
|
||||
except OSError as error:
|
||||
try:
|
||||
for relativePath in reversed(appliedFiles):
|
||||
targetPath = destination / relativePath
|
||||
backupPath = backupRoot / relativePath
|
||||
if backupPath.exists():
|
||||
shutil.copy2(backupPath, targetPath)
|
||||
else:
|
||||
targetPath.unlink()
|
||||
except OSError as restoreError:
|
||||
raise OSError(f"Update and restoration failed. Restore files from {backupRoot}. {restoreError}") from error
|
||||
raise OSError(f"Update failed; replaced files restored. Backup: {backupRoot}. {error}") from error
|
||||
print(f"Updated Tekken 3 Access to {modVersion}. Backup of replaced files: {backupRoot}")
|
||||
|
||||
|
||||
def install_game(discDir, portArchive, modArchive, destination, modHash, modVersion):
|
||||
validate_disc(discDir)
|
||||
verify_hash(modArchive, modHash)
|
||||
if destination.exists() and any(destination.iterdir()):
|
||||
raise ValueError(f"An installation already exists at {destination}. It was left untouched; back it up before reinstalling.")
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix=".tekken3-setup-", dir=destination.parent) as temporaryDir:
|
||||
gameRoot = Path(temporaryDir) / "game"
|
||||
gameRoot.mkdir()
|
||||
extract_archive(portArchive, gameRoot, f"Tekken3-PC-{PORT_COMMIT}")
|
||||
verify_hash(gameRoot / "Tekken3.exe", PORT_EXE_HASH)
|
||||
verify_hash(gameRoot / "setup_game_data.py", SETUP_HASH)
|
||||
modRoot = Path(temporaryDir) / "mod"
|
||||
modRoot.mkdir()
|
||||
extract_archive(modArchive, modRoot)
|
||||
validate_mod(modRoot)
|
||||
shutil.copytree(modRoot, gameRoot, dirs_exist_ok=True)
|
||||
subprocess.run([sys.executable, str(gameRoot / "setup_game_data.py"), str(discDir.resolve())], check=True)
|
||||
for requiredFile in ("Play Tekken 3 Access.exe", "T3Access/T3Access.dll", "T3Access/prism.dll", "game_data/disc.cue"):
|
||||
if not (gameRoot / requiredFile).is_file():
|
||||
raise ValueError(f"Setup did not produce {requiredFile}.")
|
||||
settingsPath = gameRoot / "settings.toml"
|
||||
settingsText = settingsPath.read_text()
|
||||
settingsPath.write_text(settingsText.replace("fullscreen = 1", "fullscreen = 0").replace("supersampling = 3", "supersampling = 1").replace("window_width = 1920", "window_width = 960").replace('p1_device = "keyboard"', 'p1_device = "auto"'))
|
||||
write_manual(gameRoot)
|
||||
if destination.exists():
|
||||
destination.rmdir() # Only an empty destination is accepted above.
|
||||
gameRoot.rename(destination)
|
||||
print(f"Installed Tekken 3 Access {modVersion} in {destination}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
subparsers.add_parser("latest-release")
|
||||
validateParser = subparsers.add_parser("validate-disc")
|
||||
validateParser.add_argument("discDir", type=Path)
|
||||
installedParser = subparsers.add_parser("validate-installation")
|
||||
installedParser.add_argument("destination", type=Path)
|
||||
updateParser = subparsers.add_parser("update")
|
||||
updateParser.add_argument("modArchive", type=Path)
|
||||
updateParser.add_argument("destination", type=Path)
|
||||
installParser = subparsers.add_parser("install")
|
||||
for name in ("discDir", "portArchive", "modArchive", "destination"):
|
||||
installParser.add_argument(name, type=Path)
|
||||
for commandParser in (updateParser, installParser):
|
||||
commandParser.add_argument("--mod-sha256", required=True)
|
||||
commandParser.add_argument("--mod-version", required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.command == "latest-release":
|
||||
print("\t".join(latest_release()))
|
||||
elif args.command == "validate-disc":
|
||||
validate_disc(args.discDir)
|
||||
print("Tekken 3 (USA) disc files validated.")
|
||||
elif args.command == "validate-installation":
|
||||
validate_installation(args.destination)
|
||||
elif args.command == "update":
|
||||
update_game(args.modArchive, args.destination, args.mod_sha256, args.mod_version)
|
||||
else:
|
||||
install_game(args.discDir, args.portArchive, args.modArchive, args.destination, args.mod_sha256, args.mod_version)
|
||||
except (OSError, ValueError, BadZipFile, subprocess.CalledProcessError) as error:
|
||||
print(f"Tekken 3 setup failed: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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
|
||||
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export WINEPREFIX="${WINEPREFIX:-${HOME}/.local/wine64}"
|
||||
export WINEDEBUG="${WINEDEBUG:--all}"
|
||||
|
||||
installerUrl="${WINESPEAK_INSTALLER_URL:-https://stormgames.wolfe.casa/downloads/winespeak.exe}"
|
||||
installerName="winespeak.exe"
|
||||
installerSha256="${WINESPEAK_INSTALLER_SHA256:-8007004cd024b391e8f1962455ffe2a8b241cfdf52641a2bc1aa2274bc80f236}"
|
||||
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="2282da4a23f18b40e6601a4b9a880f484196bf2f5138881e76428be10d8dbe76"
|
||||
cscriptPath="${WINEPREFIX}/drive_c/windows/syswow64/cscript.exe"
|
||||
verifyScriptPath="${WINEPREFIX}/drive_c/windows/temp/verify_winespeak.vbs"
|
||||
voiceToken="HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\Espeak-ng US English"
|
||||
|
||||
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
|
||||
local voiceOutput
|
||||
|
||||
download_installer
|
||||
|
||||
wineserver -k || true
|
||||
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 WineSpeak US English 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
|
||||
|
||||
if [[ ! -f "$cscriptPath" ]]; then
|
||||
fail "Wine's 32-bit cscript.exe was not found. Install speechsdk in this prefix first."
|
||||
fi
|
||||
mkdir -p "${verifyScriptPath%/*}"
|
||||
printf '%s\n' \
|
||||
'dim speechobject' \
|
||||
'set speechobject=createobject("sapi.spvoice")' \
|
||||
'wscript.echo speechobject.voice.getdescription' > "$verifyScriptPath"
|
||||
if ! voiceOutput="$(wine "$cscriptPath" //nologo 'c:\windows\temp\verify_winespeak.vbs' 2>&1)"; then
|
||||
fail "WineSpeak's 32-bit SAPI voice could not be created: ${voiceOutput}"
|
||||
fi
|
||||
if ! grep -Fq 'Espeak-ng US English' <<< "$voiceOutput"; then
|
||||
fail "WineSpeak's 32-bit SAPI verification returned an unexpected voice: ${voiceOutput}"
|
||||
fi
|
||||
|
||||
printf 'WineSpeak is installed and US English is the default SAPI voice.\n'
|
||||
}
|
||||
|
||||
if [[ ! -d "${WINEPREFIX}/drive_c" ]]; then
|
||||
fail "Wine prefix does not exist: ${WINEPREFIX}"
|
||||
fi
|
||||
|
||||
for commandName in curl wine wineserver sha256sum awk grep; do
|
||||
require_command "$commandName"
|
||||
done
|
||||
|
||||
install_winespeak
|
||||
@@ -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
|
||||
Reference in new issue
Block a user