Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/update-fexcore.yml
Original file line number Diff line number Diff line change
@@ -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}')"
Binary file added app/src/main/assets/fexcore/fexcore-2604.tzst
Binary file not shown.
1 change: 1 addition & 0 deletions app/src/main/res/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
<item>2512</item>
<item>2601</item>
<item>2603</item>
<item>2604</item>
</string-array>
<string-array name="dinput_mapper_type_entries">
<item>Standard (Old Gamepads)</item>
Expand Down
121 changes: 121 additions & 0 deletions tools/check-latest-fexcore.py
Original file line number Diff line number Diff line change
@@ -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 <github_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"<item>{version}</item>" 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()
54 changes: 54 additions & 0 deletions tools/convert-wcp-to-tzst.sh
Original file line number Diff line number Diff line change
@@ -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 <input.wcp> <output.tzst>
# 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 <input.wcp> <output.tzst>"
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"
46 changes: 46 additions & 0 deletions tools/update-arrays-xml.py
Original file line number Diff line number Diff line change
@@ -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 <arrays.xml path> <version>
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]} <arrays.xml> <version>", 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"<item>{version}</item>" in content:
print(f"Version {version} already present in arrays.xml — nothing to do.")
sys.exit(0)

# Find the last </item> in the fexcore_version_entries block and insert after it.
pattern = r'(name="fexcore_version_entries".*?</item>)(\s*</string-array>)'
replacement = r'\g<1>\n <item>' + version + r'</item>\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 <item>{version}</item> to fexcore_version_entries.")


if __name__ == "__main__":
main()