feat(assets): solve asset resolution (SYS4INI per-scene section manifest)
resId -> files[section_base(scene) + resId]. SYS4INI's file list is sectioned, one per scene (SCxxxx.BIN + its cross-archive asset manifest); file_number is the index within the section. Unified for set-texture, play-bgm, play-voice. Fully static/general -> no per-scene capture. - tools/parse_sys4ini.py: SYS4INI (S4IC422, LZSS) -> build/asset-index.json - tools/resolve_asset.py: sections + (scene,resId) resolver -> build/asset-sections.json - validated: 97% structural, SC0000 17/17 vs Frida, 586/595 captured loads - opcodes.toml: set-texture/create/draw-texture, play-bgm/voice enriched (frida-grounded) - Frida tooling (capture_load_order all-archive, correlate_scope, ...) + vm0 --settex - docs: asset-resolution-re (step2 SOLVED), global-memory-re (shelved), tools-reference Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,10 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
|
||||
│ └── opcode-leads.json, small-script-listings.md
|
||||
│
|
||||
├── docs/ all documentation
|
||||
│ ├── PROJECT-STRUCTURE.md this file
|
||||
│ ├── PROJECT-STRUCTURE.md this file (where things live)
|
||||
│ ├── tools-reference.md every tool: purpose, usage, I/O (operational companion)
|
||||
│ ├── asset-resolution-re.md resId→file RE (graphics/audio); asset-index steering
|
||||
│ ├── global-memory-re.md runtime global observation RE (SHELVED; future starting point)
|
||||
│ ├── remake-architecture-and-roadmap.md THE direction doc (phases A–E)
|
||||
│ ├── phase-a-slice-plan.md the current slice (A0/A1/A2)
|
||||
│ ├── vm-mapping-plan.md the phased decode plan
|
||||
|
||||
@@ -31,8 +31,9 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
- **The resolution chain is opaque statically.** `CGINIT` (`build/data/CGINIT.json`) is a
|
||||
925-column *numeric* record table (row-major, sparse) — **not** an id→filename map.
|
||||
**`SYS4INI.BIN` (magic `S4IC422`) is the authoritative asset index** the game + `BinExtractALF`
|
||||
use (name ↔ archive ↔ offset ↔ size), but filenames are **not stored as plain ASCII** (an
|
||||
`EV001AA` search misses), so it needs S4IC-format RE to parse.
|
||||
use (name ↔ archive ↔ offset ↔ size). Filenames aren't plain ASCII because the whole directory
|
||||
is **LZSS-compressed** (not encrypted). **DONE (2026-07-06):** `tools/parse_sys4ini.py` parses it
|
||||
→ `build/asset-index.json` (13206 entries). See step 1 below.
|
||||
- **Frida file-I/O is noisy.** `ReadFile` hooks on `DATA2.ALF` capture reads during the opening, but
|
||||
the offsets/spans don't line up with extracted AGF sizes → the game likely **memory-maps** the
|
||||
archives (so `ReadFile` offsets are OS paging, not clean per-asset loads) and/or uses async reads.
|
||||
@@ -40,16 +41,46 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
|
||||
## The RE plan (ordered)
|
||||
|
||||
1. **Parse `SYS4INI` (S4IC422) → an asset index** `{name, archive, offset, size}`. *Reusable and
|
||||
bounded* — it names every asset in every DATA*.ALF, gives archive-offset→name (to rescue Frida
|
||||
offsets), and is the **answer key** for step 2. Deliverable: `tools/parse_sys4ini.py` +
|
||||
`build/asset-index.json`. (Format reference: asmodean's `exs4alf`, which `BinExtractALF` is based on.)
|
||||
2. **Crack `resId → filename`.** With SYS4INI as the answer key, either (a) **order-correlate**: run
|
||||
SC0000 in our engine to get the `set-texture(resId)` sequence, capture the real game's asset-load
|
||||
order via a *reliable* Frida hook, and align them; or (b) **hook the internal load-by-id
|
||||
function** directly (find via the opcode dispatch for `0x1f9`) to read `resId → name` at the
|
||||
source. Likely underlying rule: `resId → CGINIT/table → name`. Deliverable: the mechanism +
|
||||
`vm-map/resources.json` (or a generated map) seeding at least SC0000's slideshow.
|
||||
1. **Parse `SYS4INI` (S4IC422) → an asset index** `{name, archive, offset, size}`. **✅ DONE
|
||||
(2026-07-06).** `tools/parse_sys4ini.py` → `build/asset-index.json`: 5 archives (DATA1–5),
|
||||
13206 real entries (2 `@` placeholders skipped). **Format:** `uint32 packed_size @0x134`, then an
|
||||
LZSS stream at `0x138` running to EOF (GARbro-style: 0x1000 zero-filled ring buffer, init pos
|
||||
0xFEE, control bits LSB→MSB, 1=literal / 0=two-byte backref `off=(hi&0xf0)<<4|lo`, `len=3+(hi&0xf)`).
|
||||
Decompresses to `uint32 arc_count`, `arc_count × char[256]` archive names, `uint32 file_count`,
|
||||
then `file_count ×` 80-byte records `{char name[64]; u32 arc_id, file_number, offset, size}`.
|
||||
**Validated:** decompressed length (1058783) equals the stored size dword at `0x12c`; per-archive
|
||||
counts match the `extracted/` ground truth exactly (DATA2=985, DATA3=39, DATA4=9733, DATA5=210);
|
||||
all 13206 `offset+size` fit inside their real `.ALF`; 837 name-matched files → 0 size mismatches.
|
||||
`files[]` preserves directory order (feeds step 2's order-correlation). Re-run:
|
||||
`py -3.11 -X utf8 tools/parse_sys4ini.py --check`. (Ref: asmodean's `exs4alf` / GARbro Eushully `ArcALF.cs`.)
|
||||
2. **Resolve `resId → asset file`.** **✅ SOLVED (2026-07-06) — fully static & general; NO runtime capture.**
|
||||
|
||||
**The rule:** SYS4INI's file list is organized into **SECTIONS, one per scene** — each is a
|
||||
`SCxxxx.BIN` script entry followed by that scene's **asset MANIFEST**: every asset it references,
|
||||
across *all* archives and types (EV/BG/CS/AE graphics **and** OGG/WAV audio), interleaved in usage
|
||||
order. `file_number` is the **0-based index within the section**. So:
|
||||
|
||||
> **`resId → files[ section_base(scene) + resId ]`**, where `section_base` = the start of the SYS4INI
|
||||
> section containing the scene's `SCxxxx.BIN`.
|
||||
|
||||
Unified for `set-texture(resId)`, `play-bgm(id)`, `play-voice(id)` — one manifest. **Tool:**
|
||||
`tools/resolve_asset.py --build` → `build/asset-sections.json` (359 sections, 136 scenes);
|
||||
`resolve_asset.py <SCENE> [resId]` resolves. **Validated:** `file_number == position − section_base`
|
||||
for 12848/13206 files (97%); SC0000 resolves 17/17 across archives vs the Frida capture (`0x25→EV052CA`,
|
||||
`0x36→BG030A` background, `0x6c→EM* effect`, `play-bgm 5→BGM006`); 586/595 distinct captured loads
|
||||
(all sections) satisfy `files[base+fn]==name`. This is the derivable rule that generalizes to any
|
||||
AGE game with the same container — **the "scope" was just which SYS4INI section the scene lives in.**
|
||||
|
||||
*How we got here (condensed):* first confirmed `resId == file_number` via Frida load-order correlation
|
||||
for SC0000's opening, but `file_number` is not globally unique so a per-scene "scope" was needed. A long
|
||||
hunt for the selector (thought it was native scene state; even tried reading `G[0x62424]` live — the
|
||||
VM global memory is structured/packed, see `docs/global-memory-re.md`) missed the real structure until a
|
||||
**full multi-archive capture** (user domain tip: DATA1 holds BG/CS/CB/CA/CP graphics by name prefix, not
|
||||
just DATA2 EV CGs) revealed `file_number == SYS4INI position` inside per-scene sections. Superseded tools:
|
||||
`tools/correlate_scope.py`, `vm0.py --settex` (VM set-texture trace; still useful, but vm0 diverges on
|
||||
branchy non-opening scenes — use the C# VM to trace those). Runtime note for future work: the game is
|
||||
**packed** (main VM logic in a per-run heap `r-x` region) and streams archives through a heap block-cache
|
||||
via `ReadFile` (not mmap); the stable AGF decoder is `AGE.EXE+0x74f1f`.
|
||||
3. **Wire the backend** (already designed — A2b-background plan Tasks 3–5): `ResourceMap` resolver +
|
||||
Godot `TextureRect` compositing; render only resolved full-screen slots. Mechanical once (1)+(2) land.
|
||||
4. **Audio** (parallel, same shape): resolve `play-voice`/`play-bgm` `id → OGG` via SYS4INI + a
|
||||
@@ -67,7 +98,9 @@ rendering what the executed bytecode + the map produce (never a hardcoded image)
|
||||
|
||||
## Status
|
||||
|
||||
A2b-background: **machinery landed** (texture ops engine-driven, tools, findings). The **render is
|
||||
blocked on asset resolution** (steps 1–2), which is promoted to its own foundational effort. Next:
|
||||
either start step 1 (`SYS4INI` parser) or bank momentum with the Frida-free **choices** sub-slice
|
||||
(static-RE opcode hunt) while resolution waits its scheduled turn.
|
||||
A2b-background: **machinery landed**; **steps 1 & 2 SOLVED (static, general).** Step 1 =
|
||||
`build/asset-index.json`. Step 2 = **`resId → files[section_base(scene) + resId]`** via SYS4INI
|
||||
per-scene sections (`tools/resolve_asset.py` + `build/asset-sections.json`) — no runtime capture, works
|
||||
across all archives/types and for audio too. Remaining for the render (step 3): wire a `ResourceMap`
|
||||
(scene → section_base; resId → asset via the index) + Godot `TextureRect` compositing (A2b plan Tasks 3–5,
|
||||
now purely mechanical). Audio (step 4) uses the *same* resolver (`play-bgm/play-voice id → files[base+id]`).
|
||||
|
||||
128
docs/global-memory-re.md
Normal file
128
docs/global-memory-re.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Runtime Global Observation — RE state & starting point
|
||||
|
||||
**Goal.** Read the running game's live VM global variables by their bytecode address (e.g.
|
||||
`G[0x62424]`, `global-string 0x279`). This is the **VM-validation cornerstone**: with it we can
|
||||
compare our VM's global state against the real game's at any point, verify effectful opcodes that
|
||||
have no machine oracle (battle math, flag logic, stat updates), and *name* the ~200 still-unclassified
|
||||
globals by watching them change. The roadmap flags it as the thing that replaces most Frida/Unicorn
|
||||
guesswork once the VM is validated-correct.
|
||||
|
||||
**Status: SHELVED (2026-07-06), deliberately.** We learned the shape of the problem but did not
|
||||
achieve a general "read any global by address" capability. This doc records what we tried, what we
|
||||
proved, the concrete landmarks found, and the *right* way to resume. It is **not** on the critical
|
||||
path for anything else, and — importantly — it does **not** crack the asset-resolution scope selector
|
||||
(see "What this does NOT solve").
|
||||
|
||||
---
|
||||
|
||||
## The core finding: our flat-address model ≠ physical layout
|
||||
|
||||
Our VM models globals as one flat address space (`G[addr]`), and that is **execution-correct**
|
||||
(byte-exact against the dialogue-trace oracle). But it is an *abstraction*. Physically the game's
|
||||
state is **structured and multi-store**:
|
||||
|
||||
- **Multiple stores.** VM int-globals, string-globals, and float-globals are almost certainly
|
||||
separate arrays (their address magnitudes differ wildly: `global-string 0x279` = 633 vs
|
||||
`global-int 0x62424` = 403,492). On top of that, values get **copied into native C++ objects**.
|
||||
- **Structured records, not flat int32.** Entity data (units, party) is stored as records with
|
||||
**inline fixed-size string slots + mixed int fields + native heap pointers** — not a packed int
|
||||
array. A `base + addr*4` scan therefore finds nothing (confirmed: no stride in {2,4,8,12,16}
|
||||
reproduces a known consecutive-address `*INIT` value run).
|
||||
- **Transient vs. stable globals.** A global is a real variable, but *how it's used* varies:
|
||||
- **Transient** (e.g. `G[0x62424]`, the CG resId): a scratch/argument register — `mov G[0x62424]
|
||||
= resId; call load` — set right before a call, overwritten right after. Never holds a stable
|
||||
value. Non-atomic value scans **race** against it and miss it.
|
||||
- **Stable** (e.g. `global-string 0x279`, the player name): persists across play. *These* are
|
||||
findable by value+stability scans; transient ones are not.
|
||||
- **Multiple resId globals.** Backgrounds vs. foreground portraits/sprites appear to use different
|
||||
resId globals / churn `G[0x62424]` between visible background changes (observed live).
|
||||
- **Packed process.** Main VM logic runs from a **per-run heap `r-x` region** (~30 MB, nonstable
|
||||
base) — so the interpreter can't be hooked at a fixed `AGE.EXE+off`. Archives are streamed via a
|
||||
heap block-cache through `ReadFile` (not memory-mapped). See `docs/asset-resolution-re.md`.
|
||||
|
||||
**Why the naive scan failed.** The first attempt scanned for a flat `base+addr*4` int32 array using a
|
||||
signature of `*INIT` constants. It found 0 matches because (a) the layout isn't flat int32, and (b)
|
||||
`*INIT` entity data lives in structured records. The individual values exist in memory but only
|
||||
coincidentally adjacent.
|
||||
|
||||
---
|
||||
|
||||
## What we tried (chronological, with outcomes)
|
||||
|
||||
1. **Flat-int32 signature scan** — `tools/frida/find_globals_base.py` (+ `build/globals-signature.json`,
|
||||
15,481 distinctive `(addr,value)` pairs from `*INIT` `mov (global-int A) IMM`; 215-dword contiguous
|
||||
anchor @`0x631a9`). **Result: 0 hits** at any anchor length, in the opening AND in the first dungeon
|
||||
(ruling out load-timing). Diagnostic: the individual values occur (coincidentally); no stride in
|
||||
{2,4,8,12,16} reproduces the consecutive-address sequence ⇒ **not flat int32**.
|
||||
2. **"Lily" (player name) anchor** — searched for the entered name. Found as ASCII `Lily\0`. **But the
|
||||
context proved it's the wrong anchor:** every copy is embedded in a **native unit-record** (inline
|
||||
string slot + stats + heap pointers), i.e. downstream copies, not the VM string-global store.
|
||||
3. **Differential value scan on `G[0x62424]`** — `tools/frida/find_global_by_sequence.py`. Self-driven
|
||||
by the ReadFile→file_number(=resId) signal: scan for resId at load 1, keep those that become the
|
||||
next resId, etc. **Converged to 3 addresses that tracked 35→37→39→43→46 perfectly — but all were
|
||||
STACK slots** (region `0x18f000`, self-referential + `0x76xxxxxx` return addresses); their values
|
||||
are garbage between loads. They're the per-call argument copies, not the global.
|
||||
4. **Stability filter** (re-read survivors ~1.4 s later, during the pause) — proved the point: 2084
|
||||
locations held resId 35 *stably*, but **0** of them became 37 ⇒ **`G[0x62424]` is transient**, never
|
||||
stable at a value, so neither value-scans nor stability-filters can pin it.
|
||||
|
||||
---
|
||||
|
||||
## Concrete landmarks (for a future run — but note ASLR: these are per-process)
|
||||
|
||||
- **Player name** = `global-string 0x279`; default `"リリィ"` (set in `INPUTNAME.BIN` via
|
||||
`set-string (global-string 0x279)`, then the input op `0x1aa`/`u00425920` writes the entered name to
|
||||
`0x279`, then copied to `local-string 0x135`). **Stable global** — the best future anchor.
|
||||
- **Native unit records** holding the name: ASCII `Lily\0` in 16-byte-ish slots, followed by int stats
|
||||
(level/…, e.g. `0c 0f 04 0f 13 1f`) and native pointers; arrays with stride ~`0x1c`. Region example
|
||||
`0x6e9c000 +0x82000` (rw-, no module). These are **native objects**, not the VM store.
|
||||
- **Transient-arg stack region** where load-arg resId copies appear: ~`0x18f000 +0x11000` (68 KB rw-).
|
||||
- **Large heap regions** (candidate VM stores): 51.8 MB @ `0x2f64000`, plus ~10/9.6/8/6 MB regions.
|
||||
- **Unpacked code (packer)**: 30.3 MB **r-x** @ `0x62411000` (main VM logic; nonstable base per run).
|
||||
- Element size / mapping: **unknown**; it is *not* uniform `base+addr*4` int32.
|
||||
|
||||
## Tools built (kept for resumption)
|
||||
|
||||
All under `tools/frida/` (see `tools/frida/README.md` and `docs/tools-reference.md`):
|
||||
- `find_globals_base.py` — `--build-sig` builds `build/globals-signature.json`; scan mode does the
|
||||
flat-int32 signature scan (robust prefix ladder + verify). *Currently finds nothing → layout isn't flat.*
|
||||
- `find_global_by_sequence.py` — differential resId scan with stability filter. *Finds stack proxies;
|
||||
transient globals elude it.*
|
||||
- `capture_load_order.py`, `locate_resource_load.py`, `capture_resid_args.py` — asset-resolution
|
||||
captures (context: how the loader/decoder chain was found).
|
||||
|
||||
---
|
||||
|
||||
## What this does NOT solve (avoid the trap we fell into)
|
||||
|
||||
Reading `G[0x62424]` live would **not** reveal the asset-resolution **scope selector** (why resId 37 →
|
||||
`EV052CA` and not one of the other 8 fn-37 files). The resId is already free from the ReadFile
|
||||
`file_number`; the scope is *separate native scene state*. So global observation and the scope selector
|
||||
are independent problems — don't chase globals expecting to crack resolution.
|
||||
|
||||
---
|
||||
|
||||
## How to resume properly (recommended plan)
|
||||
|
||||
Heuristic value-scans only ever find *specific, stable* globals one at a time; they give no general
|
||||
`address → memory` mapping. A **guaranteed, general** capability needs the **interpreter's
|
||||
address-resolution logic**. Recommended order:
|
||||
|
||||
1. **Anchor on a STABLE global**, not a transient one. Best: `global-string 0x279` (the name). Find its
|
||||
*VM-store* copy (distinguish from native unit copies: the VM store won't be wrapped in heap
|
||||
pointers). A distinctive name makes the value scan collapse fast.
|
||||
2. **Find the interpreter's global-access function.** Set a hardware/`MemoryAccessMonitor` watchpoint on
|
||||
that stable global's physical location, trigger a bytecode read (e.g. open a menu that draws the
|
||||
name), and **backtrace into the heap interpreter**. That function's address computation *is* the
|
||||
`bytecode-address → physical` mapping — read it rather than guessing the layout.
|
||||
3. **Generalize + verify.** Derive the mapping (likely per-store / per-region), build `read_global(addr)`,
|
||||
and verify against our VM's known state (e.g. read a stable flag whose value our VM predicts).
|
||||
4. **Then** build a runtime global-watch tool for VM validation and effectful-op naming.
|
||||
|
||||
Alternative bootstraps if watchpoints are awkward: snapshot-diff at **quiescent** points (change a
|
||||
known stable global via gameplay, diff memory); or a hardware **write** watchpoint on a stable global's
|
||||
location to catch the interpreter's write path.
|
||||
|
||||
**Bottom line for the next session:** the pieces (signature, tools, landmarks, the name anchor) are in
|
||||
place. Resume from a *stable* anchor and target the *interpreter*, not heuristic scans of transient
|
||||
globals — and only when runtime observation is actually the priority (it isn't blocking other work).
|
||||
@@ -17,6 +17,16 @@
|
||||
- **grounding:** source=inference, confidence=low
|
||||
- **evidence:** confirm via frida
|
||||
|
||||
### 0xbf `play-bgm` (play-bgm, argc 1)
|
||||
- **summary:** Play background music by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (OGG in DATA3). Same resolution as set-texture.
|
||||
- **grounding:** source=frida, confidence=high
|
||||
- **evidence:** Frida capture: `play-bgm 0x5` in SC0000 (section base 0) loaded BGM006.OGG = files[5]. Unified with set-texture resolution rule.
|
||||
|
||||
### 0xc4 `play-voice` (play-voice, argc 1)
|
||||
- **summary:** Play a voice clip by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). Same rule as set-texture/play-bgm.
|
||||
- **grounding:** source=investigation, confidence=med
|
||||
- **evidence:** Section-manifest resolution validated across archives incl. DATA4 voice OGGs (586/595 captured loads); per-clip id->OGG not individually Frida-pinned yet.
|
||||
|
||||
## compute
|
||||
|
||||
### 0x1a2 `resolve-handle?` (u00428010, argc 1)
|
||||
@@ -36,11 +46,26 @@
|
||||
- **grounding:** source=inference, confidence=med
|
||||
- **evidence:** confirm via frida
|
||||
|
||||
### 0x1f8 `create-texture` (create-texture, argc 4)
|
||||
- **summary:** Allocate/prepare a texture slot: (slot, width, height, flag). e.g. `create-texture 0xd 0x190 0x1e 0x0` = slot 13, 400x30.
|
||||
- **grounding:** source=investigation, confidence=med
|
||||
- **evidence:** SC0000 CG/UI-draw path disasm; slot/w/h roles read off the operands (400x30 text bars, etc.).
|
||||
|
||||
### 0x1f9 `set-texture` (set-texture, argc 3)
|
||||
- **summary:** Load asset #resId into texture slot: (resId, slot, flag=-1). resId resolves via the SYS4INI per-scene section manifest: files[section_base(scene)+resId] (same rule for play-bgm/play-voice). See docs/asset-resolution-re.md.
|
||||
- **grounding:** source=frida, confidence=high
|
||||
- **evidence:** SC0000 Frida-confirmed 17/17 (0x25->EV052CA, 0x2e->EV052DB, 0x36->BG030A background); resolution rule validated on 586/595 captured loads. Traced in CG-load subroutine label_12649 as `set-texture G[0x62424] <slot> -1`.
|
||||
|
||||
### 0x1fa `ui-clear?` (u00420480, argc 1)
|
||||
- **summary:** 1 arg (element id); follows 0x1f7 — show/hide/clear UI element by id
|
||||
- **grounding:** source=inference, confidence=med
|
||||
- **evidence:** confirm via frida
|
||||
|
||||
### 0x1fb `draw-texture` (draw-texture, argc 8)
|
||||
- **summary:** Blit a texture slot to screen. Observed 8 args: (handle, slot, srcx, srcy, w, h, dstx, dsty). e.g. `draw-texture 0xcf08 0x3 0 0 0x320 0x258 0 0` = full-screen (800x600) slot 3 at (0,0).
|
||||
- **grounding:** source=investigation, confidence=med
|
||||
- **evidence:** SC0000 CG-load subroutine label_12649: `draw-texture (ptr) (slot) 0 0 (w) (h) (dstx) (dsty)`; full-screen slot-3 draws use 0x320x0x258 (800x600).
|
||||
|
||||
### 0x1ff `draw?` (u00420770, argc 4)
|
||||
- **summary:** 4 args (global+imms); follows 0x217, then call
|
||||
- **grounding:** source=inference, confidence=low
|
||||
@@ -403,10 +428,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xbf `play-bgm` (play-bgm, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=med
|
||||
|
||||
### 0xc0 `u00415620` (u00415620, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
@@ -415,10 +436,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xc4 `play-voice` (play-voice, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=med
|
||||
|
||||
### 0xc5 `u0041D4A0` (u0041D4A0, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
@@ -803,18 +820,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x1f8 `create-texture` (create-texture, argc 4)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=med
|
||||
|
||||
### 0x1f9 `set-texture` (set-texture, argc 3)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=med
|
||||
|
||||
### 0x1fb `draw-texture` (draw-texture, argc 8)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=med
|
||||
|
||||
### 0x1fd `u00420620` (u00420620, argc 4)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
87
docs/tools-reference.md
Normal file
87
docs/tools-reference.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Tools Reference
|
||||
|
||||
Living catalogue of every script in `tools/` — **what it does, how to run it, and what it
|
||||
reads/writes**. This is the operational companion to `docs/PROJECT-STRUCTURE.md` (which is the
|
||||
*where-things-live* map); when they overlap, PROJECT-STRUCTURE owns layout, this file owns
|
||||
usage + I/O. Keep it current: **add a row here whenever you add a tool, and update the row
|
||||
whenever a tool's inputs/outputs change.**
|
||||
|
||||
## Conventions (apply to every tool)
|
||||
|
||||
- **Run with** `py -3.11 -X utf8 tools/<name>.py …` — the `-X utf8` is required on Windows so
|
||||
cp932/Shift-JIS source text renders (and generated files stay UTF-8).
|
||||
- **Paths are never hard-coded.** Every tool imports `tools/paths.py` for `GAME_DIR` /
|
||||
`EXTRACTED` / `DATA1` / `BUILD` / `VM_MAP` / `BIN`. Relocate the tree by editing only that file.
|
||||
- **Generated files are never hand-edited** (they're marked ⚙ below). Edit the source, re-run
|
||||
the generator.
|
||||
- **`build/` and `extracted/` are disposable** — everything under them regenerates from a tool.
|
||||
|
||||
## Path anchor
|
||||
|
||||
| Tool | Purpose | I/O |
|
||||
|---|---|---|
|
||||
| `paths.py` | ★ Single path anchor — derives all workspace dirs from its own location; `paths.scripts()` returns the override-aware `{NAME.BIN → path}` corpus map (loose game-folder patches shadow `extracted/DATA1`). | *Imported, not run.* |
|
||||
|
||||
## Container parse / disassemble
|
||||
|
||||
| Tool | Purpose | Run | Reads → Writes |
|
||||
|---|---|---|---|
|
||||
| `sys4load.py` | Loader + opcode-decoding disassembler for SYS4 `.BIN` scripts (the container-format core every other tool builds on). | `sys4load.py <file.BIN>` · `--summary` · `--strings` · `--json` · `sys4load.py <dir> --validate` (corpus check) | `.BIN` + `age_opcodes*.py` + `build/global-var-map.json` → stdout listing, or `build/scripts-json/` with `--json` |
|
||||
| `age_opcodes.py` | 548-entry Kelebek AGE opcode/arg-type table. **PRISTINE upstream data — never edit.** | *Imported.* | — |
|
||||
|
||||
## Opcode reference toolchain — single source of truth = `vm-map/opcodes.toml`
|
||||
|
||||
All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited **only** in
|
||||
`vm-map/opcodes.toml`. Everything else is generated from it.
|
||||
|
||||
| Tool | Purpose | Run | Reads → Writes |
|
||||
|---|---|---|---|
|
||||
| `opcodes_build.py` | Generator + linter for the opcode reference. | `--build` · `--lint` · `--bootstrap` | `vm-map/opcodes.toml` → ⚙ `tools/age_opcodes_himegari.py`, ⚙ `build/opcodes.json`, ⚙ `docs/opcode-reference.md`, ⚙ `build/opcode-coverage.md` |
|
||||
| `opcodes_model.py` | In-memory model + loader + linter (dangling-ref / confidence-ceiling / vocabulary / dependents). | *Imported by `opcodes_build.py`.* | `vm-map/opcodes.toml` → — |
|
||||
| `test_opcodes.py` | Unit tests for the opcode tooling. | `test_opcodes.py` | — |
|
||||
| `opcode_context.py` | Read-only evidence gatherer for classifying unnamed opcodes (frequency, argc, operand-type signature, neighbours, disasm snippets, Kelebek comment). | `--top 20` · `opcode_context.py 0x1f4 0x71 …` | corpus → stdout |
|
||||
| `validate_opcode_table.py` | Definitive decode-coverage validator (replicates Kelebek's `data_array_end` code/data split). | `validate_opcode_table.py` | corpus → stdout |
|
||||
| `validate_opcode_table_naive.py` | Naïve variant of the above (baseline comparison). | `validate_opcode_table_naive.py` | corpus → stdout |
|
||||
| `age_opcodes_himegari.py` | ⚙ Inferred Himegari opcode semantics — **generated; do not hand-edit.** | *Imported by `sys4load.py`.* | — |
|
||||
|
||||
## Extraction / data corpora
|
||||
|
||||
| Tool | Purpose | Run | Reads → Writes |
|
||||
|---|---|---|---|
|
||||
| `extract_phase2.py` | Batch: disassembly + text corpora for every script. | `extract_phase2.py` | corpus → `build/disasm/*.asm`, `build/text/{dialogue.jsonl,strings.jsonl,*.strings.txt}`, `build/manifest.json` |
|
||||
| `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer shape). | `extract_init.py <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` → `build/data/<OUTNAME>.json` |
|
||||
| `global_map.py` | Build the partial global-variable name map from static evidence. | `global_map.py` | corpus + `build/data/` → `build/global-var-map.{json,md}` |
|
||||
|
||||
## VM
|
||||
|
||||
| Tool | Purpose | Run | Reads → Writes |
|
||||
|---|---|---|---|
|
||||
| `vm0.py` | Headless Python bytecode VM (Phase A0 execution-model prototype; reuses `sys4load`). | `--test` (RECOVER unit test) · `--sweep [N]` (oracle coverage) · `--scene NAME` · `--settex NAME` (set-texture resId trace + exec trace) · `<file.BIN>` | corpus → stdout; `build/vm0-trace.json`; `build/settex-<NAME>.json` |
|
||||
| `correlate_scope.py` | Align the VM's `set-texture(resId)` trace with the game's Frida load order → tag each load's DATA2 package, flag package transitions, dump the significant ops in each transition span (the **scope selector** hunt). | `correlate_scope.py <SCENE>` | `build/settex-<SCENE>.json` + `build/frida-load-order-result.json` + index → stdout |
|
||||
|
||||
## Asset resolution / graphics
|
||||
|
||||
| Tool | Purpose | Run | Reads → Writes |
|
||||
|---|---|---|---|
|
||||
| `parse_sys4ini.py` | Parse `SYS4INI.BIN` (S4IC422, LZSS-compressed) into the authoritative asset index — name ↔ archive ↔ offset ↔ size for all DATA*.ALF (the `resId→file` answer key). | `parse_sys4ini.py [--check]` (`--check` validates vs `extracted/` + `.ALF` sizes) | `姫狩り…/SYS4INI.BIN` → `build/asset-index.json` |
|
||||
| `resolve_asset.py` | ★ **The static asset resolver.** SYS4INI is sectioned (one per scene: `SCxxxx.BIN` + its cross-archive manifest; `file_number` = index within section). Resolves `resId → files[section_base(scene) + resId]` for graphics AND audio, no capture. | `resolve_asset.py --build` · `resolve_asset.py <SCENE> [resId]` | `build/asset-index.json` → `build/asset-sections.json`; resolves any (scene, resId) |
|
||||
| `resolve_frida_reads.py` | Rescue noisy Frida archive-read offsets → asset names via the index (per-archive range search; drops 0x20000 paging reads); recovers the per-scene asset load order. | `resolve_frida_reads.py [reads.log] [-o out.json]` | `build/frida-reads.log` + `build/asset-index.json` → `build/frida-asset-loads.json` |
|
||||
| `convert_agf.py` | Convert named AGF stills to BMP via `AGF2BMP2AGF.exe`. | `convert_agf.py EV001AA.AGF …` | `extracted/DATA2|DATA5/*.AGF` → `build/textures/*.BMP` |
|
||||
|
||||
## Runtime capture (Frida)
|
||||
|
||||
| Tool | Purpose | Run | Reads → Writes |
|
||||
|---|---|---|---|
|
||||
| `tools/frida/capture_graphics.py` | Attach Frida to the running game; log archive reads/opens (ground-truth for asset resolution). See `tools/frida/README.md`. | `py -3.11 -u -X utf8 tools/frida/capture_graphics.py [AGE.EXE]` | running game → `build/frida-reads.log`, `build/frida-opens.log` |
|
||||
| `tools/frida/capture_load_order.py` | **Primary asset-resolution capture:** recover a scene's per-asset load order from exact-start `ReadFile` reads → names via the index; confirms `resId==file_number`. Attach; replay scene; `--analyze`. | `py -3.11 -u -X utf8 tools/frida/capture_load_order.py [pid]` · `--analyze` | running game + index → `build/frida-load-order.jsonl`, `…-result.json` |
|
||||
| `tools/frida/locate_resource_load.py` | Phase-1 locator: back-traces asset-opens to find the native AGF load chain (`0x16d5d7→0x74f1f`). | `py -3.11 -u -X utf8 tools/frida/locate_resource_load.py [pid]` · `--aggregate` | running game → `build/frida-resource-bt.jsonl` |
|
||||
| `tools/frida/capture_resid_args.py` | Phase-2 probe: dumps the decoder's args / context / caller frame (established the loader carries only offsets, not names). | `py -3.11 -u -X utf8 tools/frida/capture_resid_args.py [pid]` · `--analyze` | running game → `build/frida-resid-args.jsonl` |
|
||||
| `tools/frida/find_globals_base.py` | Runtime-global RE (SHELVED — see `docs/global-memory-re.md`): flat-int32 signature scan for the VM global array. Finds nothing → layout isn't flat. | `--build-sig` · `py -3.11 -u -X utf8 tools/frida/find_globals_base.py [pid]` | `*INIT` → `build/globals-signature.json`; scans running game |
|
||||
| `tools/frida/find_global_by_sequence.py` | Runtime-global RE (SHELVED): differential resId value-scan + stability filter. Finds stack proxies; proved `G[0x62424]` is a transient arg-register. | `py -3.11 -u -X utf8 tools/frida/find_global_by_sequence.py [pid]` | running game + index → stdout |
|
||||
|
||||
## Historical / one-off
|
||||
|
||||
| Tool | Purpose |
|
||||
|---|---|
|
||||
| `probe_*.py` (`probe_header`, `probe_leads`, `probe_refs`, `probe_tables`, `probe_tags`, `probe_types`, `probe_xref`) | Container/opcode format-RE probes used to reverse the format originally. Kept for reproducibility; not part of the normal workflow. |
|
||||
| `pack_check.py` | Checks whether `AGE.EXE` is packed (it is). No longer a blocker — we run our own VM. |
|
||||
Reference in New Issue
Block a user