From 27371e86715f625059605402f9d62dd257dff548 Mon Sep 17 00:00:00 2001 From: Flawed <33593723+ff14wed@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:04:45 -0800 Subject: [PATCH] First pass at github workflow for generating diff --- .github/workflows/minor_patch_diff.yml | 85 ++++++++++ automation/ffxiv_info.py | 226 +++++++++++++++++++++++++ automation/ffxiv_versions_global.json | 4 - vtable_diff.py | 4 +- 4 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/minor_patch_diff.yml create mode 100644 automation/ffxiv_info.py diff --git a/.github/workflows/minor_patch_diff.yml b/.github/workflows/minor_patch_diff.yml new file mode 100644 index 0000000..a88e04f --- /dev/null +++ b/.github/workflows/minor_patch_diff.yml @@ -0,0 +1,85 @@ +name: Minor Patch Diff Workflow + +on: + schedule: + - cron: '0 7 * * *' # 11PM PST is 7AM UTC + workflow_dispatch: + +jobs: + check-and-diff: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download Latest ffxiv_dx11.exe + id: downloader-latest + uses: WorkingRobot/ffxiv-downloader@v8 + with: + output-path: latest + regex: '^ffxiv_dx11\.exe$' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Fetch Patch Info and Update Versions + id: versions + if: steps.downloader-latest.outputs.updated == 'true' || github.event_name == 'workflow_dispatch' + run: python automation/ffxiv_info.py >> $GITHUB_OUTPUT + + - name: Download Previous ffxiv_dx11.exe + if: steps.versions.outputs.is_new == 'true' + uses: WorkingRobot/ffxiv-downloader@v8 + with: + version: ${{ steps.versions.outputs.date_prev }} + output-path: previous + regex: '^ffxiv_dx11\.exe$' + + - name: Install Analysis Tools + if: steps.versions.outputs.is_new == 'true' + run: | + sudo apt-get update + sudo apt-get install -y radare2 + + - name: Install Python Dependencies + if: steps.versions.outputs.is_new == 'true' + run: | + pip install -r requirements.txt + + - name: Run Diff + if: steps.versions.outputs.is_new == 'true' + run: | + LATEST_EXE=$(find latest -name "ffxiv_dx11.exe" | head -n 1) + PREVIOUS_EXE=$(find previous -name "ffxiv_dx11.exe" | head -n 1) + + mv "$LATEST_EXE" "ffxiv_dx11.${{ steps.versions.outputs.retail_new }}.exe" + mv "$PREVIOUS_EXE" "ffxiv_dx11.${{ steps.versions.outputs.retail_prev }}.exe" + + # Run the diffing process and capture JSON output + python vtable_diff.py "ffxiv_dx11.${{ steps.versions.outputs.retail_prev }}.exe" "ffxiv_dx11.${{ steps.versions.outputs.retail_new }}.exe" > "diffs/${{ steps.versions.outputs.retail_new }}.diff.json" + + - name: Create Pull Request + if: steps.versions.outputs.is_new == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "Update for retail version ${{ steps.versions.outputs.retail_new }} (${{ steps.versions.outputs.date_new }})" + title: "Update for retail version ${{ steps.versions.outputs.retail_new }} (${{ steps.versions.outputs.date_new }})" + body: | + This automatic update contains the following changes: + - Updated `ffxiv_versions_global.json` + - Generated vtable diff for retail version ${{ steps.versions.outputs.retail_new }} + branch: "auto-update-${{ steps.versions.outputs.date_new }}" + base: "main" + delete-branch: true + + - name: Upload Results + if: steps.versions.outputs.is_new == 'true' + uses: actions/upload-artifact@v4 + with: + name: vtable-diffs-${{ steps.versions.outputs.date_new }} + path: | + diffs/*.diff.json diff --git a/automation/ffxiv_info.py b/automation/ffxiv_info.py new file mode 100644 index 0000000..622ab4b --- /dev/null +++ b/automation/ffxiv_info.py @@ -0,0 +1,226 @@ +import os +import sys +import re +import json +import urllib.request +from typing import Optional, List + +# --- Constants --- +THALIAK_REPO = "4e9a232b" +THALIAK_API = f"https://thaliak.xiv.dev/api/v2beta/repositories/{THALIAK_REPO}/patches" +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +VERSIONS_FILE = os.path.join(BASE_DIR, "automation", "ffxiv_versions_global.json") + +# --- Utilities --- + + +def fetch_url(url, is_json=False): + print(f"Fetching {url}...", file=sys.stderr) + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "accept": "application/json", + } + req = urllib.request.Request(url, headers=headers) + try: + with urllib.request.urlopen(req) as response: + content = response.read().decode("utf-8") + if is_json: + return json.loads(content) + return content + except Exception as e: + print(f" Error fetching {url}: {e}", file=sys.stderr) + return None + + +# --- Version Info Helpers --- + + +def get_version_sort_key(v): + """ + Generates a sortable key for FFXIV versions. + Handles both strings (e.g., '7.05h2') and dicts containing 'retail_version'. + """ + if isinstance(v, dict): + v = v.get("retail_version", "0.0") + # Using a list of (type_priority, value) tuples to avoid comparing list vs int + # Priorities: 0 for numeric, 1 for alphabetic + parts = re.findall(r"(\d+|[a-zA-Z]+)", v) + return [(0, int(p)) if p.isdigit() else (1, [ord(c) for c in p]) for p in parts] + + +def load_versions() -> List[dict]: + """Loads all registered versions from the global registry.""" + if os.path.exists(VERSIONS_FILE): + try: + with open(VERSIONS_FILE, "r") as f: + data = json.load(f) + if isinstance(data, list): + data.sort(key=get_version_sort_key) + return data + except (json.JSONDecodeError, IOError) as e: + print(f"Warning: Failed to load versions file: {e}", file=sys.stderr) + return [] + + +def get_latest_version_entry() -> Optional[dict]: + """Returns the most recent version entry from the version file.""" + data = load_versions() + return data[-1] if data else None + + +def fetch_latest_thaliak_patch() -> Optional[str]: + """ + Fetches the latest date-based version string from the Thaliak API. + """ + data = fetch_url(THALIAK_API, is_json=True) + if not data or "patches" not in data or not data["patches"]: + return None + return data["patches"][-1]["version_string"] + + +# --- Lodestone Scraper --- + + +def parse_lodestone_news_list(html): + """ + Parses the Lodestone news category page to extract individual news items. + """ + if not html: + return [] + pattern = re.compile( + r'
(?:]*>.*?)?(?P