Tekken 3 update script added.
This commit is contained in:
1 parent
4886578fc5
commit
d5b688314c
5 files changed
+169
-16
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 ships this exact Prism 0.18.3 DLL. Refuse an unverified update.
|
# Access 1.0 and 1.0.1 ship this exact Prism 0.18.3 DLL.
|
||||||
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
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ fi
|
|||||||
# Keep the base executable paired with the accessibility mod tested against it.
|
# Keep the base executable paired with the accessibility mod tested against it.
|
||||||
tekkenPortCommit="b6e941378ad03dbd834fef340bd8d90bae6e430b"
|
tekkenPortCommit="b6e941378ad03dbd834fef340bd8d90bae6e430b"
|
||||||
download "https://github.com/Muqaddas12/Tekken3-PC/archive/${tekkenPortCommit}.zip" \
|
download "https://github.com/Muqaddas12/Tekken3-PC/archive/${tekkenPortCommit}.zip" \
|
||||||
"https://github.com/BlindChallengerX/Tekken3-Access/releases/download/v1.0/Tekken3Access-1.0.zip"
|
"https://github.com/BlindChallengerX/Tekken3-Access/releases/download/v1.0.1/Tekken3Access-1.0.1.zip"
|
||||||
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.zip" \
|
"$tekkenDiscDir" "${cache}/${tekkenPortCommit}.zip" "${cache}/Tekken3Access-1.0.1.zip" \
|
||||||
"${WINEPREFIX}/drive_c/Program Files/Tekken 3 Access" 2>&1)"; then
|
"${WINEPREFIX}/drive_c/Program Files/Tekken 3 Access" 2>&1)"; then
|
||||||
alert "Tekken 3 installation" "$tekkenSetupOutput"
|
alert "Tekken 3 installation" "$tekkenSetupOutput"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ 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, verifies the mod
|
`b6e941378ad03dbd834fef340bd8d90bae6e430b` and Access 1.0.1, verifies the mod
|
||||||
archive and the port executable/extractor, and uses native Linux Python 3 to
|
archive and the port executable/extractor, and uses native Linux Python 3 to
|
||||||
extract game data. Allow approximately 1 GB for installed files, in addition to
|
extract game data. Allow approximately 1 GB for installed files, in addition to
|
||||||
your original dump. Python 3 is an additional installer dependency.
|
your original dump. Python 3 is an additional installer dependency.
|
||||||
@@ -108,6 +108,23 @@ controller; the mod's function keys still work. To play with the keyboard, set
|
|||||||
Enter for Start, arrows to move, and Z/A/X/S for Square/Triangle/Cross/Circle.
|
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.
|
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 installs the verified Access 1.0.1 release 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 backup path. Future releases need a compatibility
|
||||||
|
check before the pinned update is changed.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
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,
|
||||||
@@ -115,6 +132,10 @@ pause/resume, controller input if available, and saving/relaunching. Also check
|
|||||||
Arcadelux speech if modifying the shared Prism helper. Logs are
|
Arcadelux speech if modifying the shared Prism helper. Logs are
|
||||||
`T3Access/T3Access.log` and `T3Access/launch.log` in the game folder; copy them
|
`T3Access/T3Access.log` and `T3Access/launch.log` in the game folder; copy them
|
||||||
before another launch overwrites 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.
|
||||||
|
|
||||||
## AI-assisted contributions
|
## AI-assisted contributions
|
||||||
|
|
||||||
|
|||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/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 ! download "https://github.com/BlindChallengerX/Tekken3-Access/releases/download/v1.0.1/Tekken3Access-1.0.1.zip"; then
|
||||||
|
alert "Tekken 3 Update" "Could not download Tekken 3 Access 1.0.1."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! updateOutput="$(python3 "${scriptDir}/game-scripts/tekken3_setup.py" update \
|
||||||
|
"${cache}/Tekken3Access-1.0.1.zip" "$gamePath" 2>&1)"; then
|
||||||
|
alert "Tekken 3 Update" "$updateOutput"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
agm_msgbox "Tekken 3 Update" "Tekken 3 Update" "$updateOutput"
|
||||||
+109
-12
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -16,7 +17,8 @@ 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 = "bacaf33cda0ba615bffe4082a9b230cdb9d9f63327f78baf76dec9098381a1d6"
|
MOD_ARCHIVE_HASH = "7aacee1ce376dff83823a6fc9c6717ad31e94cd37fa85551a3a8e8048e709646"
|
||||||
|
PRISM_HASH = "7c7d09c8c7306e8e1a0c46d603ccb2a391c194c77843400c11b746e7dd56a467"
|
||||||
USA_EXE_HASH = "fbda8b68e5799dbef4af39a161783bc670c15b0aa0e87dce65e210717da19b8c"
|
USA_EXE_HASH = "fbda8b68e5799dbef4af39a161783bc670c15b0aa0e87dce65e210717da19b8c"
|
||||||
TRACK_SIZES = (632532768, 27701856, 28042896)
|
TRACK_SIZES = (632532768, 27701856, 28042896)
|
||||||
TRACK_NAMES = tuple(f"Tekken 3 (USA) (Track {trackNumber}).bin" for trackNumber in range(1, 4))
|
TRACK_NAMES = tuple(f"Tekken 3 (USA) (Track {trackNumber}).bin" for trackNumber in range(1, 4))
|
||||||
@@ -74,6 +76,102 @@ def verify_hash(path, 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 tested version. Remove the cached download and retry.")
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
validate_installation(destination)
|
||||||
|
verify_hash(modArchive, MOD_ARCHIVE_HASH)
|
||||||
|
with tempfile.TemporaryDirectory(prefix=".tekken3-update-", dir=destination.parent) as temporaryDir:
|
||||||
|
gameRoot = Path(temporaryDir) / "mod"
|
||||||
|
gameRoot.mkdir()
|
||||||
|
extract_archive(modArchive, gameRoot)
|
||||||
|
for name in ("Play Tekken 3 Access.exe", "T3Access/T3Access.dll", "T3Access/README.txt"):
|
||||||
|
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)
|
||||||
|
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 1.0.1. Backup of replaced files: {backupRoot}")
|
||||||
|
|
||||||
|
|
||||||
def install_game(discDir, portArchive, modArchive, destination):
|
def install_game(discDir, portArchive, modArchive, destination):
|
||||||
validate_disc(discDir)
|
validate_disc(discDir)
|
||||||
verify_hash(modArchive, MOD_ARCHIVE_HASH)
|
verify_hash(modArchive, MOD_ARCHIVE_HASH)
|
||||||
@@ -94,17 +192,7 @@ def install_game(discDir, portArchive, modArchive, destination):
|
|||||||
settingsPath = gameRoot / "settings.toml"
|
settingsPath = gameRoot / "settings.toml"
|
||||||
settingsText = settingsPath.read_text()
|
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"'))
|
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"'))
|
||||||
manualText = (
|
write_manual(gameRoot)
|
||||||
"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 (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())
|
|
||||||
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)
|
||||||
@@ -116,6 +204,11 @@ def main():
|
|||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
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.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")
|
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)
|
||||||
@@ -124,6 +217,10 @@ def main():
|
|||||||
if args.command == "validate-disc":
|
if 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":
|
||||||
|
validate_installation(args.destination)
|
||||||
|
elif args.command == "update":
|
||||||
|
update_game(args.modArchive, args.destination)
|
||||||
else:
|
else:
|
||||||
install_game(args.discDir, args.portArchive, args.modArchive, args.destination)
|
install_game(args.discDir, args.portArchive, args.modArchive, args.destination)
|
||||||
except (OSError, ValueError, BadZipFile, subprocess.CalledProcessError) as error:
|
except (OSError, ValueError, BadZipFile, subprocess.CalledProcessError) as error:
|
||||||
|
|||||||
Reference in new issue
Block a user