Reverse-engineering + open reimplementation workspace for Eushully's AGE/SYS4 engine (first target: Himegari). The repo root is age-reimpl/; the original game install and the extracted ALF data are siblings outside the repo and are never tracked. build/ (derived corpora) is gitignored and regenerated by the tools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17 KiB
AGE Remake — Architecture & Roadmap (option 3: remake / enhance / mod)
Decided goal (2026-07-06): not a translation patch (a translated build is already playable), not a bare cross-platform port — but an open reimplementation of the AGE engine that runs the original games and makes modding a first-class feature. Himegari (SYS4) is the first target; the design must extend to other AGE games and engine versions (SYS3/SYS5).
The right mental model is ScummVM / OpenMW for Eushully's AGE engine: we ship an engine; the user supplies the original game data they own; mods layer on top. Those projects prove this shape is feasible — and that it's a large, long-lived effort. Our decoding work (done) is the foundation; the runtime + backends + mod system is the bulk of the remaining work.
1. Guiding principles
- Bytecode-faithful logic, pragmatic presentation. Run the original
.BINscripts on a reimplemented VM — you get every gameplay rule (damage, dungeon, battle, recovery) correct by construction, without re-deriving them. Reimplement the effectful ops (draw/text/audio/input) against a clean modern backend that looks right, not byte-identical to D3D9. - One engine, many profiles. Do NOT fork per game or per version. A shared VM core, with version front-ends (parser/codec/opcode table) and per-game profiles (maps, data schemas, asset rules) selected by a manifest.
- Modding is architecture, not an afterthought. The data model, content loading, and script
dispatch are designed so mods can override assets, edit data, patch scripts, and inject
host-language hooks. The engine already hints at this: 52 loose root
.BINfiles natively shadow their archived copies — a built-in override mechanism we generalize. - The original owns the content; we own the engine. Users provide their AGE install; the runtime imports/loads it. This keeps us on the right side of distribution and mirrors ScummVM.
- De-risk with vertical slices. Prove "run one scene end-to-end" before breadth. Nothing is real until a script executes and renders.
2. Target architecture — the split
Three layers, cleanly separated:
┌──────────────────────────────────────────────────────────────────────┐
│ RUNTIME (Godot + C#) — ships to players & modders │
│ ├─ AGE VM core (C#) fetch/execute, typed var banks, control flow │
│ ├─ Version front-ends SYS3 / SYS4 / SYS5: header, string codec, │
│ │ opcode table, archive format │
│ ├─ Backend adapters render(Godot 2D) · text/msg · audio · input │
│ │ — the effectful opcodes call into these │
│ ├─ Content loader ALF archives + loose overrides + mod folders │
│ ├─ Data layer game data from moddable files (bootstrapped │
│ │ from *INIT extraction) │
│ └─ Mod system override resolution · hook API · patch loader │
├──────────────────────────────────────────────────────────────────────┤
│ PROFILE / MANIFEST (per game) — data, not code │
│ engine_version, archives, string_codec, opcode_table_ref, │
│ global_var_map, callscript_map, data_schemas, boot_entry, │
│ asset_conversion rules │
├──────────────────────────────────────────────────────────────────────┤
│ TOOLCHAIN (Python — what we've already built) — offline, for modders │
│ disassembler · assembler · extractors · global-map builder · │
│ data exporters · mod packaging │
└──────────────────────────────────────────────────────────────────────┘
- Runtime language. VM core in C# (Godot's C# support) — a 1.5M-instruction fetch/execute loop is too hot for GDScript. Presentation, UI, mod tooling, and export targets use Godot. This is why Godot now fits: under the earlier "faithful port" framing it was overkill (you'd use ~10% of it); under remake/enhance/mod its editor, UI toolkit, asset pipeline, GDScript modding, and multi-platform export all earn their keep.
- Toolchain vs runtime. The runtime owns the canonical parser+VM (C#). The Python tools remain the offline analysis/authoring chain; they were the reference implementation and stay useful for modders. They share the format spec (documented), not code — acceptable for a small, stable container format.
- Profile = manifest. Adding a game = a new profile + its maps. Adding an engine version = a new front-end plugin + profiles that reference it. See §5.
3. How modding works with a bytecode VM
Modding is tiered from trivial to deep. The first two tiers cover the large majority of "proper modding" and need no decompilation.
Tier 1 — assets & data (easy, no tools needed beyond a text/image editor).
- Asset overrides: drop replacement textures/CGs/voices/BGM into a mod folder; the content loader resolves mod → loose-override → archive (generalizing the engine's native override behavior).
- Data edits: game data (skills/items/units/maps/stages) is externalized to editable files
(JSON) that the runtime loads, bootstrapped from our
*INITextraction. Rebalancing, new items, new skills = editing JSON. No bytecode involved.
Tier 2 — logic (medium; asm-level or host-language).
- Script patches: disassemble → edit the
.age-asm→ reassemble to.BIN(Kelebek's project has a reassembler to adapt). Mods ship patched/replacement scripts; the VM runs them unmodified. - Host hooks: a mod API lets mods register callbacks in GDScript/C# — fire before/after a script, intercept an opcode, replace a script by id, react to events, add UI. Original scripts run as-is; mods augment. (This is the BepInEx/script-extender model and avoids a bytecode compiler for most behavioral mods.)
Tier 3 — a friendly modding language (stretch, later).
- A high-level decompiled DSL + a compiler back to bytecode, so mods are written in readable source. This is a real compiler project and its quality is bounded by how complete the global-var map and call-script resolution are. Realistic as a later milestone, not near-term.
Readability, concretely: annotated disassembly is achievable today (opcode names + global-var aliases + eventually call-script names). Pseudo-decompilation for reading is feasible (demonstrated on RECOVER). Clean round-trippable source is Tier 3. So near-term "how readable" = well-annotated assembly + external data/assets + host hooks; the read-like-C dream is a stretch goal.
Two enablers become load-bearing under this goal (they were "polish" for a port):
- Global-var map — modders must know what game state a global is to touch it safely.
- Call-script resolution — needed both to run scripts and to add/replace scenes. This is now on the critical path, not deferred.
4. Roadmap — high-level progression
Each phase ends with something demonstrable. The documented side-tasks map into these phases (noted).
Phase A — Prove the VM (vertical slice) ⟵ the immediate priority
Goal: run one ADV scene end-to-end in the new runtime — background image + dialogue + a choice +
a voice line — and match its show-text sequence to build/text/dialogue.jsonl.
Forces, and thereby de-risks, every core unknown at once:
- Port the container parser + VM core to C#.
- Implement the ADV effectful ops against Godot:
show-text,end-text-line,wait-for-input,set-font,play-voice,play-bgm,draw-texture/create-texture/draw-string, choices. - Resolve just enough
call-scriptto enter/leave a scene (side-task:SCJUMP.BINdecode or a targeted Frida capture — now critical-path). - AGF → texture for the one scene's art (side-task;
AGF2BMP2AGF.exealready on disk). - Treat the classified no-op markers as skips; validate the tentative-no-op ops via the dialogue diff.
Phase B — Broaden coverage (playable ADV, then systems)
- Implement the remaining effectful ops; Frida sessions for the opaque ones (the shortlist in
build/opcode-coverage.md); Unicorn for0x215-style computational ops. - Grow the global-var map (side-task 2.5:
*MESwriters → record-table readers → Frida field naming) — now a core enabler, not polish. - Get a full chapter of ADV playable; then the dungeon/battle/menu systems (they run as bytecode — we implement the ops they use, not the rules).
- Side-tasks absorbed here:
STINITparser, remaining data schemas, save-file format (needed for a real playthrough — reversible struct work).
Phase C — Externalize & modding foundation
- Move game data from bytecode-embedded tables to editable external files the runtime loads.
- Generalize the override/mod-loading (mod folders, load order) from the engine's native loose-file mechanism.
- Asset pipeline: AGF↔PNG, audio, packaging. → Tier-1 modding works.
Phase D — Logic modding
- Integrate the assembler (Tier-2 bytecode-patch mods) and ship the host hook API (GDScript/C#). → Tier-2 modding works.
- Optionally invest in decompiler quality toward Tier 3.
Phase E — Enhance, polish, productize
- Enhancements the VM unlocks: higher/wide resolution, faster text, QoL, save-anywhere, new-content mods. Modding docs + tools. Save/UX polish.
5. The VM as our analysis instrument — and the correctness bootstrap
Beyond being the runtime, the VM is the best analysis tool we can build — but only once it is validated-correct, and that ordering is load-bearing.
Upside: owning the VM turns static RE into dynamic observation. Instrumenting the original
packed AGE.EXE means fighting an anti-debug binary with Frida; instrumenting our interpreter is
one line in a handler. So a class of documented side-tasks become built-in debugger views instead of
separate investigations:
- Live named-global watch — with the global-var map, watch state change in real time; take damage
→ see which global moved → that is the "name which stat" step (
name-resolution.md→ Future), now a debugger feature, not a Frida session. - Call-graph / dispatch trace — watch
call-scriptresolve live, helping crack the entry-point registry dynamically. - Breakpoints, single-step, var-bank inspection, opcode/script/global coverage; and because VM state is just variable banks + a program counter, cheap snapshot / rewind (time-travel debugging nearly falls out of the design).
The hard caveat: the instrument is only as trustworthy as the VM is correct. A VM that executes wrong produces wrong observations — and circularly so: you would "learn" false facts about game state from a broken interpreter and bake them into the global map, the dispatch model, and everything downstream. The analysis power is unlocked by correctness, not a substitute for achieving it. You cannot debug the unknown with an instrument you have not first validated. Until the VM is running mostly correctly, using it for analysis is meaningless.
Therefore the bootstrap order matters:
- Build the VM.
- Validate it against ground truth we already hold, by independent means — chiefly the dialogue
oracle (
build/text/dialogue.jsonl: the VM'sshow-textsequence per scene must match), plus differential checks against known-correct behavior. This external oracle certifies the instrument. - Only then use the validated core's observability to understand the adjacent unknown — which globals mean what, dispatch, effectful-op behavior. Correctness propagates outward from validated anchors.
Two refinements that bound the trust:
- Trust is per-subsystem, and only as strong as the oracle covering it. The dialogue diff strongly validates the ADV/text layer. But computational/battle logic has weaker oracles — a wrong damage number can look plausible and pass unnoticed. So "mostly working" must mean demonstrated correct per subsystem; where oracles are weak (battle math), the VM-as-instrument is correspondingly less trustworthy, and Frida/Unicorn cross-checks retain their value there (exactly why Unicorn stayed on the list for computational ops). A green ADV oracle does not imply the battle math is right.
- Coverage-limited, plus a chicken-and-egg. Runtime tools only observe what a playthrough
exercises (rare branches stay dark), so they complement rather than replace static analysis. And
bootstrapping the VM needs just enough
call-scriptdispatch to run before observation can help refine dispatch — hence Phase A hardcodes a minimal dispatch first.
Net effect on sequencing: the runtime debugger makes most of the Frida/Unicorn side-tasks cheaper or obsolete — but only after the VM earns trust on a validated core. So the priority is to reach validated correctness on the ADV layer first (via the dialogue oracle); that trusted anchor is what makes the instrument usable for everything else. The debugger is a force multiplier on a correct VM and dead weight on an incorrect one.
6. Extending to other AGE games and versions
Other AGE games, same version (e.g. Kamidori, also SYS4)
Reused for free: container parser, VM core, backend adapters, the opcode table (the engine ABI
is shared across the family), disassembler/assembler, the whole extraction methodology.
Per-game (inherent content work): the global-var map (globals are game-specific), the
call-script registry, the data-table layouts (each game's *INIT differs), assets, and any
game-specific effectful behavior. Process: point the toolchain at the new game's archives, re-run
extraction, rebuild its global map, resolve its call-script registry, author a profile. The long
poles are exactly the two enablers (global map + call-script). This is the core payoff of the VM
approach: the engine cost amortizes across all AGE games; only content-mapping recurs — far less
than re-coding each game's logic bespoke.
Other engine versions (SYS3 / SYS5) — one app, not many
Versions differ in: header (SYS4 0x3C vs SYS5 0x44), string codec (SYS4 cp932^0xFF vs SYS5
UTF-16^0xFFFF), opcode set (overlapping, version-specific; Kelebek's table already spans the family
and notes version-gated ops), operand types (SYS5 adds 0x8003+), and archive magic (S3IC/S4IC/
S5IN). Architecture answer: version-parameterize, don't fork. One runtime with:
- a version-detection step (magic → SYS3/4/5),
- pluggable front-ends (header parser, string decoder, opcode table, archive reader per version),
- the shared VM core (the execution model — opcode+typed operands, var banks, control flow — is the same engine evolving),
- per-game profiles that name the version + game-specific maps.
So: not a separate application per version — a manifest/profile selects the front-end + game data. SYS5 is well-covered (it's Kelebek's target); SYS3 is older and less documented and would need more front-end work (Kelebek's parser only does SYS4/SYS5), but the plug-in shape accommodates it. Result: one "AGE Engine" app that, given a profile, runs Himegari, Kamidori, a SYS5 title, etc., each moddable through the same system.
7. Feasibility, risks, open questions
Feasible? Yes — but it is the largest phase of the whole effort, on the scale of a small ScummVM target. The decoding groundwork substantially de-risks it (we understand the format, 97% of opcodes, the data, a partial global map). Biggest risks, with mitigations:
call-scriptdispatch entangled in the packed AGE.EXE → trySCJUMP.BINstatic decode first; fall back to a targeted Frida capture. Gating for anything running.- Effectful-op surface is large and quirk-laden (esp. SRPG battle/dungeon UI) → ADV-first; defer SRPG; lean on the Frida shortlist.
- AGF graphics → low risk;
AGF2BMP2AGF.exe(asmodean) already present. - Save format → reversible struct work; needed before a full playthrough.
- Toolchain (Python) vs runtime (C#) drift → share the documented format spec; runtime is canonical.
Open questions to resolve early: exact call-script mechanism; how scenes register (needed to
add content); how much the SRPG layer's rendering diverges from ADV; save layout.
8. Immediate next step
Start Phase A, the vertical slice — it converts all of the above from architecture into evidence
and tells us fast whether option 3 is as feasible as it looks. Concretely: pick one small ADV scene,
stand up the C# VM core + Godot ADV backend, resolve just-enough call-script, convert that scene's
AGF art, and get its dialogue rendering and matching build/text/dialogue.jsonl. Everything else in
this roadmap is sequenced behind that proof.