Files
OpenMaidEngine/vm-map/himegari-opcode-notes.md
gamer147 463877773c chore: initialize age-reimpl repo
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>
2026-07-06 12:58:30 -04:00

8.1 KiB
Raw Blame History

Himegari opcode inference notes

Classification of the top ~20 unnamed opcodes (Phase 3.0, inference-only, 2026-07-06). Machine-readable form: tools/age_opcodes_himegari.py (INFERRED). Evidence gathered with tools/opcode_context.py over the 481-script Output/DATA1 corpus (1,502,986 instructions).

Baseline: named opcodes = 72.62% of instruction volume. Classifying the top 20 unnamed ops covers to ~96.7% cumulative. Kelebek labels (u004xxxx) are engine addresses from a later AGE title — opcode number + argc are validated for Himegari (481/481 clean decode); the meanings below are inferred and flagged by confidence + confirmation method.

Buckets

A. Statement/scope scaffolding — zero-arg, no operands, safe VM no-ops (HIGH)

The compiler brackets statements/blocks with operand-less markers. They carry no data and sit at structural boundaries, so the VM v1 skips them; the Phase-4 dialogue harness confirms.

  • 0x1f4 stmt-begin (60,297; 4.01%) — opens scripts (first instr of ADDEN/ADDEXP/…), and stmt-end 0x1f5 → 0x1f4 pairs 25,899×. Successor is the statement's first real op (eq/mov/call).
  • 0x1f5 stmt-end (60,297; 4.01%) — equal count to 0x1f4; precedes exit/next statement. ... → 0x1f5 → stmt-begin/lt/show-text.
  • 0x1d5 cond-block (34,238; 2.28%) — always preceded by jcc; marks the fall-through (condition-true) block entry. jcc cond -1 label; 0x1d5; <body>.
  • 0x1bc block-mark (26,865; 1.79%) — jcc → 0x1bc → mov (26,413×): another block boundary.
  • 0x1bf call-end (8,752; 0.58%) — call → 0x1bf → stmt-end (8,600×): end-of-call statement.

B. Statement metadata with an id — tentative no-op, confirm via harness (MED/LOW)

Same scaffolding family but carrying an immediate; likely line numbers / statement descriptors. Treated as no-ops in v1 but flagged method=harness because a wrong no-op here could drop state.

  • 0x21b line-id? (27,016; 1.80%) — mov → 0x21b <imm> → stmt-end. Arg is 0/1/small. Sits near savemesskip/loadmesskip dev-comments (MENU/BUNKI) — verify it isn't a message-control toggle before trusting the no-op.
  • 0x1d2 stmt-desc? (17,323; 1.15%) — always immediately after stmt-begin 0x1f4; 2 imm args (e.g. 0x2 0x5) then a mov. Reads like a per-statement descriptor.
  • 0x258 decl? (7,224; 0.48%) — runs in a chain right after script-entry 0x259, enumerating ids (0x4 0x1, 0x5 0x1, 0x6 0x1, …). Prologue declaration/registration table.

C. Structural (HIGH)

  • 0x71 label-def (26,445; 1.76%) — its corpus count exactly equals the T1 table size, so this is the instruction T1 (tag 0x71) indexes: a label/anchor definition. Arg is a small id. Preceded by end-text-line/call. VM v1 = no-op; revisit if menu/callback dispatch resolves entries by this id (T1 may be a named-entry-point table).

D. Computational — exact behaviour via Unicorn/inference (MED/LOW)

  • 0x215 count? (5,675; 0.38%) — writes its first operand (a global) which is then tested gre …, 0 / lt and branched on. Signature (global,global). Looks like a count / search-returns -index helper. Micro-exec candidate.
  • 0x1a2 resolve-handle? (17,585; 1.17%) — takes a local-ptr just filled by lookup-array, then create-texture follows. Resolves a looked-up resource/handle. Borderline effectful → Frida.

E. Effectful — draw / UI / audio / ADV; need Frida to confirm (see shortlist)

All in the 0x4200x421 graphics family or 0x41D audio family, clustered with named draw/audio ops. Not semantically no-ops; VM must implement (or knowingly stub) them.

  • 0x7a text-param? (17,324; 1.15%) — sub computes a value, then 0x7a <imm> <computed> <imm>, then show-text. A text speed / auto-wait / window parameter for the following line. (The review guessed "computational"; context says ADV-text-effectful.)
  • 0x202 draw-blit? (9,223) / 0x203 draw? (9,149) — 45 coord/size args, preceded by coordinate arithmetic, chained with draw-texture/u00420950. Blit/positioned-draw.
  • 0x1f7 ui-elem? (7,735) / 0x1fa ui-clear? (6,126) — paired (0x1f7 → 0x1fa 5,014×); create/begin a UI element then show/hide/clear it by id. From MENU: 0x1f7 0x11170 0xc80, 0x1fa 0x49/0x4a/0x4b.
  • 0x217 / 0x218 / 0x21a gfx-geom? (~5,000 each) — a chain each taking 4 global-ints (0x21a → 0x218, 0x217 → 0x1ff); rect/coordinate/geometry transforms in the draw library.
  • 0x1ff draw? (3,926) — 4 args then call; follows 0x217.
  • 0xb6 snd-ctrl? (4,805) — 0x41D family, self-chains, near play-sound-effect/0xb5; sound channel / volume / stop control.

F. Input / UI hotspot branch — op 0x90 & 0x97 (A0-slice deep-dive, 2026-07-06 s2, HIGH)

Solved while investigating the A0 EMPTY scenes. Kelebek: {0x90,"u0041BEB0",0x7} // args 5,6,7 = code locations (they left it "ukn"). Corpus evidence (scratchpad/analyze_0x90.py over all scripts):

  • op 0x90 = pointer/input hotspot hit-test branch. Encoding 0x90 x y w h tgt_a tgt_b tgt_c (argc 7; targets 0xffffffff = unused). Branches to one of 3 targets per interaction outcome and falls through to pc+1 when nothing matches — design-confirmed: a 0x90 is 15 dwords, and the instruction after the last one lands exactly on the fall-through statement (0xd0+15 = 0xdf).
  • Two forms, both only in one shared subroutine:
    • Mode A (1505 = 5×301): all-immediate, w=h=20. The five on-screen ADV buttons — identical in all 301 scripts at (x,y) = (684|706|728|750|772, 572), each setting one of G[0x6c9..0x6cd] to 1 (target a) / 0 (target b) / 0+run-action (target c). Reads as hover-enter / hover-leave / click.
    • Mode B (903 = 3×301): local-int operands, w=h=1, only target c real — a keyed/degenerate 2-way input test (fall through, or jump to c). Operands are 0-1000-based sentinels (key codes?).
  • Distribution is unanimous: every one of the 301 ADV scripts has EXACTLY 8 sites (5 A + 3 B). Zero scene-specific use — it is boilerplate ADV chrome (the system button row + input polling), not gameplay branching.
  • VM handling: headless (no cursor/input) ⇒ fall through ⇒ our stub is already correct; proven safe by all 279 CLEAN dialogue scenes (which contain these same 8 sites). So 0x90 is NOT a headless correctness hole. It must be modelled for real in A2 (Godot input backend) as a live hotspot test; confirm the exact target→interaction-state mapping via input capture / Frida then.
  • op 0x97 (u0041C150, argc 5: v1 v2 1 1 <action-id>, NO code targets) interleaves with 0x90 in the same subroutine → companion register-hotspot / set-widget-action call (trailing imm = action id 0x0/0x7/0x8). Not control flow; part of the 0x90/0x91/0x92/0x95/0x97 widget cluster.

Consequence for A0/A1: the 12 EMPTY scenes are gated by state + interactive input-wait loops (this chrome polls hotspots until a click), NOT by unmodelled 0x90 semantics. A1 can keep 0x90 as fall-through with confidence; unlocking the EMPTY family is a Phase-A2/B task (seed state + input).

Coverage after classification

See build/opcode-coverage.md. Roughly: named 72.6% + no-op scaffolding & label-def (A+C, high conf) ≈ 14.4% + tentative-no-op metadata (B) ≈ 3.4% → ~90% handleable without live tools; the effectful/computational remainder (D+E) ≈ 67% is classified but needs Frida/Unicorn for exact behaviour.

Frida / Unicorn shortlist (feeds Phase 3.1/3.2)

  • Frida (effectful): 0x7a (adv text param), 0x202/0x203/0x1f7/0x1fa/0x217/0x218/0x21a/0x1ff (draw/UI), 0xb6 (audio), 0x1a2 (resource resolve). Also Frida-confirm already-named effectful ops the VM will rely on: play-voice 0xc4, play-bgm 0xbf, play-sound-effect 0xb4, draw-texture 0x1fb, draw-string 0x204, create-texture 0x1f8, set-texture 0x1f9.
  • Unicorn (computational): 0x215 (count/search).
  • Harness (no live tools): 0x21b / 0x1d2 / 0x258 — confirm no-op assumption by diffing the VM's show-text sequence per scene against build/text/dialogue.jsonl.