Integrations: the CDN, game updates and season cutovers

Where this repo meets the game's servers and the downstream repos (stargazer, chaldea).

Contents

  1. The game's CDN
  2. Game updates and season cutovers

1  The game's CDN

The URL scheme was solved on 2026-09-25 by reading the downloader Lua statically; afkj_extract.cdn implements it (./afkj-extract --only loc --locales-from cdn) and tools/luadis.py re-traces it.

All 17 languages download from the game's public, unauthenticated CDN with no install and no traffic capture. The scheme came from reading Src/App.luae and Src/Framework/Init.luae statically:

  1. GET <cdn>mystical/global/mystical.codee (CDNs from Game.jsone's cdn_list: https://igame-cdn-first.farlightgames.com/, …-second…). A .jsone frame around Lua bytecode that sets ed.packageInfo: latest_pkgs[platform], headers_info[platform] (files_info_offset, files_info_size, header_info_hash), latest_version. The tool reads its constants only and never runs it.
  2. The package is <cdn>pkg/AFKArena.DataPackage.PublicRelease.Build<N>.<platform>.pkg (Build14368.iOS: 37.8 GB). Every file is an HTTP Range read of it (206).
  3. The last files_info_size bytes are afk.bin, byte-identical to the container's. Its records are [u32 n][name][u16][u32 size][u64 offset][u64 xxh64]. The "byte number" from the lead is this offset; the earlier "u32 number + u32 0" reading was the two halves of it.
  4. LanguageDb<LANG>.bytes are stored raw at [offset, offset+size). The FR and EN downloads were byte-identical to the game's; all 17 (478 MB) took 78 s.

xxHash64 (seed 0) verifies each step: afk.bin against header_info_hash, each file against its record. Per update only the build number, the table location and the offsets change. Used by the loc stage's --locales-from cdn (which make feed runs).

For future reference: the same package holds igame.pkg and every other file the extractor reads, so a CdnSource behind the Source interface could run with no install at all. Not planned; local game files remain the source.

2  Game updates and season cutovers

2.1 The update routine

After a game update, with the game closed, from the afkj-parser folder:

  1. ./afkj-extract --locales-from cdn, then ./afkj-parse build (shortcut: make feed). build ends with PASSED or FAILED and a one-line summary of what changed since the last import.
  2. If something is flagged, read out/build/diff.md (or ./afkj-parse diff); ./afkj-parse diff --hero <slug> shows one hero's changed texts with the changed words marked. A fix goes into config.json or the parser, then ./afkj-parse build again, with no re-extract.
  3. In stargazer, npm run import:seasonal (at a season start, npm run import:structure first, section 2.2), then commit there. Stargazer finds the feed through DATA_FEED_DIR in its local .env.local.
  4. Back here, ./afkj-parse accept: the imported feed becomes the next diff's base.
  5. Optionally make clean to free disk: it deletes everything under out/ except the baseline, so the next update extracts again.
  6. At a season start, or when art changes: ./afkj-extract --only portraits --only artifacts --only phantimals (shortcut: make images).

The gates and the diff are described in Model & feed §6 and §7; the commands in CLI §10.

2.2 Season cutover inputs

The cross-repo procedure is the season cutover runbook. What each phase takes from this repo:

PhaseFrom afkj-parserInto
1, structural--only artifacts, --only phantimals (icons, images §6)tools/seasonal_icons.py, into chaldea's dist/img/seasonal/
1, structuralThe feed (./afkj-parse build): artifact ids and stats, phantimal camp slots, factions (HeroTagID), attack ranges and damage types, en/zh namesstargazer npm run import:structure, which writes its season data and name files
2, text./afkj-parse build after the extract (make feed): the feed in out/api/, checked by its gates and the diff (section 2.1)stargazer npm run import:seasonal, then ./afkj-parse accept

import:structure writes the season's artifact data files ({id, name, season, stats}), the artifact en/zh name files and the phantimal data files ({id, name, season, range, damage, faction, qualifyingFactions}). An artifact's id is 6 plus its rank by fairyId in the season set; a phantimal's id is its campSlot; range is its attackRange; damage is physical for a damageType of AD and magic for AP; faction is the feed's, and a camp 5 phantimal's qualifyingFactions is ["hypogean","celestial"]; season is seasonType − 100; English names drop " Spell". It keeps curated keys (targeting, an existing qualifyingFactions), refuses to change an id within a season, and removes the previous season's files. On the Season 8 feed it reproduces all 29 committed files byte for byte.

Lessons from Season 8: phase 1 took factions from CampPet.Camp and three of five were wrong; a phantimal's hidden sixth skill (IsShow false) looked like missing text until checked in game; and an Awakening Lv2 whose text keys ship empty belongs on config.json's awakeningLv2Blocklist, which the model gate's M6 suggests.

Checks only a real update can run

2.3 Updating away from the local machine

For when the local machine and its game install are out of reach, for example travelling with a tablet. A Claude Code cloud session (claude.ai/code: a machine that starts from a fresh clone of each repo) builds the feed from the fallback source, imports it into stargazer and pushes a branch there. The steps were run locally from fresh clones on 2026-10-11 (build 14368); they have not been run in a cloud session yet, see the list at the end of this section.

While awayWhy
Updates: hero, artifact, phantimal and charm text and numbersThe fallback carries the same tables and text. For build 14368 the feed built from it, imported into a fresh clone of stargazer, changed 0 files.
Waits: portraits and season iconsArt comes only from the install (Images).
Waits: the filter check (make audit)It reads the heroes' battle scripts in the install.
Waits: a new hero's pageStargazer adds a hero by a hand-written character file and a portrait. Until then its importer lists the hero as available in the feed and writes nothing for it.
Differs: buffs.jsonNo scriptBuffs, and 14 fewer records in build 14368 (Model §3.10). It is one of our internal files; stargazer never sees it.
  1. Start one cloud session with both repos attached, this one and stargazer (once, beforehand: Claude's GitHub app needs access to this repo, which is private). Nothing needs installing for ./afkj-parse: it uses the standard library only.
  2. Fetch the fallback's latest snapshot, outside both repos so it can never be committed: git clone --depth 1 https://github.com/Koko-boya/AFKJourneyData.git. That is a 200 MB download and about 1.3 GB on disk; a full clone is a 1.6 GB download. It must be a git clone, not a zip download: parse reads the game version and data build from the last commit's subject.
  3. ./afkj-parse build --source <the clone>. The first line names the data build (Data 14368). A fresh clone has no baseline, so the diff line reads "no baseline yet" and passes; the gates run as usual. A failed gate is fixed in config.json or the parser and committed here, then build again.
  4. In stargazer: npm ci, then DATA_FEED_DIR=<absolute path to this repo>/out/api npm run import:seasonal. A fresh clone has no .env.local, so the variable is given on the command line (or set once in the cloud environment).
  5. Read stargazer's git diff. It is the comparison with what is live, because the importers rewrite a file only when its content changes; git diff --word-diff marks the changed words. No diff means the fallback has nothing newer than stargazer: stop there. Read the importers' warnings too: a character with no entry in the feed is a hero that left it.
  6. Run stargazer's own checks (its AGENTS.md), commit, push a branch and open a pull request, where the changes are read before they go live. Stargazer is public: the branch name, commit message and pull request describe the content change only, and never name this repo, the fallback or this routine. git grep -i "afkj-parser\|afkj-data-viewer\|AFKJourneyData" there must find nothing.
  7. Skip ./afkj-parse accept: a cloud session's out/ is discarded with it, so there is no baseline to keep (Model §7.4).

What is lost without a baseline is the diff gate's warnings (Model §7.1): heroes with many changed levels (D2), a source older than the last import (D3) and removed heroes (D4). Stargazer's diff shows the first as large changes to one hero's files and the second as text going back to an earlier wording; its importer reports the third.

Back home. The baseline is still the one accepted before leaving, so the first make feed lists every change since then, most of it already live. Import as usual: stargazer's diff then shows only what is new since the last update made while away. Then ./afkj-parse accept, and make images and make audit for any hero that arrived meanwhile.

Backup: the local machine itself. A Remote Control session (claude remote-control, started inside tmux from this folder before leaving) runs on the local machine and is driven from the tablet. The install updates only when the game is opened, so this path builds from the fallback too: git -C ../AFKJourneyData pull, then section 2.1 with ./afkj-parse build --source ../AFKJourneyData in place of the extract. accept works as usual; the first diff after the source changes, in either direction, also lists the buffs.json difference above (Model §7.4). This path depends on that machine staying up: as set up on 2026-10-11, after a power cut it stays off, and after any restart it waits at the disk-unlock screen for a password typed at the keyboard.

Not yet run