diff --git a/.github/workflows/update-fexcore.yml b/.github/workflows/update-fexcore.yml new file mode 100644 index 0000000000..71dcfc3686 --- /dev/null +++ b/.github/workflows/update-fexcore.yml @@ -0,0 +1,71 @@ +name: Update FEXCore + +on: + schedule: + - cron: '0 6 * * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-fexcore: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install zstd + run: sudo apt-get install -y zstd + + - name: Find latest FEXCore .wcp + id: check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python3 tools/check-latest-fexcore.py + + - name: Download ${{ steps.check.outputs.LATEST_FILE }} + if: steps.check.outputs.ALREADY_EXISTS == 'false' + run: | + curl -fL -o latest.wcp \ + "https://raw.githubusercontent.com/StevenMXZ/Winlator-Contents/main/FEXCore/${{ steps.check.outputs.LATEST_FILE }}" + + - name: Convert .wcp → .tzst + if: steps.check.outputs.ALREADY_EXISTS == 'false' + run: | + chmod +x tools/convert-wcp-to-tzst.sh + ./tools/convert-wcp-to-tzst.sh latest.wcp "${{ steps.check.outputs.TZST_PATH }}" + + - name: Update arrays.xml + if: steps.check.outputs.ALREADY_EXISTS == 'false' + run: | + python3 tools/update-arrays-xml.py \ + app/src/main/res/values/arrays.xml \ + "${{ steps.check.outputs.VERSION }}" + + - name: Create PR branch, commit, and open PR + if: steps.check.outputs.ALREADY_EXISTS == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.check.outputs.VERSION }}" + BRANCH="fexcore/update-${VERSION}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git checkout -b "$BRANCH" + git add "${{ steps.check.outputs.TZST_PATH }}" + git add app/src/main/res/values/arrays.xml + git commit -m "chore: add FEXCore ${VERSION}" + git push origin "$BRANCH" + + gh pr create \ + --title "chore: add FEXCore ${VERSION}" \ + --body "Automated update from [StevenMXZ/Winlator-Contents](https://github.com/StevenMXZ/Winlator-Contents/tree/main/FEXCore). + + - Converted \`${{ steps.check.outputs.LATEST_FILE }}\` → \`${{ steps.check.outputs.TZST_PATH }}\` + - Added \`${VERSION}\` to \`fexcore_version_entries\` in \`arrays.xml\`" \ + --head "$BRANCH" \ + --base "$(git remote show origin | awk '/HEAD branch/ {print $NF}')" diff --git a/app/src/main/assets/fexcore/fexcore-2604.tzst b/app/src/main/assets/fexcore/fexcore-2604.tzst new file mode 100644 index 0000000000..ea9fe600e6 Binary files /dev/null and b/app/src/main/assets/fexcore/fexcore-2604.tzst differ diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index c5339720c8..805c8ef12d 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -195,6 +195,7 @@ 2512 2601 2603 + 2604 Standard (Old Gamepads) diff --git a/tools/check-latest-fexcore.py b/tools/check-latest-fexcore.py new file mode 100644 index 0000000000..4b5919818c --- /dev/null +++ b/tools/check-latest-fexcore.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +check-latest-fexcore.py +Queries the StevenMXZ/Winlator-Contents FEXCore directory and prints the +latest .wcp filename (by YYMM version number). + +Usage: + python3 tools/check-latest-fexcore.py [--token ] + +A token is optional but recommended to avoid the 60 req/hr anonymous rate limit. +It can also be supplied via the GITHUB_TOKEN environment variable. +""" + +import argparse +import json +import os +import re +import subprocess +import sys + +API_URL = "https://api.github.com/repos/StevenMXZ/Winlator-Contents/contents/FEXCore" + + +def fetch_listing(token: str | None) -> list[dict]: + cmd = [ + "curl", "-sf", + "-H", "Accept: application/vnd.github.v3+json", + ] + if token: + cmd += ["-H", f"Authorization: Bearer {token}"] + cmd.append(API_URL) + + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"curl error: {result.stderr.strip()}", file=sys.stderr) + sys.exit(1) + return json.loads(result.stdout) + + +def pick_latest(entries: list[dict]) -> tuple[str | None, tuple[int, int]]: + best_name = None + best_ver: tuple[int, int] = (-1, -1) + for entry in entries: + if entry.get("type") != "file": + continue + name = entry["name"] + if not name.endswith(".wcp"): + continue + m = re.match(r"^(\d{4})(?:\.(\d+))?\.wcp$", name) + if m: + ver = (int(m.group(1)), int(m.group(2) or 0)) + if ver > best_ver: + best_ver = ver + best_name = name + return best_name, best_ver + + +def main() -> None: + parser = argparse.ArgumentParser(description="Find the latest FEXCore .wcp release.") + parser.add_argument("--token", default=os.environ.get("GITHUB_TOKEN"), help="GitHub token (or set GITHUB_TOKEN)") + parser.add_argument( + "--gha-output", + metavar="FILE", + default=os.environ.get("GITHUB_OUTPUT"), + help="Append key=value pairs to this file (GitHub Actions $GITHUB_OUTPUT). " + "Automatically set when GITHUB_OUTPUT env var is present.", + ) + args = parser.parse_args() + + if not args.gha_output: + # Human-readable mode + print(f"Querying {API_URL} ...") + + entries = fetch_listing(args.token) + + all_wcp = [e["name"] for e in entries if e.get("type") == "file" and e["name"].endswith(".wcp")] + + latest, ver = pick_latest(entries) + if not latest: + print("ERROR: Could not determine latest versioned .wcp file.", file=sys.stderr) + sys.exit(1) + + version = latest[:4] # first four digits = YYMM + tzst_path = f"app/src/main/assets/fexcore/fexcore-{version}.tzst" + download_url = f"https://raw.githubusercontent.com/StevenMXZ/Winlator-Contents/main/FEXCore/{latest}" + + # Use arrays.xml as the source of truth — it only contains a version once the + # full workflow has completed and committed, unlike the .tzst which can exist + # locally from test runs (e.g. act --bind). + arrays_xml = "app/src/main/res/values/arrays.xml" + try: + with open(arrays_xml, "r", encoding="utf-8") as f: + already_exists = f"{version}" in f.read() + except FileNotFoundError: + already_exists = False + + if args.gha_output: + # GitHub Actions mode: write outputs, print minimal log to stdout + with open(args.gha_output, "a", encoding="utf-8") as f: + f.write(f"LATEST_FILE={latest}\n") + f.write(f"VERSION={version}\n") + f.write(f"TZST_PATH={tzst_path}\n") + f.write(f"ALREADY_EXISTS={'true' if already_exists else 'false'}\n") + if already_exists: + print(f"fexcore-{version}.tzst already present — nothing to do.") + else: + print(f"New release found: {latest} → {tzst_path}") + else: + # Human-readable mode + print(f"\nAll .wcp files found ({len(all_wcp)}):") + for name in sorted(all_wcp): + print(f" {name}") + print(f"\nLatest : {latest} (parsed version {ver[0]}.{ver[1]})") + print(f"VERSION: {version}") + print(f"Output : {tzst_path}") + print(f"Already exists: {already_exists}") + print(f"Download URL: {download_url}") + + +if __name__ == "__main__": + main() diff --git a/tools/convert-wcp-to-tzst.sh b/tools/convert-wcp-to-tzst.sh new file mode 100755 index 0000000000..541fdc527a --- /dev/null +++ b/tools/convert-wcp-to-tzst.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# convert-wcp-to-tzst.sh +# Converts a FEX .wcp release (XZ-compressed tar) into a fexcore .tzst file +# compatible with the GameNative app assets format. +# +# Usage: ./tools/convert-wcp-to-tzst.sh +# Example: ./tools/convert-wcp-to-tzst.sh FEX-2603.wcp app/src/main/assets/fexcore/fexcore-2603.tzst + +set -euo pipefail + +INPUT="${1:-}" +OUTPUT="${2:-}" + +if [[ -z "$INPUT" || -z "$OUTPUT" ]]; then + echo "Usage: $0 " + exit 1 +fi + +if [[ ! -f "$INPUT" ]]; then + echo "Error: input file '$INPUT' not found" + exit 1 +fi + +# Ensure output directory exists +mkdir -p "$(dirname "$OUTPUT")" + +echo "Converting '$INPUT' -> '$OUTPUT' ..." + +# Decompress XZ, extract only the two DLLs from system32/, strip the +# system32/ path component, then repack as a zstd-compressed tar. +# +# The reference format (fexcore-2601.tzst) contains: +# ./libwow64fex.dll +# ./libarm64ecfex.dll +# The .wcp source contains them under system32/, so we use --strip-components=1. + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +echo " Extracting DLLs from archive..." +xz -dc "$INPUT" \ + | tar -x \ + --strip-components=1 \ + -C "$TMPDIR" \ + --wildcards \ + '*/libwow64fex.dll' \ + '*/libarm64ecfex.dll' + +echo " Repacking as zstd tar..." +# Use compression level 19 for smallest output (matches prior releases in size range). +tar -c -C "$TMPDIR" . \ + | zstd -19 -o "$OUTPUT" --force + +echo "Done: $(du -sh "$OUTPUT" | cut -f1) $OUTPUT" diff --git a/tools/update-arrays-xml.py b/tools/update-arrays-xml.py new file mode 100644 index 0000000000..7a797c2979 --- /dev/null +++ b/tools/update-arrays-xml.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +""" +update-arrays-xml.py +Appends a new version entry to the fexcore_version_entries array in arrays.xml. + +Usage: + python3 tools/update-arrays-xml.py + e.g. python3 tools/update-arrays-xml.py app/src/main/res/values/arrays.xml 2604 +""" + +import re +import sys + + +def main() -> None: + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + path, version = sys.argv[1], sys.argv[2] + + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + # Idempotency check + if f"{version}" in content: + print(f"Version {version} already present in arrays.xml — nothing to do.") + sys.exit(0) + + # Find the last in the fexcore_version_entries block and insert after it. + pattern = r'(name="fexcore_version_entries".*?)(\s*)' + replacement = r'\g<1>\n ' + version + r'\g<2>' + new_content, count = re.subn(pattern, replacement, content, count=1, flags=re.DOTALL) + + if count == 0: + print("ERROR: fexcore_version_entries array not found in arrays.xml", file=sys.stderr) + sys.exit(1) + + with open(path, "w", encoding="utf-8") as f: + f.write(new_content) + + print(f"Appended {version} to fexcore_version_entries.") + + +if __name__ == "__main__": + main()