Author SHA1 Message Date
ff14wedandgithub-actions[bot] d4e943f31a Update for 7.50h (2026.05.01.0000.0000) 2026-05-07 12:12:42 +00:00
Flawed 28ea068169 fix typo 2026-04-28 00:55:02 -07:00
Flawed 555829c343 Update for 7.50 2026-04-28 00:36:25 -07:00
Flawed b01e2dc3b6 7.5 opcode offset hack 2026-04-28 00:33:31 -07:00
FlawedandGitHub 01017f29ff Merge pull request #8 from xivdev/auto-update-964e2e9
Update for 7.45h2 (2026.03.17.0000.0000)
2026-03-24 00:41:33 -07:00
ff14wedandgithub-actions[bot] ceabd258b2 Update for 7.45h2 (2026.03.17.0000.0000) 2026-03-24 07:39:04 +00:00
Flawed 964e2e909d Fix dependency order in workflow 2026-03-11 21:55:47 -07:00
Flawed 8bfa266661 Only download latest ffxiv exes if there is a new patch 2026-03-11 21:53:24 -07:00
Flawed 4ba7cfaea1 More robust hotfix version handling in automation 2026-03-11 21:50:14 -07:00
FlawedandGitHub 1c3a2e2ffe Merge pull request #7 from xivdev/auto-update-65e6d9a
Update for 7.45h (2026.03.07.0000.0000)
2026-03-11 01:32:58 -07:00
7 changed files with 18479 additions and 55 deletions
+10 -11
View File
@@ -15,13 +15,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
- 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 - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@@ -30,14 +23,20 @@ jobs:
- name: Fetch Patch Info and Update Versions - name: Fetch Patch Info and Update Versions
id: versions id: versions
if: steps.downloader-latest.outputs.updated == 'true' || github.event_name == 'workflow_dispatch'
run: python automation/ffxiv_info.py >> $GITHUB_OUTPUT run: python automation/ffxiv_info.py >> $GITHUB_OUTPUT
- name: Download Latest ffxiv_dx11.exe
if: steps.versions.outputs.is_new == 'true'
uses: WorkingRobot/ffxiv-downloader@v8
with:
output-path: latest
regex: '^ffxiv_dx11\.exe$'
- name: Download Previous ffxiv_dx11.exe - name: Download Previous ffxiv_dx11.exe
if: steps.versions.outputs.is_new == 'true' if: steps.versions.outputs.is_new == 'true'
uses: WorkingRobot/ffxiv-downloader@v8 uses: WorkingRobot/ffxiv-downloader@v8
with: with:
version: ${{ steps.versions.outputs.date_prev }} version: ${{ steps.versions.outputs.thaliak_version_prev }}
output-path: previous output-path: previous
regex: '^ffxiv_dx11\.exe$' regex: '^ffxiv_dx11\.exe$'
@@ -78,8 +77,8 @@ jobs:
uses: peter-evans/create-pull-request@v8 uses: peter-evans/create-pull-request@v8
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update for ${{ steps.versions.outputs.retail_new }} (${{ steps.versions.outputs.date_new }})" commit-message: "Update for ${{ steps.versions.outputs.retail_new }} (${{ steps.versions.outputs.thaliak_version_new }})"
title: "Update for ${{ steps.versions.outputs.retail_new }} (${{ steps.versions.outputs.date_new }})" title: "Update for ${{ steps.versions.outputs.retail_new }} (${{ steps.versions.outputs.thaliak_version_new }})"
body: | body: |
This automatic update contains the following changes: This automatic update contains the following changes:
- Updated `ffxiv_versions_global.json` - Updated `ffxiv_versions_global.json`
+4 -1
View File
@@ -135,7 +135,10 @@ def get_packet_handler_opcode_offset(r2, exe_file: str):
r2.cmd(f"s {opcode_offset_target}") r2.cmd(f"s {opcode_offset_target}")
r2.cmd("aei; aeim; aeip") # Initialize ESIL VM, stack, and instruction pointer r2.cmd("aei; aeim; aeip") # Initialize ESIL VM, stack, and instruction pointer
if semver.compare(sem_ver, "7.4.0") >= 0: if semver.compare(sem_ver, "7.5.0") >= 0:
offset_reg = "r13"
opcode_offset = _get_opcode_offset_post_72(r2, offset_reg)
elif semver.compare(sem_ver, "7.4.0") >= 0:
offset_reg = "r15" offset_reg = "r15"
opcode_offset = _get_opcode_offset_post_72(r2, offset_reg) opcode_offset = _get_opcode_offset_post_72(r2, offset_reg)
elif semver.compare(sem_ver, "7.3.8") >= 0: elif semver.compare(sem_ver, "7.3.8") >= 0:
+67 -43
View File
@@ -79,12 +79,6 @@ def load_versions() -> List[dict]:
return [] 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]: def fetch_latest_thaliak_patch() -> Optional[str]:
""" """
Fetches the latest date-based version string from the Thaliak API. Fetches the latest date-based version string from the Thaliak API.
@@ -131,8 +125,36 @@ def extract_patch_version(html):
if not html or not (m := re.search(r"patch\s+(\d+\.\d+)", html, re.I)): if not html or not (m := re.search(r"patch\s+(\d+\.\d+)", html, re.I)):
return None return None
v = m.group(1) v = m.group(1)
v = v + "0" if len(v.split(".")[1]) == 1 else v if len(v.split(".")[1]) == 1:
return v + "h" if "HotFixes" in html else v v += "0"
return v
def get_next_hotfix_version(base_v, existing_versions):
matching = [
v["retail_version"]
for v in existing_versions
if v["retail_version"].startswith(base_v)
]
hotfixes = [v for v in matching if "h" in v]
if not hotfixes:
return f"{base_v}h"
# Extract numeric suffix: h -> 1, h2 -> 2, etc.
nums = [int(h.split("h")[1]) if h.split("h")[1].isdigit() else 1 for h in hotfixes]
return f"{base_v}h{max(nums) + 1}"
def determine_retail_version(base_v, existing_versions, thaliak_version_new):
matching = [v for v in existing_versions if v["retail_version"].startswith(base_v)]
latest = matching[-1] if matching else None
# If we have a record for this base version but the Thaliak version is new, it's a hotfix.
if latest and thaliak_version_new and thaliak_version_new != latest["version_string"]:
return get_next_hotfix_version(base_v, existing_versions)
return latest["retail_version"] if latest else base_v
def is_maintenance_post(title): def is_maintenance_post(title):
"""Returns True if the title represents a primary world maintenance post.""" """Returns True if the title represents a primary world maintenance post."""
@@ -140,14 +162,7 @@ def is_maintenance_post(title):
return "all worlds" in t and "maintenance" in t and "follow-up" not in t return "all worlds" in t and "maintenance" in t and "follow-up" not in t
def format_retail_version(v, count): def scrape_latest_maintenance(thaliak_version_new=None):
"""Formats the version with hotfix suffixes: 7.41, 7.41h, 7.41h2, etc."""
if count <= 1:
return v
return f"{v}h{count - 1 if count > 2 else ''}"
def scrape_latest_maintenance():
""" """
Scrapes the Lodestone to find the most recent 'All Worlds Maintenance' post. Scrapes the Lodestone to find the most recent 'All Worlds Maintenance' post.
""" """
@@ -164,6 +179,7 @@ def scrape_latest_maintenance():
return None return None
news_items = parse_lodestone_news_list(html) news_items = parse_lodestone_news_list(html)
existing_versions = load_versions()
for item in news_items: for item in news_items:
title = item["title"] title = item["title"]
@@ -173,12 +189,18 @@ def scrape_latest_maintenance():
continue continue
detail_html = fetch_url(link) detail_html = fetch_url(link)
version = extract_patch_version(detail_html) if not detail_html:
continue
if version: base_version = extract_patch_version(detail_html)
# We assume the first (most recent) valid maintenance post if not base_version:
# with a patch version is our target. continue
return {"retail_version": version, "title": title, "url": link}
retail_version = determine_retail_version(
base_version, existing_versions, thaliak_version_new
)
return {"retail_version": retail_version, "title": title, "url": link}
return None return None
@@ -187,15 +209,15 @@ def get_version_info() -> Optional[dict]:
""" """
Returns the latest version information by combining Thaliak and Lodestone data. Returns the latest version information by combining Thaliak and Lodestone data.
""" """
date_new = fetch_latest_thaliak_patch() thaliak_version_new = fetch_latest_thaliak_patch()
maintenance = scrape_latest_maintenance() maintenance = scrape_latest_maintenance(thaliak_version_new=thaliak_version_new)
if not maintenance and not date_new: if not maintenance and not thaliak_version_new:
return None return None
return { return {
"retail_version": maintenance["retail_version"] if maintenance else "Unknown", "retail_version": maintenance["retail_version"] if maintenance else "Unknown",
"version_string": date_new, "version_string": thaliak_version_new,
"title": maintenance["title"] if maintenance else "Unknown", "title": maintenance["title"] if maintenance else "Unknown",
"url": maintenance["url"] if maintenance else "Unknown", "url": maintenance["url"] if maintenance else "Unknown",
} }
@@ -204,27 +226,26 @@ def get_version_info() -> Optional[dict]:
def get_patch_context(): def get_patch_context():
""" """
Returns a dictionary containing: Returns a dictionary containing:
- retail_prev, date_prev: The latest registered version. - retail_prev, thaliak_version_prev: The latest registered version.
- retail_new, date_new: The latest versions found externally. - retail_new, thaliak_version_new: The latest versions found externally.
- is_new: True if date_new is not in the registry. - is_new: True if thaliak_version_new is not in the registry.
""" """
versions = load_versions() versions = load_versions()
prev = versions[-1] if versions else None prev = versions[-1] if versions else None
date_new = fetch_latest_thaliak_patch() thaliak_version_new = fetch_latest_thaliak_patch()
maintenance = scrape_latest_maintenance(thaliak_version_new=thaliak_version_new)
maintenance = scrape_latest_maintenance()
retail_new = maintenance["retail_version"] if maintenance else None retail_new = maintenance["retail_version"] if maintenance else None
is_new = False is_new = thaliak_version_new is not None and not any(
if date_new: v["version_string"] == thaliak_version_new for v in versions
is_new = not any(v["version_string"] == date_new for v in versions) )
return { return {
"retail_prev": prev["retail_version"] if prev else None, "retail_prev": prev["retail_version"] if prev else None,
"date_prev": prev["version_string"] if prev else None, "thaliak_version_prev": prev["version_string"] if prev else None,
"retail_new": retail_new, "retail_new": retail_new,
"date_new": date_new, "thaliak_version_new": thaliak_version_new,
"is_new": is_new, "is_new": is_new,
} }
@@ -236,23 +257,26 @@ def update_and_get_info():
""" """
ctx = get_patch_context() ctx = get_patch_context()
if ctx["date_prev"]: if ctx["thaliak_version_prev"]:
print(f"date_prev={ctx['date_prev']}") print(f"thaliak_version_prev={ctx['thaliak_version_prev']}")
if ctx["retail_prev"]: if ctx["retail_prev"]:
print(f"retail_prev={ctx['retail_prev']}") print(f"retail_prev={ctx['retail_prev']}")
if ctx["date_new"]: if ctx["thaliak_version_new"]:
print(f"date_new={ctx['date_new']}") print(f"thaliak_version_new={ctx['thaliak_version_new']}")
if ctx["retail_new"]: if ctx["retail_new"]:
print(f"retail_new={ctx['retail_new']}") print(f"retail_new={ctx['retail_new']}")
print(f"is_new={'true' if ctx['is_new'] else 'false'}") print(f"is_new={'true' if ctx['is_new'] else 'false'}")
if ctx["is_new"] and ctx["date_new"] and ctx["retail_new"]: if ctx["is_new"] and ctx["thaliak_version_new"] and ctx["retail_new"]:
data = load_versions() data = load_versions()
data.append( data.append(
{"retail_version": ctx["retail_new"], "version_string": ctx["date_new"]} {
"retail_version": ctx["retail_new"],
"version_string": ctx["thaliak_version_new"],
}
) )
print( print(
f"Added new version mapping: {ctx['retail_new']} -> {ctx['date_new']}", f"Added new version mapping: {ctx['retail_new']} -> {ctx['thaliak_version_new']}",
file=sys.stderr, file=sys.stderr,
) )
with open(VERSIONS_FILE, "w") as f: with open(VERSIONS_FILE, "w") as f:
+12
View File
@@ -134,5 +134,17 @@
{ {
"retail_version": "7.45h", "retail_version": "7.45h",
"version_string": "2026.03.07.0000.0000" "version_string": "2026.03.07.0000.0000"
},
{
"retail_version": "7.45h2",
"version_string": "2026.03.17.0000.0000"
},
{
"retail_version": "7.50",
"version_string": "2026.04.21.0000.0000"
},
{
"retail_version": "7.50h",
"version_string": "2026.05.01.0000.0000"
} }
] ]
File diff suppressed because it is too large Load Diff
+6158
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff