Compare commits
30
Commits
92745d8af8
...
testing
| 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 |
No files matched your search
@@ -1,3 +1,4 @@
|
|||||||
*.x.c
|
*.x.c
|
||||||
*.x
|
*.x
|
||||||
music/
|
music/
|
||||||
|
/game.log
|
||||||
+154
-12
@@ -22,6 +22,35 @@ get_bottle() {
|
|||||||
fi
|
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 environment for installation (called from install_wine_bottle)
|
||||||
set_wine_env() {
|
set_wine_env() {
|
||||||
local architecture="$1"
|
local architecture="$1"
|
||||||
@@ -40,15 +69,41 @@ set_wine_env() {
|
|||||||
|
|
||||||
# Note: install_wine function removed - we now use system wine with simplified bottle management
|
# 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() {
|
winetricks() {
|
||||||
# Report used packages to the winetricks maintainer so he knows they are being used.
|
# 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
|
enable_winetricks_usage_reporting
|
||||||
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
|
|
||||||
# Download or update agm's copy of winetricks
|
# Download or update agm's copy of winetricks
|
||||||
if [[ ! -e "${winetricksPath}/winetricks" ]]; then
|
if [[ ! -e "${winetricksPath}/winetricks" ]]; then
|
||||||
checkWinetricksUpdate="true"
|
checkWinetricksUpdate="true"
|
||||||
@@ -58,17 +113,104 @@ winetricks() {
|
|||||||
else
|
else
|
||||||
if [[ "$checkWinetricksUpdate" != "true" ]]; then
|
if [[ "$checkWinetricksUpdate" != "true" ]]; then
|
||||||
checkWinetricksUpdate="true"
|
checkWinetricksUpdate="true"
|
||||||
${winetricksPath}/winetricks --self-update
|
"${winetricksPath}/winetricks" --self-update
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# Run the requested winetricks parameters
|
# Run the requested winetricks parameters
|
||||||
if command -v FEXLoader &> /dev/null ; then
|
if command -v FEXLoader &> /dev/null ; then
|
||||||
WINE="" FEXLoader -- ${winetricksPath}/winetricks "$@"
|
WINE="" FEXLoader -- "${winetricksPath}/winetricks" "$@"
|
||||||
else
|
else
|
||||||
${winetricksPath}/winetricks "$@"
|
"${winetricksPath}/winetricks" "$@"
|
||||||
fi
|
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() {
|
install_rhvoice() {
|
||||||
if [[ -d "${WINEPREFIX}/drive_c/Program Files/Olga Yakovleva/" ]]; then
|
if [[ -d "${WINEPREFIX}/drive_c/Program Files/Olga Yakovleva/" ]]; then
|
||||||
return
|
return
|
||||||
@@ -168,7 +310,7 @@ install_wine_bottle() {
|
|||||||
if [[ ${#regularDeps[@]} -gt 0 ]]; then
|
if [[ ${#regularDeps[@]} -gt 0 ]]; then
|
||||||
echo "Installing additional dependencies: ${regularDeps[*]}"
|
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..."
|
} | agm_progressbox "Wine Setup" "Installing additional dependencies..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -176,7 +318,7 @@ install_wine_bottle() {
|
|||||||
if [[ "$needsSpeechsdk" == "true" ]]; then
|
if [[ "$needsSpeechsdk" == "true" ]]; then
|
||||||
echo "Installing speechsdk with WINETRICKS_FORCE=1"
|
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..."
|
} | agm_progressbox "Wine Setup" "Installing Speech SDK..."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -251,3 +251,166 @@ install_with_progress() {
|
|||||||
;;
|
;;
|
||||||
esac
|
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
|
||||||
|
}
|
||||||
@@ -142,6 +142,7 @@ help() {
|
|||||||
echo "noqjoypad=\"true\" # Do not launch qjoypad."
|
echo "noqjoypad=\"true\" # Do not launch qjoypad."
|
||||||
echo "norh=\"true\" # Do not install RHVoice."
|
echo "norh=\"true\" # Do not install RHVoice."
|
||||||
echo "redownload=\"true\" # Redownload sources, do not use the version stored in cache."
|
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 "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 "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."
|
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"
|
[nvdaControllerClient32]="${ipfsGateway}/ipfs/QmTrRrT4QFKSkZ8ivfUawA6iJ6adEyyogccE3nLDTfSK8u?filename=nvdaControllerClient32.dll"
|
||||||
[nvdaControllerClient64]="${ipfsGateway}/ipfs/QmaYE7RFDtwHCiXCVLcuA3esfFx6E7koidtvrck9AwPuuN?filename=nvdaControllerClient64.dll"
|
[nvdaControllerClient64]="${ipfsGateway}/ipfs/QmaYE7RFDtwHCiXCVLcuA3esfFx6E7koidtvrck9AwPuuN?filename=nvdaControllerClient64.dll"
|
||||||
[nvda2speechd]="${ipfsGateway}/ipfs/QmPxhoNsoFoJC7bCfioBBCcK8tEoSoYpm342z6u7KjFsVz?filename=nvda2speechd"
|
[nvda2speechd]="${ipfsGateway}/ipfs/QmPxhoNsoFoJC7bCfioBBCcK8tEoSoYpm342z6u7KjFsVz?filename=nvda2speechd"
|
||||||
|
[speechsdk]="${ipfsGateway}/ipfs/QmXKp7xKoaLmpLJ8GoGJbQcGXnfMkfoqq5PDpyWHWnL9DB?filename=speechsdk.zip"
|
||||||
|
[winespeak]="https://stormgames.wolfe.casa/downloads/winespeak.exe"
|
||||||
|
|
||||||
# Games (alphabetical order)
|
# Games (alphabetical order)
|
||||||
[BG 15 Puzzle]="${ipfsGateway}/ipfs/QmQiocMpMXoxejDftKKvmrR5xxpj1qcWcgkhBBwTcyijXg?filename=FPB32Setup10a.exe"
|
[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"
|
||||||
|
}
|
||||||
+27
-1
@@ -44,6 +44,32 @@ install_proton_bottle() {
|
|||||||
fi
|
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() {
|
umu_windows_path_to_unix() {
|
||||||
local windowsPath="$1"
|
local windowsPath="$1"
|
||||||
local relativePath=""
|
local relativePath=""
|
||||||
@@ -71,7 +97,7 @@ run_umu_game() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
pushd "${exePath%/*}" > /dev/null || return 1
|
pushd "${exePath%/*}" > /dev/null || return 1
|
||||||
umu-run "$exePath"
|
run_with_optional_gamemode umu-run "$exePath"
|
||||||
popd > /dev/null || return 1
|
popd > /dev/null || return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,41 @@
|
|||||||
download "https://blindgamers.com/downloads/a-heros-call-freeware.zip" "https://stormgames.wolfe.casa/downloads/nvdaControllerClient32.dll"
|
# shellcheck shell=bash disable=SC2154 # cache, game, and helper functions are set by audiogame-manager.
|
||||||
export winVer="win7"
|
|
||||||
export winetricksSettings="vd=1024x768"
|
export game="A Hero's Call"
|
||||||
install_wine_bottle
|
herosCallGameId="a-heros-call"
|
||||||
# Dotnet is evil. That is all.
|
herosCallPath="c:\\Program Files\\a-heros-call\\A Hero's Call.exe"
|
||||||
LC_ALL=C DISPLAY="" winetricks -q dotnet462 xna40
|
|
||||||
wineserver -k # Really!
|
download "https://blindgamers.com/downloads/a-heros-call-freeware.zip" \
|
||||||
install_with_progress unzip "Extracting game files..." -d "$WINEPREFIX/drive_c/Program Files" "${cache}/a-heros-call-freeware.zip"
|
"https://stormgames.wolfe.casa/downloads/nvdaControllerClient32.dll"
|
||||||
add_launcher "c:\Program Files\a-heros-call\A Hero's Call.exe"
|
|
||||||
|
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
|
# shellcheck shell=bash disable=SC2154 # cache, game, and helper functions are set by audiogame-manager.
|
||||||
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 game="Entombed"
|
||||||
export winVer="win7"
|
entombedGameId="entombed"
|
||||||
install_wine_bottle sapi msvcrt40 gdiplus ie7 wmp11 mf
|
entombedPath='c:\Program Files (x86)\Entombed\Entombed.exe'
|
||||||
# Ok, more dotnet.
|
|
||||||
LC_ALL=C DISPLAY="" winetricks -q dotnet40 xna40
|
download "http://blind-games.com/newentombed/EntombedSetup.exe" \
|
||||||
wineserver -k # Sigh.
|
"https://download.microsoft.com/download/E/C/1/EC1B2340-67A0-4B87-85F0-74D987A27160/SSCERuntime-ENU.exe" \
|
||||||
mkdir -p "${WINEPREFIX}/drive_c/temp"
|
"https://stormgames.wolfe.casa/downloads/Entombed.exe.config"
|
||||||
pushd "${WINEPREFIX}/drive_c/temp"
|
|
||||||
install_with_progress 7z "Extracting game files..." x "${cache}/SSCERuntime-ENU.exe"
|
# Use the same verified package as Winetricks, including on repeat installs
|
||||||
wine msiexec /i "${WINEPREFIX}/drive_c/temp/SSCERuntime_x86-ENU.msi" /q
|
# where wmp11 is already recorded but its download cache has been cleared.
|
||||||
rm *
|
entombedWmpCache="${W_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/winetricks}/wmp11"
|
||||||
popd
|
entombedWmpInstaller="${entombedWmpCache}/wmp11-windowsxp-x64-enu.exe"
|
||||||
pushd "${WINEPREFIX}/drive_c/Program Files/Microsoft SQL Server Compact Edition/v3.5"
|
mkdir -p "$entombedWmpCache"
|
||||||
wine regsvr32 sqlceoledb35.dll
|
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"
|
||||||
wine regsvr32 sqlceca35.dll
|
if ! sha256sum -c <<< "5af407cf336849aff435044ec28f066dd523bbdc22d1ce7aaddb5263084f5526 ${entombedWmpInstaller}"; then
|
||||||
popd
|
agm_msgbox "Entombed" "Entombed" "The cached WMP11 installer failed verification. Remove ${entombedWmpInstaller} and try again."
|
||||||
wine "${cache}/EntombedSetup.exe" /silent
|
exit 1
|
||||||
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/"
|
|
||||||
fi
|
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
|
# shellcheck shell=bash disable=SC2154 # cache and WINEPREFIX are set by audiogame-manager
|
||||||
export WINEARCH="win64" # Migrated to wine64
|
|
||||||
export winVer="win7"
|
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_wine_bottle
|
||||||
install_with_progress 7z "Extracting game files..." x -o"$WINEPREFIX/drive_c/Program Files/Mist World" "$cache/Mist World_Setup.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/Mist World/mw.exe.vmoptions"
|
sed -i 's/1024m/768m/g' "$WINEPREFIX/drive_c/Program Files (x86)/Mist World/mw.exe.vmoptions"
|
||||||
cp "$WINEPREFIX/drive_c/Program Files/Mist World/"{mw.exe.vmoptions,update.exe.vmoptions}
|
cp "$WINEPREFIX/drive_c/Program Files (x86)/Mist World/"{mw.exe.vmoptions,update.exe.vmoptions}
|
||||||
mkdir "$WINEPREFIX/drive_c/Program Files/Mist World/"{user,users}
|
mkdir "$WINEPREFIX/drive_c/Program Files (x86)/Mist World/"{user,users}
|
||||||
add_launcher 'c:\Program Files\Mist World\mw.exe'
|
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."
|
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."
|
||||||
@@ -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.
|
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
|
## Language and Keyboard
|
||||||
|
|
||||||
@@ -46,3 +60,115 @@ Voice names used must exactly match the way they appear in the set-voice.sh menu
|
|||||||
- MSMike
|
- MSMike
|
||||||
- MSSam
|
- MSSam
|
||||||
- RHVoice
|
- 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.
|
||||||
+89
-40
@@ -33,7 +33,7 @@ start_nvda2speechd() {
|
|||||||
if [[ "$nvda2speechdStarted" == "true" ]]; then
|
if [[ "$nvda2speechdStarted" == "true" ]]; then
|
||||||
return
|
return
|
||||||
fi
|
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
|
if [[ -x "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" ]]; then
|
||||||
local translateSetting="${TRANSLATE:-unset}"
|
local translateSetting="${TRANSLATE:-unset}"
|
||||||
local translateFromSetting="${TRANSLATE_FROM:-unset}"
|
local translateFromSetting="${TRANSLATE_FROM:-unset}"
|
||||||
@@ -285,11 +285,14 @@ check_wine32() {
|
|||||||
# Ensure wine64 bottle exists with all dependencies (wine32 no longer needed!)
|
# Ensure wine64 bottle exists with all dependencies (wine32 no longer needed!)
|
||||||
ensure_wine_bottles() {
|
ensure_wine_bottles() {
|
||||||
local wine64Bottle="$HOME/.local/wine64"
|
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
|
# 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..."
|
echo "# Creating wine64 bottle for modern games..."
|
||||||
|
mkdir -p "$wine64Bottle"
|
||||||
|
touch "$setupMarker"
|
||||||
|
|
||||||
# Set up environment for wine64
|
# Set up environment for wine64
|
||||||
export WINEPREFIX="$wine64Bottle"
|
export WINEPREFIX="$wine64Bottle"
|
||||||
@@ -300,6 +303,9 @@ ensure_wine_bottles() {
|
|||||||
# Initialize wine64 bottle
|
# Initialize wine64 bottle
|
||||||
echo "# Initializing wine64 environment..."
|
echo "# Initializing wine64 environment..."
|
||||||
DISPLAY="" wine wineboot -u
|
DISPLAY="" wine wineboot -u
|
||||||
|
|
||||||
|
# Seed Winetricks with the Speech SDK payload before installing dependencies.
|
||||||
|
prepare_speechsdk_cache || return 1
|
||||||
|
|
||||||
# Install mono and gecko
|
# Install mono and gecko
|
||||||
echo "# Installing .NET Framework..."
|
echo "# Installing .NET Framework..."
|
||||||
@@ -324,27 +330,20 @@ ensure_wine_bottles() {
|
|||||||
|
|
||||||
# Install Speech SDK for SAPI compatibility (experimental - requires WINETRICKS_FORCE=1)
|
# Install Speech SDK for SAPI compatibility (experimental - requires WINETRICKS_FORCE=1)
|
||||||
echo "# Installing Speech SDK for wine64 SAPI support..."
|
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)
|
# Restore win10 after speechsdk (speechsdk sets winxp)
|
||||||
winetricks -q win10
|
winetricks -q win10
|
||||||
|
|
||||||
# Initialize SAPI and set Microsoft Mike as default voice
|
# Install and verify the default 32-bit SAPI voice.
|
||||||
echo "# Setting Microsoft Mike as default voice..."
|
echo "# Installing WineSpeak US English..."
|
||||||
mkdir -p "${WINEPREFIX}/drive_c/windows/temp"
|
if ! install_winespeak; then
|
||||||
cat << "EOF" > "${WINEPREFIX}/drive_c/windows/temp/init_sapi.vbs"
|
echo "# WineSpeak installation or SAPI verification failed."
|
||||||
dim speechobject
|
return 1
|
||||||
set speechobject=createobject("sapi.spvoice")
|
fi
|
||||||
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"
|
|
||||||
|
|
||||||
# Setup nvda2speechd for accessibility if needed
|
# 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..."
|
echo "# Setting up accessibility support..."
|
||||||
download "${nvda2speechdBinary}"
|
download "${nvda2speechdBinary}"
|
||||||
if [[ ! -f "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" ]]; then
|
if [[ ! -f "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" ]]; then
|
||||||
@@ -353,6 +352,7 @@ EOF
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rm -f "$setupMarker"
|
||||||
echo "# Wine64 bottle creation complete."
|
echo "# Wine64 bottle creation complete."
|
||||||
} > >(agm_progressbox "Wine Bottle Setup" "Creating unified wine64 bottle with SAPI support (this may take several minutes)...")
|
} > >(agm_progressbox "Wine Bottle Setup" "Creating unified wine64 bottle with SAPI support (this may take several minutes)...")
|
||||||
fi
|
fi
|
||||||
@@ -471,6 +471,8 @@ game_removal() {
|
|||||||
if [[ ${#game[@]} -gt 0 ]]; then
|
if [[ ${#game[@]} -gt 0 ]]; then
|
||||||
process_launcher_flags
|
process_launcher_flags
|
||||||
if [[ "${game[0]}" == "umu" ]]; then
|
if [[ "${game[0]}" == "umu" ]]; then
|
||||||
|
# process_launcher_flags populates umuGameId from launcher settings.
|
||||||
|
# shellcheck disable=SC2154
|
||||||
get_umu_bottle "$umuGameId"
|
get_umu_bottle "$umuGameId"
|
||||||
else
|
else
|
||||||
# Set up wine environment for this game
|
# Set up wine environment for this game
|
||||||
@@ -570,14 +572,14 @@ custom_launch_parameters() {
|
|||||||
fi
|
fi
|
||||||
start_nvda2speechd
|
start_nvda2speechd
|
||||||
pushd "$(winepath "$winePath")" || exit 1
|
pushd "$(winepath "$winePath")" || exit 1
|
||||||
wine "$wineExec"
|
run_with_optional_gamemode wine "$wineExec"
|
||||||
popd || exit 1
|
popd || exit 1
|
||||||
customLaunchHandled="true"
|
customLaunchHandled="true"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [[ "${game[2]}" == "Dreamland" ]]; then
|
if [[ "${game[2]}" == "Dreamland" ]]; then
|
||||||
start_nvda2speechd
|
start_nvda2speechd
|
||||||
"$WINE" "${game[1]}" &> /dev/null
|
run_with_optional_gamemode "$WINE" "${game[1]}" &> /dev/null
|
||||||
customLaunchHandled="true"
|
customLaunchHandled="true"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -600,12 +602,16 @@ custom_launch_parameters() {
|
|||||||
if [[ ! -d "${dictPath}/dict" ]]; then
|
if [[ ! -d "${dictPath}/dict" ]]; then
|
||||||
cp "${cache}/bk3-dict.dat" "${dictPath}/dict.dat"
|
cp "${cache}/bk3-dict.dat" "${dictPath}/dict.dat"
|
||||||
fi
|
fi
|
||||||
if [[ -r "${cache}/nvdaControllerClient32.dll" ]]; then
|
fi
|
||||||
cp "${cache}/nvdaControllerClient32.dll" "${dataDir}/nvdaControllerClient32.dll"
|
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
|
fi
|
||||||
else
|
|
||||||
rm -f "${dataDir}/nvdaControllerClient32.dll"
|
|
||||||
find "${WINEPREFIX}/drive_c/nyanchangame/bk3" -type f -name 'nvdaControllerClient.dll' -exec rm -v "{}" \;
|
|
||||||
fi
|
fi
|
||||||
# DLL replacement now handled by update_nvda_dlls()
|
# DLL replacement now handled by update_nvda_dlls()
|
||||||
if [[ ! -d "${dictPath}/dict" ]]; then
|
if [[ ! -d "${dictPath}/dict" ]]; then
|
||||||
@@ -637,20 +643,20 @@ custom_launch_parameters() {
|
|||||||
# sketchbook: DLL replacement now handled by update_nvda_dlls()
|
# sketchbook: DLL replacement now handled by update_nvda_dlls()
|
||||||
if [[ "${game[2]}" == "Audiodisc" ]]; then
|
if [[ "${game[2]}" == "Audiodisc" ]]; then
|
||||||
start_nvda2speechd
|
start_nvda2speechd
|
||||||
wine "$winePath\\$wineExec"
|
run_with_optional_gamemode wine "$winePath\\$wineExec"
|
||||||
customLaunchHandled="true"
|
customLaunchHandled="true"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [[ "${game[2]}" == "Audioquake" ]]; then
|
if [[ "${game[2]}" == "Audioquake" ]]; then
|
||||||
start_nvda2speechd
|
start_nvda2speechd
|
||||||
wine "$winePath\\$wineExec"
|
run_with_optional_gamemode wine "$winePath\\$wineExec"
|
||||||
customLaunchHandled="true"
|
customLaunchHandled="true"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [[ "${game[2]}" == "Screaming Strike 2" ]]; then
|
if [[ "${game[2]}" == "Screaming Strike 2" ]]; then
|
||||||
start_nvda2speechd
|
start_nvda2speechd
|
||||||
pushd "$(winepath "$winePath")" || exit 1
|
pushd "$(winepath "$winePath")" || exit 1
|
||||||
wine "$wineExec"
|
run_with_optional_gamemode wine "$wineExec"
|
||||||
popd || exit 1
|
popd || exit 1
|
||||||
customLaunchHandled="true"
|
customLaunchHandled="true"
|
||||||
return 0
|
return 0
|
||||||
@@ -658,7 +664,7 @@ custom_launch_parameters() {
|
|||||||
if [[ "${game[2]}" == "Warsim" ]]; then
|
if [[ "${game[2]}" == "Warsim" ]]; then
|
||||||
start_nvda2speechd
|
start_nvda2speechd
|
||||||
pushd "$(winepath "${game[1]%\\*}")" || exit 1
|
pushd "$(winepath "${game[1]%\\*}")" || exit 1
|
||||||
wine "${game[1]##*\\}"
|
run_with_optional_gamemode wine "${game[1]##*\\}"
|
||||||
popd || exit 1
|
popd || exit 1
|
||||||
customLaunchHandled="true"
|
customLaunchHandled="true"
|
||||||
return 0
|
return 0
|
||||||
@@ -666,7 +672,7 @@ custom_launch_parameters() {
|
|||||||
if [[ "${game[2]}" == "Interceptor" ]]; then
|
if [[ "${game[2]}" == "Interceptor" ]]; then
|
||||||
start_nvda2speechd
|
start_nvda2speechd
|
||||||
pushd "$(winepath "$winePath")" || exit 1
|
pushd "$(winepath "$winePath")" || exit 1
|
||||||
wine "$wineExec"
|
run_with_optional_gamemode wine "$wineExec"
|
||||||
popd || exit 1
|
popd || exit 1
|
||||||
customLaunchHandled="true"
|
customLaunchHandled="true"
|
||||||
return 0
|
return 0
|
||||||
@@ -687,19 +693,27 @@ create_game_array() {
|
|||||||
# Game array 0 bottle, 1 path, 2 title, 3+ flags
|
# Game array 0 bottle, 1 path, 2 title, 3+ flags
|
||||||
local selectedGame="$1"
|
local selectedGame="$1"
|
||||||
for i in "${lines[@]}" ; do
|
for i in "${lines[@]}" ; do
|
||||||
# Only compare the launcher section
|
local selectedLauncher="${selectedGame#*|}"
|
||||||
local j="${selectedGame#*|}"
|
local candidate=()
|
||||||
local k="${i#*|}"
|
IFS='|' read -ra candidate <<< "$i"
|
||||||
k="${k%%|*}"
|
if [[ "$selectedLauncher" == "${candidate[1]}" || "$selectedGame" == "${candidate[2]}" ]]; then
|
||||||
if [[ "$j" == "$k" ]]; then
|
game=("${candidate[@]}")
|
||||||
IFS='|' read -ra game <<< "$i"
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
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 controller client DLLs in wine bottles
|
||||||
update_nvda_dlls() {
|
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
|
# Ensure we have the replacement DLLs
|
||||||
# shellcheck source=.includes/functions.sh
|
# shellcheck source=.includes/functions.sh
|
||||||
source "${scriptDir}/.includes/functions.sh"
|
source "${scriptDir}/.includes/functions.sh"
|
||||||
@@ -821,8 +835,10 @@ game_launcher() {
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
get_bottle "${game[0]}"
|
get_bottle "${game[0]}"
|
||||||
echo -n "launching "
|
local wineVersion
|
||||||
wine --version
|
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.
|
# kill any previous existing wineservers for this prefix in case they didn't shut down properly.
|
||||||
wineserver -k
|
wineserver -k
|
||||||
# launch the game
|
# launch the game
|
||||||
@@ -849,12 +865,26 @@ game_launcher() {
|
|||||||
start_nvda2speechd
|
start_nvda2speechd
|
||||||
# Change to game directory before launching (required for proper game operation)
|
# Change to game directory before launching (required for proper game operation)
|
||||||
pushd "$(winepath "${game[1]%\\*}")" > /dev/null || exit 1
|
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
|
if [[ "$debugGdb" == "1" ]]; then
|
||||||
winedbg --gdb "${game[1]##*\\}"
|
run_with_optional_gamemode winedbg --gdb "${game[1]##*\\}"
|
||||||
else
|
else
|
||||||
# Use direct wine execution instead of 'wine start' to ensure clipboard works
|
# Use direct wine execution instead of 'wine start' to ensure clipboard works
|
||||||
# See: https://bugs.winehq.org/show_bug.cgi?id=50598
|
# See: https://bugs.winehq.org/show_bug.cgi?id=50598
|
||||||
wine "${game[1]##*\\}"
|
run_with_optional_gamemode wine "${game[1]##*\\}"
|
||||||
fi
|
fi
|
||||||
popd > /dev/null || exit 1
|
popd > /dev/null || exit 1
|
||||||
restore_executioners_rage_focus_workaround
|
restore_executioners_rage_focus_workaround
|
||||||
@@ -927,6 +957,8 @@ export ipfsGateway="${ipfsGateway:-https://ipfs.stormux.org}"
|
|||||||
# Source helper functions
|
# Source helper functions
|
||||||
# shellcheck source=.includes/bottle.sh
|
# shellcheck source=.includes/bottle.sh
|
||||||
source "${scriptDir}/.includes/bottle.sh" # Also sourced in functions that need it
|
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
|
# shellcheck source=.includes/proton.sh
|
||||||
source "${scriptDir}/.includes/proton.sh"
|
source "${scriptDir}/.includes/proton.sh"
|
||||||
# shellcheck source=.includes/desktop.sh
|
# shellcheck source=.includes/desktop.sh
|
||||||
@@ -944,13 +976,14 @@ source "${scriptDir}/.includes/update.sh"
|
|||||||
export nvdaControllerClient32Dll="${ipfs[nvdaControllerClient32]}"
|
export nvdaControllerClient32Dll="${ipfs[nvdaControllerClient32]}"
|
||||||
export nvdaControllerClient64Dll="${ipfs[nvdaControllerClient64]}"
|
export nvdaControllerClient64Dll="${ipfs[nvdaControllerClient64]}"
|
||||||
export nvda2speechdBinary="${ipfs[nvda2speechd]}"
|
export nvda2speechdBinary="${ipfs[nvda2speechd]}"
|
||||||
|
export wineSpeakInstaller="${ipfs[winespeak]}"
|
||||||
|
|
||||||
# Check minimum requirements
|
# Check minimum requirements
|
||||||
check_requirements || exit 1
|
check_requirements || exit 1
|
||||||
# Wine32 no longer needed - all games use wine64 with SAPI support via WINETRICKS_FORCE=1
|
# Wine32 no longer needed - all games use wine64 with SAPI support via WINETRICKS_FORCE=1
|
||||||
# check_wine32 # Disabled - wine32 eliminated 2025-12-06
|
# check_wine32 # Disabled - wine32 eliminated 2025-12-06
|
||||||
# Ensure wine bottles exist with dependencies
|
# Ensure wine bottles exist with dependencies
|
||||||
ensure_wine_bottles
|
ensure_wine_bottles || exit 1
|
||||||
# Check for updates
|
# Check for updates
|
||||||
update
|
update
|
||||||
# Get latest news if available
|
# Get latest news if available
|
||||||
@@ -981,6 +1014,7 @@ declare -A command=(
|
|||||||
[R]="Redownload. Removes old versions of packages from cache before installing."
|
[R]="Redownload. Removes old versions of packages from cache before installing."
|
||||||
[r]="Remove a game. This will delete all game data."
|
[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:]="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."
|
[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 voice to be installed, default is Bdl. Options are alan, bdl, clb, or slt."
|
||||||
[V:]="Select the default voice for a bottle."
|
[V:]="Select the default voice for a bottle."
|
||||||
@@ -989,6 +1023,15 @@ declare -A command=(
|
|||||||
# Convert the keys of the associative array to a format usable by getopts
|
# Convert the keys of the associative array to a format usable by getopts
|
||||||
args="${!command[*]}"
|
args="${!command[*]}"
|
||||||
args="${args//[[:space:]]/}"
|
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
|
while getopts "${args}" i ; do
|
||||||
case "$i" in
|
case "$i" in
|
||||||
c) "${scriptDir}/.includes/checkup.sh";;
|
c) "${scriptDir}/.includes/checkup.sh";;
|
||||||
@@ -1031,6 +1074,12 @@ while getopts "${args}" i ; do
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
defaultRate="${OPTARG}"
|
defaultRate="${OPTARG}"
|
||||||
;;
|
;;
|
||||||
|
s)
|
||||||
|
skipDllReplace="true"
|
||||||
|
if [[ $# -eq 1 ]]; then
|
||||||
|
game_launcher
|
||||||
|
fi
|
||||||
|
;;
|
||||||
t)
|
t)
|
||||||
gameCount=$(find "${scriptDir}/.install" -type f -iname "*.sh" | wc -l)
|
gameCount=$(find "${scriptDir}/.install" -type f -iname "*.sh" | wc -l)
|
||||||
agm_infobox "Linux Game Manager" "Linux Game Manager" "There are currently ${gameCount} games available."
|
agm_infobox "Linux Game Manager" "Linux Game Manager" "There are currently ${gameCount} games available."
|
||||||
|
|||||||
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.
|
# 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
|
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
|
if [[ $# -ne 2 ]]; then
|
||||||
echo "Usage: $0 \"application name\" \"file name\"."
|
echo "Usage: $0 \"application name\" \"file name\"."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# Wait for the application to start
|
||||||
while ! pgrep -f -u "$USER" "$1" &> /dev/null ; do
|
while ! is_app_running "$1"; do
|
||||||
sleep 0.05
|
sleep 0.05
|
||||||
done
|
done
|
||||||
|
|
||||||
fileName="${2,,}"
|
fileName="${2,,}"
|
||||||
fileName="${fileName//[[:space:]]/-}.sqlite"
|
fileName="${fileName//[[:space:]]/-}.sqlite"
|
||||||
translationFile="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager/${fileName}"
|
translationFile="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager/${fileName}"
|
||||||
@@ -44,26 +51,8 @@ CREATE INDEX translations_text_idx ON translations (text);
|
|||||||
EOF
|
EOF
|
||||||
fi
|
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
|
# 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
|
sleep 0.05
|
||||||
text="$(xclip -d "${DISPLAY:-:0}" -selection clipboard -o 2> /dev/null)"
|
text="$(xclip -d "${DISPLAY:-:0}" -selection clipboard -o 2> /dev/null)"
|
||||||
if [[ -f ~/.agmsilent ]]; then
|
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://en.wikipedia.org/wiki/Unicode_equivalence#Combining_and_precomposed_characters
|
||||||
# https://www.effectiveperlprogramming.com/2011/09/normalize-your-perl-source/
|
# 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
|
# Normalize different unicode space characters to the same space
|
||||||
# https://stackoverflow.com/a/43640405
|
# https://stackoverflow.com/a/43640405
|
||||||
alias normalizeSpaces="perl -CSDA -plE 's/[^\\S\\t]/ /g'"
|
normalize_spaces() {
|
||||||
alias normalizeUnicode="normalizeSpaces | nfc"
|
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
|
# Normalize text
|
||||||
normalizedText="$(echo "$text" | normalizeUnicode)"
|
normalizedText="$(echo "$text" | normalize_unicode)"
|
||||||
|
|
||||||
# Create a temporary database for import
|
# Create a temporary database for import
|
||||||
sqlite3 "$translationFile" "CREATE TABLE IF NOT EXISTS temp_import(text TEXT, translation TEXT);"
|
sqlite3 "$translationFile" "CREATE TABLE IF NOT EXISTS temp_import(text TEXT, translation TEXT);"
|
||||||
|
|
||||||
# Check if we already have a translation
|
# Check if we already have a translation
|
||||||
translated=$(sqlite3 "$translationFile" "SELECT translation FROM translations WHERE text = '$normalizedText' LIMIT 1;" 2>/dev/null)
|
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
|
if [[ -z "$translated" ]]; then
|
||||||
echo "DEBUG: No cached translation, calling trans command" >&2
|
|
||||||
# Get translation from the trans utility
|
# Get translation from the trans utility
|
||||||
translated="$(trans -no-autocorrect -no-warn -brief "$normalizedText" | head -1 | sed 's/\s*$//' | normalizeUnicode)"
|
translated="$(trans -no-autocorrect -no-warn -brief "$normalizedText" | head -1 | sed 's/\s*$//' | normalize_unicode)"
|
||||||
echo "DEBUG: trans returned: '$translated'" >&2
|
|
||||||
|
|
||||||
if [[ -n "$translated" ]]; then
|
if [[ -n "$translated" ]]; then
|
||||||
# Insert using echo piping to avoid escaping issues
|
# Insert using echo piping to avoid escaping issues
|
||||||
echo "$normalizedText|$translated" | sqlite3 -separator "|" "$translationFile" ".import /dev/stdin temp_import"
|
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;"
|
sqlite3 "$translationFile" "INSERT OR IGNORE INTO translations SELECT * FROM temp_import; DELETE FROM temp_import;"
|
||||||
echo "DEBUG: Saved to database" >&2
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If we got a translation, speak it
|
# If we got a translation, speak it
|
||||||
if [[ -n "$translated" ]]; then
|
if [[ -n "$translated" ]]; then
|
||||||
echo "DEBUG: Speaking translation: '$translated'" >&2
|
|
||||||
spd-say -- "$translated"
|
spd-say -- "$translated"
|
||||||
else
|
else
|
||||||
echo "DEBUG: No translation available, speaking original: '$text'" >&2
|
spd-say -- "$normalizedText"
|
||||||
spd-say -- "$text"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clear clipboard
|
# Clear clipboard
|
||||||
echo "" | xclip -d "${DISPLAY:-:0}" -selection clipboard 2> /dev/null
|
echo "" | xclip -d "${DISPLAY:-:0}" -selection clipboard 2> /dev/null
|
||||||
done
|
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
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
repoRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
testRoot="$(mktemp -d)"
|
|
||||||
trap 'rm -rf "$testRoot"' EXIT
|
|
||||||
|
|
||||||
export HOME="${testRoot}/home"
|
|
||||||
export XDG_DATA_HOME="${HOME}/.local/share"
|
|
||||||
export XDG_CONFIG_HOME="${HOME}/.config"
|
|
||||||
export XDG_CACHE_HOME="${HOME}/.cache"
|
|
||||||
export DISPLAY=""
|
|
||||||
export cache="${XDG_CACHE_HOME}/audiogame-manager"
|
|
||||||
export configFile="${XDG_CONFIG_HOME}/storm-games/audiogame-manager/games.conf"
|
|
||||||
export game="Shadow Line"
|
|
||||||
export scriptDir="$repoRoot"
|
|
||||||
mkdir -p "$cache" "${configFile%/*}" "${testRoot}/bin"
|
|
||||||
touch "$configFile"
|
|
||||||
|
|
||||||
cat > "${testRoot}/bin/umu-run" <<'STUB'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
printf '%s|%s|%s|%s\n' "$WINEPREFIX" "$GAMEID" "${STORE:-}" "$*" >> "$UMU_STUB_LOG"
|
|
||||||
if [[ "${1:-}" == "" ]]; then
|
|
||||||
mkdir -p "$WINEPREFIX/drive_c"
|
|
||||||
fi
|
|
||||||
STUB
|
|
||||||
chmod +x "${testRoot}/bin/umu-run"
|
|
||||||
|
|
||||||
cat > "${testRoot}/bin/wine" <<'STUB'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
if [[ "${1:-}" == "winepath" || "${1:-}" == "winepath.exe" ]]; then
|
|
||||||
shift
|
|
||||||
fi
|
|
||||||
if [[ "${1:-}" == "-u" ]]; then
|
|
||||||
input="$2"
|
|
||||||
path="${input#c:\\}"
|
|
||||||
path="${path//\\//}"
|
|
||||||
printf '%s/drive_c/%s\n' "$WINEPREFIX" "$path"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
printf 'wine %s\n' "$*" >> "$WINE_STUB_LOG"
|
|
||||||
STUB
|
|
||||||
chmod +x "${testRoot}/bin/wine"
|
|
||||||
|
|
||||||
cat > "${testRoot}/bin/wineserver" <<'STUB'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
printf 'wineserver %s\n' "$*" >> "$WINE_STUB_LOG"
|
|
||||||
STUB
|
|
||||||
chmod +x "${testRoot}/bin/wineserver"
|
|
||||||
|
|
||||||
export PATH="${testRoot}/bin:$PATH"
|
|
||||||
export UMU_STUB_LOG="${testRoot}/umu.log"
|
|
||||||
export WINE_STUB_LOG="${testRoot}/wine.log"
|
|
||||||
|
|
||||||
# shellcheck source=.includes/proton.sh
|
|
||||||
source "${repoRoot}/.includes/proton.sh"
|
|
||||||
|
|
||||||
assert_equals() {
|
|
||||||
local expected="$1"
|
|
||||||
local actual="$2"
|
|
||||||
local message="$3"
|
|
||||||
if [[ "$expected" != "$actual" ]]; then
|
|
||||||
printf 'FAIL: %s\nexpected: %s\nactual: %s\n' "$message" "$expected" "$actual" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_file_contains() {
|
|
||||||
local file="$1"
|
|
||||||
local pattern="$2"
|
|
||||||
local message="$3"
|
|
||||||
if ! grep -Fq "$pattern" "$file"; then
|
|
||||||
printf 'FAIL: %s\nmissing pattern: %s\nfile contents:\n' "$message" "$pattern" >&2
|
|
||||||
cat "$file" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
test_get_umu_bottle_sets_environment() {
|
|
||||||
get_umu_bottle "shadow-line"
|
|
||||||
assert_equals "${XDG_DATA_HOME}/audiogame-manager/protonBottles/shadow-line" "$WINEPREFIX" "WINEPREFIX points at AGM proton bottle"
|
|
||||||
assert_equals "shadow-line" "$GAMEID" "GAMEID is exported"
|
|
||||||
assert_equals "none" "$STORE" "STORE defaults to none"
|
|
||||||
assert_equals ":0" "$DISPLAY" "DISPLAY defaults to :0"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_add_umu_launcher_records_backend_and_game_id() {
|
|
||||||
get_umu_bottle "shadow-line"
|
|
||||||
add_umu_launcher "shadow-line" 'c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe'
|
|
||||||
assert_file_contains "$configFile" 'umu|c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe|Shadow Line|export umuGameId=shadow-line' "UMU launcher entry is recorded"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_run_umu_game_uses_converted_path() {
|
|
||||||
get_umu_bottle "shadow-line"
|
|
||||||
mkdir -p "${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice"
|
|
||||||
touch "${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice/play_sr.exe"
|
|
||||||
run_umu_game 'c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe'
|
|
||||||
assert_file_contains "$UMU_STUB_LOG" "${WINEPREFIX}|shadow-line|none|${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice/play_sr.exe" "UMU launches converted exe path"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_install_crlf_file_normalizes_line_endings() {
|
|
||||||
local sourceFile="${testRoot}/language_en.dat"
|
|
||||||
local destFile="${testRoot}/installed/language_en.dat"
|
|
||||||
local expectedFile="${testRoot}/expected-language_en.dat"
|
|
||||||
printf 'line one\nline two\n' > "$sourceFile"
|
|
||||||
printf 'line one\r\nline two\r\n' > "$expectedFile"
|
|
||||||
|
|
||||||
install_crlf_file "$sourceFile" "$destFile"
|
|
||||||
|
|
||||||
if ! cmp -s "$expectedFile" "$destFile"; then
|
|
||||||
printf 'FAIL: Translation file is installed with CRLF line endings\n' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
test_get_umu_bottle_sets_environment
|
|
||||||
test_add_umu_launcher_records_backend_and_game_id
|
|
||||||
test_run_umu_game_uses_converted_path
|
|
||||||
test_install_crlf_file_normalizes_line_endings
|
|
||||||
printf 'UMU backend tests passed\n'
|
|
||||||
@@ -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