Tekken 3 updated. Should be self-updating from now on because it uses github releases.

This commit is contained in:
Storm Dragon committed 2026-10-19 01:16:41 -04:00
1 parent 7709ec83d8
commit 8e33ef63d0
6 files changed
+125 -186

No files matched your search

+1 -1
View File
@@ -86,7 +86,7 @@ prepare_prism_bridge() {
prepare_tekken3_prism_bridge() { prepare_tekken3_prism_bridge() {
local gameDir="$1" local gameDir="$1"
local prismHash local prismHash
# Access 1.0 and 1.0.1 ship this exact Prism 0.18.3 DLL. # Only this Prism 0.18.3 DLL is supported by the Tekken integration.
prismHash="$(sha256sum "${gameDir}/T3Access/prism.dll")" || return 1 prismHash="$(sha256sum "${gameDir}/T3Access/prism.dll")" || return 1
if [[ "${prismHash%% *}" != "7c7d09c8c7306e8e1a0c46d603ccb2a391c194c77843400c11b746e7dd56a467" ]]; then if [[ "${prismHash%% *}" != "7c7d09c8c7306e8e1a0c46d603ccb2a391c194c77843400c11b746e7dd56a467" ]]; then
echo "Tekken 3 Access has an unrecognized Prism DLL; update its AGM integration before launching." >&2 echo "Tekken 3 Access has an unrecognized Prism DLL; update its AGM integration before launching." >&2
+13 -5
View File
@@ -23,14 +23,22 @@ if ! tekkenSetupOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" v
exit 1 exit 1
fi fi
# Keep the base executable paired with the accessibility mod tested against it. # Keep the base executable pinned while resolving the latest stable access mod.
tekkenPortCommit="b6e941378ad03dbd834fef340bd8d90bae6e430b" tekkenPortCommit="b6e941378ad03dbd834fef340bd8d90bae6e430b"
download "https://github.com/Muqaddas12/Tekken3-PC/archive/${tekkenPortCommit}.zip" \ if ! tekkenSetupOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" latest-release 2>&1)"; then
"https://github.com/BlindChallengerX/Tekken3-Access/releases/download/v1.0.1/Tekken3Access-1.0.1.zip" 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 install_wine_bottle
if ! tekkenSetupOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" install \ if ! tekkenSetupOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" install \
"$tekkenDiscDir" "${cache}/${tekkenPortCommit}.zip" "${cache}/Tekken3Access-1.0.1.zip" \ "$tekkenDiscDir" "${cache}/${tekkenPortCommit}.zip" "${cache}/${tekkenModUrl##*/}" \
"${WINEPREFIX}/drive_c/Program Files/Tekken 3 Access" 2>&1)"; then "${WINEPREFIX}/drive_c/Program Files/Tekken 3 Access" \
--mod-sha256 "$tekkenModHash" --mod-version "$tekkenModVersion" 2>&1)"; then
alert "Tekken 3 installation" "$tekkenSetupOutput" alert "Tekken 3 installation" "$tekkenSetupOutput"
exit 1 exit 1
fi fi
+29 -11
View File
@@ -99,16 +99,19 @@ downloads. The disc files are read without modification, outside AGM's download
cache. After installation, the extracted game data is sufficient for play. cache. After installation, the extracted game data is sufficient for play.
The installer downloads the tested PC-port commit The installer downloads the tested PC-port commit
`b6e941378ad03dbd834fef340bd8d90bae6e430b` and Access 1.0.1, verifies the mod `b6e941378ad03dbd834fef340bd8d90bae6e430b` and the latest stable Access release
archive and the port executable/extractor, and uses native Linux Python 3 to from GitHub. It verifies the mod archive against GitHub's published SHA-256
extract game data. Allow approximately 1 GB for installed files, in addition to checksum, checks the supported mod layout and Prism DLL, verifies the pinned
your original dump. Python 3 is an additional installer dependency. 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, 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 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. Versions cached for reuse. No NVDA replacement DLL or Windows Python is needed. The base
are pinned because the access mod hooks the port executable; upstream updates port stays pinned because the access mod hooks its executable. Mod releases are
need a compatibility check before changing the pins. 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/`. 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 Select **Tekken 3** in AGM to launch the access executable. It starts in a
@@ -128,17 +131,25 @@ To update an existing AGM installation, close Tekken 3 and run from this checkou
bash game-scripts/tekken3-update.sh bash game-scripts/tekken3-update.sh
``` ```
This installs the verified Access 1.0.1 release without the original disc dump This resolves the latest stable Access release on each run, without the original
or another game-data extraction. It checks the installed port executable, disc dump or another game-data extraction. It checks the installed port executable,
preserves settings, controller selection, memory cards, logs and mod preferences, preserves settings, controller selection, memory cards, logs and mod preferences,
and refreshes the manual. Replaced files are backed up beside the game folder; and refreshes the manual. Replaced files are backed up beside the game folder;
the completion message gives the backup path. Future releases need a compatibility the completion message gives the version and backup path. GitHub API failures,
check before the pinned update is changed. 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 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 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. 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 Before submitting changes, test installation with missing or incorrect disc
files and with a valid USA dump. In the installed game, check the loaded 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, announcement, menu and character selection, F1 distance/F2 health, fight sounds,
@@ -150,6 +161,13 @@ After updating, check both hum-volume shortcuts, level zero, and persistence
after restarting. Check sound suppression and resumption in a normal fight, after restarting. Check sound suppression and resumption in a normal fight,
Tekken Force and Tekken Ball, including the pause command list. Confirm your Tekken Force and Tekken Ball, including the pause command list. Confirm your
controller/keyboard selection and existing memory-card saves still work. 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
+8 -3
View File
@@ -23,12 +23,17 @@ if ! mkdir -p "$cache"; then
alert "Tekken 3 Update" "Could not create the download cache: ${cache}" alert "Tekken 3 Update" "Could not create the download cache: ${cache}"
exit 1 exit 1
fi fi
if ! download "https://github.com/BlindChallengerX/Tekken3-Access/releases/download/v1.0.1/Tekken3Access-1.0.1.zip"; then if ! updateOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" latest-release 2>&1)"; then
alert "Tekken 3 Update" "Could not download Tekken 3 Access 1.0.1." 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 exit 1
fi fi
if ! updateOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" update \ if ! updateOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" update \
"${cache}/Tekken3Access-1.0.1.zip" "$gamePath" 2>&1)"; then "${cache}/${modUrl##*/}" "$gamePath" --mod-sha256 "$modHash" --mod-version "$modVersion" 2>&1)"; then
alert "Tekken 3 Update" "$updateOutput" alert "Tekken 3 Update" "$updateOutput"
exit 1 exit 1
fi fi
+74 -16
View File
@@ -3,6 +3,7 @@
import argparse import argparse
import hashlib import hashlib
import json
import os import os
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
import re import re
@@ -11,13 +12,14 @@ import stat
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from urllib.request import Request, urlopen
from zipfile import BadZipFile, ZipFile from zipfile import BadZipFile, ZipFile
PORT_COMMIT = "b6e941378ad03dbd834fef340bd8d90bae6e430b" PORT_COMMIT = "b6e941378ad03dbd834fef340bd8d90bae6e430b"
PORT_EXE_HASH = "25d038f55d7a1c02a0e5bbcc97fe9b1738c8292198e7fa0d1b171b57129d18c0" PORT_EXE_HASH = "25d038f55d7a1c02a0e5bbcc97fe9b1738c8292198e7fa0d1b171b57129d18c0"
SETUP_HASH = "c1e0430498b2f4212d7e04187e3ba822d3ac273de487cd35d93aa2ef823ac5aa" SETUP_HASH = "c1e0430498b2f4212d7e04187e3ba822d3ac273de487cd35d93aa2ef823ac5aa"
MOD_ARCHIVE_HASH = "7aacee1ce376dff83823a6fc9c6717ad31e94cd37fa85551a3a8e8048e709646" RELEASE_API = "https://api.github.com/repos/BlindChallengerX/Tekken3-Access/releases/latest"
PRISM_HASH = "7c7d09c8c7306e8e1a0c46d603ccb2a391c194c77843400c11b746e7dd56a467" PRISM_HASH = "7c7d09c8c7306e8e1a0c46d603ccb2a391c194c77843400c11b746e7dd56a467"
USA_EXE_HASH = "fbda8b68e5799dbef4af39a161783bc670c15b0aa0e87dce65e210717da19b8c" USA_EXE_HASH = "fbda8b68e5799dbef4af39a161783bc670c15b0aa0e87dce65e210717da19b8c"
TRACK_SIZES = (632532768, 27701856, 28042896) TRACK_SIZES = (632532768, 27701856, 28042896)
@@ -26,6 +28,36 @@ CUE_NAME = "Tekken 3 (USA).cue"
SECTOR_SIZE = 2352 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): def validate_disc(discDir):
missingFiles = [name for name in (CUE_NAME, *TRACK_NAMES) if not (discDir / name).is_file()] missingFiles = [name for name in (CUE_NAME, *TRACK_NAMES) if not (discDir / name).is_file()]
if missingFiles: if missingFiles:
@@ -73,7 +105,26 @@ def extract_archive(archivePath, destination, stripRoot=None):
def verify_hash(path, expectedHash): def verify_hash(path, expectedHash):
if hashlib.sha256(path.read_bytes()).hexdigest() != expectedHash: if hashlib.sha256(path.read_bytes()).hexdigest() != expectedHash:
raise ValueError(f"{path.name} does not match the tested version. Remove the cached download and retry.") 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): def write_manual(gameRoot):
@@ -115,17 +166,14 @@ def validate_installation(destination):
raise ValueError(f"The existing installation is missing {name}. Install Tekken 3 through AGM first.") raise ValueError(f"The existing installation is missing {name}. Install Tekken 3 through AGM first.")
def update_game(modArchive, destination): def update_game(modArchive, destination, modHash, modVersion):
validate_installation(destination) validate_installation(destination)
verify_hash(modArchive, MOD_ARCHIVE_HASH) verify_hash(modArchive, modHash)
with tempfile.TemporaryDirectory(prefix=".tekken3-update-", dir=destination.parent) as temporaryDir: with tempfile.TemporaryDirectory(prefix=".tekken3-update-", dir=destination.parent) as temporaryDir:
gameRoot = Path(temporaryDir) / "mod" gameRoot = Path(temporaryDir) / "mod"
gameRoot.mkdir() gameRoot.mkdir()
extract_archive(modArchive, gameRoot) extract_archive(modArchive, gameRoot)
for name in ("Play Tekken 3 Access.exe", "T3Access/T3Access.dll", "T3Access/README.txt"): validate_mod(gameRoot)
if not (gameRoot / name).is_file():
raise ValueError(f"The update is missing {name}.")
verify_hash(gameRoot / "T3Access/prism.dll", PRISM_HASH)
write_manual(gameRoot) write_manual(gameRoot)
relativeFiles = sorted(path.relative_to(gameRoot) for path in gameRoot.rglob("*") if path.is_file()) relativeFiles = sorted(path.relative_to(gameRoot) for path in gameRoot.rglob("*") if path.is_file())
for relativePath in relativeFiles: for relativePath in relativeFiles:
@@ -169,12 +217,12 @@ def update_game(modArchive, destination):
except OSError as restoreError: except OSError as restoreError:
raise OSError(f"Update and restoration failed. Restore files from {backupRoot}. {restoreError}") from error 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 raise OSError(f"Update failed; replaced files restored. Backup: {backupRoot}. {error}") from error
print(f"Updated Tekken 3 Access to 1.0.1. Backup of replaced files: {backupRoot}") print(f"Updated Tekken 3 Access to {modVersion}. Backup of replaced files: {backupRoot}")
def install_game(discDir, portArchive, modArchive, destination): def install_game(discDir, portArchive, modArchive, destination, modHash, modVersion):
validate_disc(discDir) validate_disc(discDir)
verify_hash(modArchive, MOD_ARCHIVE_HASH) verify_hash(modArchive, modHash)
if destination.exists() and any(destination.iterdir()): 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.") 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) destination.parent.mkdir(parents=True, exist_ok=True)
@@ -184,7 +232,11 @@ def install_game(discDir, portArchive, modArchive, destination):
extract_archive(portArchive, gameRoot, f"Tekken3-PC-{PORT_COMMIT}") extract_archive(portArchive, gameRoot, f"Tekken3-PC-{PORT_COMMIT}")
verify_hash(gameRoot / "Tekken3.exe", PORT_EXE_HASH) verify_hash(gameRoot / "Tekken3.exe", PORT_EXE_HASH)
verify_hash(gameRoot / "setup_game_data.py", SETUP_HASH) verify_hash(gameRoot / "setup_game_data.py", SETUP_HASH)
extract_archive(modArchive, gameRoot) 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) 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"): for requiredFile in ("Play Tekken 3 Access.exe", "T3Access/T3Access.dll", "T3Access/prism.dll", "game_data/disc.cue"):
if not (gameRoot / requiredFile).is_file(): if not (gameRoot / requiredFile).is_file():
@@ -196,12 +248,13 @@ def install_game(discDir, portArchive, modArchive, destination):
if destination.exists(): if destination.exists():
destination.rmdir() # Only an empty destination is accepted above. destination.rmdir() # Only an empty destination is accepted above.
gameRoot.rename(destination) gameRoot.rename(destination)
print(f"Installed Tekken 3 Access in {destination}") print(f"Installed Tekken 3 Access {modVersion} in {destination}")
def main(): def main():
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True) subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("latest-release")
validateParser = subparsers.add_parser("validate-disc") validateParser = subparsers.add_parser("validate-disc")
validateParser.add_argument("discDir", type=Path) validateParser.add_argument("discDir", type=Path)
installedParser = subparsers.add_parser("validate-installation") installedParser = subparsers.add_parser("validate-installation")
@@ -212,17 +265,22 @@ def main():
installParser = subparsers.add_parser("install") installParser = subparsers.add_parser("install")
for name in ("discDir", "portArchive", "modArchive", "destination"): for name in ("discDir", "portArchive", "modArchive", "destination"):
installParser.add_argument(name, type=Path) 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() args = parser.parse_args()
try: try:
if args.command == "validate-disc": if args.command == "latest-release":
print("\t".join(latest_release()))
elif args.command == "validate-disc":
validate_disc(args.discDir) validate_disc(args.discDir)
print("Tekken 3 (USA) disc files validated.") print("Tekken 3 (USA) disc files validated.")
elif args.command == "validate-installation": elif args.command == "validate-installation":
validate_installation(args.destination) validate_installation(args.destination)
elif args.command == "update": elif args.command == "update":
update_game(args.modArchive, args.destination) update_game(args.modArchive, args.destination, args.mod_sha256, args.mod_version)
else: else:
install_game(args.discDir, args.portArchive, args.modArchive, args.destination) install_game(args.discDir, args.portArchive, args.modArchive, args.destination, args.mod_sha256, args.mod_version)
except (OSError, ValueError, BadZipFile, subprocess.CalledProcessError) as error: except (OSError, ValueError, BadZipFile, subprocess.CalledProcessError) as error:
print(f"Tekken 3 setup failed: {error}", file=sys.stderr) print(f"Tekken 3 setup failed: {error}", file=sys.stderr)
return 1 return 1
-150
View File
@@ -1,150 +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
if [[ "${1:-}" == "winetricks" && "${3:-}" == "already-installed" ]]; then
printf "winetricks verb 'already-installed' is already installed\n" >&2
exit 1
fi
STUB
chmod +x "${testRoot}/bin/umu-run"
cat > "${testRoot}/bin/gamemoderun" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$GAMEMODE_STUB_LOG"
exec "$@"
STUB
chmod +x "${testRoot}/bin/gamemoderun"
cat > "${testRoot}/bin/wine" <<'STUB'
#!/usr/bin/env bash
if [[ "${1:-}" == "winepath" || "${1:-}" == "winepath.exe" ]]; then
shift
fi
if [[ "${1:-}" == "-u" ]]; then
input="$2"
path="${input#c:\\}"
path="${path//\\//}"
printf '%s/drive_c/%s\n' "$WINEPREFIX" "$path"
exit 0
fi
printf 'wine %s\n' "$*" >> "$WINE_STUB_LOG"
STUB
chmod +x "${testRoot}/bin/wine"
cat > "${testRoot}/bin/wineserver" <<'STUB'
#!/usr/bin/env bash
printf 'wineserver %s\n' "$*" >> "$WINE_STUB_LOG"
STUB
chmod +x "${testRoot}/bin/wineserver"
export PATH="${testRoot}/bin:$PATH"
export UMU_STUB_LOG="${testRoot}/umu.log"
export WINE_STUB_LOG="${testRoot}/wine.log"
export GAMEMODE_STUB_LOG="${testRoot}/gamemode.log"
# shellcheck source=.includes/gamemode.sh
source "${repoRoot}/.includes/gamemode.sh"
# shellcheck source=.includes/proton.sh
source "${repoRoot}/.includes/proton.sh"
assert_equals() {
local expected="$1"
local actual="$2"
local message="$3"
if [[ "$expected" != "$actual" ]]; then
printf 'FAIL: %s\nexpected: %s\nactual: %s\n' "$message" "$expected" "$actual" >&2
exit 1
fi
}
assert_file_contains() {
local file="$1"
local pattern="$2"
local message="$3"
if ! grep -Fq "$pattern" "$file"; then
printf 'FAIL: %s\nmissing pattern: %s\nfile contents:\n' "$message" "$pattern" >&2
cat "$file" >&2
exit 1
fi
}
test_get_umu_bottle_sets_environment() {
get_umu_bottle "shadow-line"
assert_equals "${XDG_DATA_HOME}/audiogame-manager/protonBottles/shadow-line" "$WINEPREFIX" "WINEPREFIX points at AGM proton bottle"
assert_equals "shadow-line" "$GAMEID" "GAMEID is exported"
assert_equals "none" "$STORE" "STORE defaults to none"
assert_equals ":0" "$DISPLAY" "DISPLAY defaults to :0"
}
test_add_umu_launcher_records_backend_and_game_id() {
get_umu_bottle "shadow-line"
add_umu_launcher "shadow-line" 'c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe'
assert_file_contains "$configFile" 'umu|c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe|Shadow Line|export umuGameId=shadow-line' "UMU launcher entry is recorded"
}
test_add_umu_launcher_records_extra_environment_flags() {
get_umu_bottle "entombed"
game="Entombed"
add_umu_launcher "entombed" 'c:\Program Files (x86)\Entombed\Entombed.exe' "export PROTON_USE_XALIA=0"
assert_file_contains "$configFile" 'umu|c:\Program Files (x86)\Entombed\Entombed.exe|Entombed|export umuGameId=entombed|export PROTON_USE_XALIA=0' "UMU launcher entry records extra environment flags"
}
test_run_umu_game_uses_converted_path() {
get_umu_bottle "shadow-line"
mkdir -p "${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice"
touch "${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice/play_sr.exe"
run_umu_game 'c:\Program Files (x86)\GalaxyLaboratory\ShadowRine_FullVoice\play_sr.exe'
assert_file_contains "$GAMEMODE_STUB_LOG" "umu-run ${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice/play_sr.exe" "UMU launches through GameMode"
assert_file_contains "$UMU_STUB_LOG" "${WINEPREFIX}|shadow-line|none|${WINEPREFIX}/drive_c/Program Files (x86)/GalaxyLaboratory/ShadowRine_FullVoice/play_sr.exe" "UMU launches converted exe path"
}
test_install_crlf_file_normalizes_line_endings() {
local sourceFile="${testRoot}/language_en.dat"
local destFile="${testRoot}/installed/language_en.dat"
local expectedFile="${testRoot}/expected-language_en.dat"
printf 'line one\nline two\n' > "$sourceFile"
printf 'line one\r\nline two\r\n' > "$expectedFile"
install_crlf_file "$sourceFile" "$destFile"
if ! cmp -s "$expectedFile" "$destFile"; then
printf 'FAIL: Translation file is installed with CRLF line endings\n' >&2
exit 1
fi
}
test_install_proton_winetricks_verb_ignores_already_installed() {
get_umu_bottle "shadow-line"
install_proton_winetricks_verb "already-installed"
}
test_get_umu_bottle_sets_environment
test_add_umu_launcher_records_backend_and_game_id
test_add_umu_launcher_records_extra_environment_flags
test_run_umu_game_uses_converted_path
test_install_crlf_file_normalizes_line_endings
test_install_proton_winetricks_verb_ignores_already_installed
printf 'UMU backend tests passed\n'