Tekken 3 updated. Should be self-updating from now on because it uses github releases.
This commit is contained in:
1 parent
7709ec83d8
commit
8e33ef63d0
6 files changed
+125
-186
No files matched your search
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
|
||||||
Reference in new issue
Block a user