From 9948f23a9704e8cfd8e428e0829fca6b7054c371 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 13:08:48 -0400 Subject: [PATCH] Decode SCINIT scene dispatch registry --- docs/PROJECT-STRUCTURE.md | 2 +- docs/global-reference.md | 7 +- docs/name-resolution.md | 41 +++++--- docs/phase-b-framework.md | 8 +- docs/scjump-progression.md | 20 +++- docs/script-inventory.md | 8 +- docs/tools-reference.md | 4 +- docs/vm-mapping-plan.md | 5 +- tools/extract_init.py | 157 ++++++++++++++++++++++++++++++- tools/init_table_profile.py | 39 +++++++- tools/scjump_decode.py | 10 +- tools/test_extract_init.py | 41 ++++++++ tools/test_init_table_profile.py | 25 +++++ vm-map/globals.toml | 32 ++++++- 14 files changed, 359 insertions(+), 40 deletions(-) diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index 96591e3..c0b42c4 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -79,7 +79,7 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings) │ ├── script-inventory.md what the 481 scripts are │ ├── opcode-reference.md GENERATED from opcodes.toml (human-readable opcode reference) │ ├── global-reference.md GENERATED from globals.toml (human-readable global registry) - │ ├── scjump-progression.md SCJUMP progression decode + native decision→scene boundary + │ ├── scjump-progression.md SCJUMP progression decode + SCINIT decision→scene registry │ └── engine-re.md native-engine RE (Ghidra+MCP): dispatch table, decompiled ops │ ├── build/ DERIVED (our-work-side) — generated by tools/; disposable diff --git a/docs/global-reference.md b/docs/global-reference.md index 76cc011..ec8571c 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5035 globals (210 curated, 4825 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5036 globals (212 curated, 4824 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -11,7 +11,7 @@ | `0x699` | next_script_resource_id | high | investigation | SYSTEM4's computed child-script resource id. On the normal ADV path SYSTEM4 copies G[0x87a57][scjump_decision_out] here, substitutes raw id 0x22 (SC0000.BIN) when zero, executes call-script through this cell at offset 0x477, then clears it after the child returns. | | `0xa68` | — | med | auto-shape | TODO: confirm. Branch-read in 11 scenes / 12 scripts; compared against [0, 1]; writers=['SC0740.BIN', 'SC1580.BIN', 'SC1590.BIN']. | | `0x62ccc` | scjump_decision_out2 | low | inference | Adjacent to scjump_decision_out (0x62ccf) in the 0x62ccc-0x62ccf progression decision-output cluster; same 136-scene reach, written by CAMP/CLOSE/DEBUGADV. INFERENCE from adjacency — confirm meaning before relying on it. | -| `0x62ccf` | scjump_decision_out | low | inference | One of SCJUMP's output/decision globals (progression state machine writes it). Related to chapter_mode. | +| `0x62ccf` | scjump_decision_out | high | investigation | SCJUMP's selected progression decision id. Its chapter/flag decision tree writes one of 847 distinct values across 1,755 sites; SYSTEM4, FIELD, SALLY, and TRAIN use it to index scjump_scene_script_resource_ids and dispatch the corresponding SCxxxx scene script. | ## counter @@ -79,6 +79,8 @@ | `0x837ee` | unit_drop_item_ids | high | investigation | Enemy drop table. Values cross-resolve to ITINIT items (for example treasure puttetto drops bronze/silver/gold coins and planet stones); BTL rolls and awards the rows, while INFOEN displays the possible drops. Columns: 0=drop_1_item_id, 1=drop_2_item_id, 2=drop_3_item_id, 3=drop_4_item_id, 4=drop_5_item_id, 5=drop_6_item_id, 6=drop_7_item_id, 7=drop_8_item_id. | | `0x8572e` | unit_drop_chance_percent | high | investigation | Per-slot drop chance paired with unit_drop_item_ids. BTL draws random-modulo 100 for each populated slot and awards the item when the result is below this value; only columns 0..4 are populated in shipped EBINIT. Columns: 0=drop_1_percent, 1=drop_2_percent, 2=drop_3_percent, 3=drop_4_percent, 4=drop_5_percent, 5=drop_6_percent, 6=drop_7_percent, 7=drop_8_percent. | | `0x8766e` | unit_large_battle_sprite | high | investigation | Presentation flag for 33 large demons, dragons, gods, and their variants. BTL and INFOEN use it to anchor the CB battle figure at the lower screen edge and omit the ordinary-unit framing treatment required by smaller sprites. | +| `0x87a57` | scjump_scene_script_resource_ids | high | investigation | SCINIT's sparse decision-to-script registry. SYSTEM4, FIELD, SALLY, and TRAIN index it by scjump_decision_out, then either call the returned packed resource id or copy it to next_script_resource_id. SCINIT contains 1,209 final decision rows backed by 2,179 source assignments; all 135 distinct packed ids resolve to numbered SC scene scripts. | +| `0x8a167` | scjump_authored_chapters | high | investigation | SCINIT's parallel authored chapter metadata for each scjump_scene_script_resource_ids entry. Source assignments form contiguous chapter 1..9 runs followed by an unassigned -1 run. Of the 847 decision ids currently emitted by SCJUMP, 844 final tags agree with SCJUMP's independently decoded chapter paths; three retained mismatches are legacy/stale metadata. No shipped script reads this array directly. | | `0x8c879` | item_sort_key | high | investigation | ITINIT field for all 287 populated item ids. CHMENU, IMPROVE, and INFOIT pass this array as the primary key to op 0x12f's stable index sort, establishing it as the catalog/display ordering key. The runtime lookup base is one cell before ITINIT's first write because item ids are one-based. | | `0x8cc61` | item_random_tier | high | investigation | ITINIT field for all 287 items. ADDRANDOMITEM and LOSTRANDOMITEM bucket eligible item ids by this value before choosing a random gain/loss; IMPROVE compares it with the current progression rank. This is the random-item availability/rarity tier, distinct from item_category. | | `0x8d049` | item_category | high | investigation | ITINIT field for all 287 items and ITMES's top-level behavior dispatch. Observed groups: 0 innate attacks, 1 key/story items, 2 consumables, 4 stat stones, 8 synthesis materials, 9 coins, 10..17 weapon families, 19 boots, 20 armor, 21 shields, 22 accessories, and 23 capture ropes. | @@ -240,7 +242,6 @@ | `0x665d8` | — | low | auto-shape | array | | `0x665e3` | — | low | auto-shape | array | | `0x66647` | — | low | auto-shape | array | -| `0x87a57` | — | low | auto-shape | array | | `0x8fb41` | — | low | auto-shape | array | | `0xa5eb9` | — | low | auto-shape | array | | `0xaac78` | — | low | auto-shape | array | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index 194e1c6..6503716 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -59,15 +59,12 @@ on disk to read. Resolving it needs one of: **Status: ✅ SOLVED** (see the banner at the top of this section). It did belong with the engine/dispatch work — the Ghidra + MCP loop resolved it via the opcode-dispatch table. -**Update (2026-07-07):** SCJUMP's *decision logic* is now decoded — `(chapter_mode, guards) → -decision value` — see `docs/scjump-progression.md` and `tools/scjump_decode.py`. That confirmed -SCJUMP is not the `call-script` registry (it produces a decision value, not a script id). Then the -Ghidra + MCP loop **cracked `call-script` itself** (the SOLVED banner above): via the opcode-dispatch -table it walked the handler → loader → resolver and found the id is a raw SYS4INI file index. What -remains of the earlier `decision→scene` question is now narrow: scenes are `SCxxxx.BIN` records loaded -through the *same* id-indexed loader, so the only open piece is where the SCJUMP decision *value* -becomes a scene *id* (a caller of SCJUMP). The `u00428010` guess for that hop was disproven via Ghidra -(it's a graphics command-buffer op; see `docs/engine-re.md`). +**Updates (2026-07-07 through 2026-07-23):** SCJUMP's *decision logic* is decoded as +`(chapter_mode, guards) → decision value`; `call-script` ids are raw SYS4INI file indices; and the +remaining join is now closed. SCINIT writes `G[0x87a57 + decision] = packed SCxxxx resource id`, which +SYSTEM4, FIELD, SALLY, and TRAIN consume. Its parallel `G[0x8a167 + decision]` column is authored +chapter metadata. The `u00428010` guess for this hop was disproven via Ghidra: that operation persists +a selected global cell and is unrelated to dispatch. See `docs/scjump-progression.md` for the full join. --- @@ -404,6 +401,23 @@ the raw keys remain provenance. The shipped profile reports 69 titled rules, two working, and persistent globals are now named there as one coherent class-change ABI rather than as unrelated addresses. +### SCINIT scene-dispatch registry (2026-07-23) + +SCINIT is a paired sparse registry, not the malformed 372-row numeric table produced by the generic +numeric heuristic. Each source assignment writes a decision-indexed packed script resource id at +`0x87a57` and an authored chapter tag at the parallel base `0x8a167`, exactly 10,000 cells later. +All 135 distinct resource ids resolve through SYS4INI to `SCxxxx.BIN`. The source contains 2,179 +assignments and 1,209 final decision ids; 710 ids are assigned more than once, so extraction preserves +the complete offset-tagged assignment history as well as final values. + +The chapter meaning is independently supported rather than inferred from the small integer domain: +SCINIT's source order consists of contiguous 1-through-9 runs followed by a `-1` unassigned run, and +844 of the 847 decision ids emitted by decoded SCJUMP end with the same chapter. The three mismatches +are retained as legacy/stale authoring metadata. `extract_init.py` detects this sixth shape as +`dispatch`; `build/data/SCINIT.json` is the single join from decision id to packed resource id, +resolved script name, authored chapter, SCJUMP chapters, overwrite history, and raw column keys. +`field_semantics`/`semantic_fields` resolves those raw keys through `vm-map/globals.toml`. + ### The curated registry — `vm-map/globals.toml` (2026-07-07) The v1 auto map (`build/global-var-map.json`) infers *shapes* but cannot recover branch-flag @@ -448,12 +462,13 @@ are *not* story flags — the miner over-tags them; they are recategorized `unkn The v1 map labels *shapes and tables*; the next increments add *meaning*, cheapest first: -1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, EBINIT, STINIT, and CCINIT now have +1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, EBINIT, STINIT, CCINIT, and SCINIT now have machine-readable investigation surfaces and semantic joins; EBINIT's populated schema is fully named, STINIT's joined object/enemy payloads are decoded, and CCINIT's 71 class-change rules expose predicates - and effects. Inventory RTINIT and SCINIT next, starting with whichever has the clearest repeated - structural boundary and runtime consumers. Preserve explicit joins and do not infer meaning from - column position alone. + and effects. SCINIT closes the progression decision-to-scene join. Take RTINIT next as a separate + sparse multi-table family: its 3,336 writes span several 20,000-cell banks used by movement/battle + routines, so it needs layout segmentation before column naming. Preserve explicit joins and do not + infer meaning from column position alone. 2. **Extend message-table joins beyond the completed ITMES/SKMES pair** (`VIMES`, other id dispatchers, …) and fold in other `set-string`/`copy-to-global` writers not covered by the `*INIT` set. 3. **Label 2D record tables by their readers** — cross-reference which scripts read each diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index 96ebc99..7acf443 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -775,15 +775,17 @@ floor/cap/scaling, weighted selection, and three difficulty-specific movement/ba generated view assembles all 1,378 populated enemy slots across 66 stages, including 485 enemies suppressed after the stage's first clear. -CCINIT is now the fifth extracted shape rather than a failed name-table parse. Its 71 conditional +CCINIT is the fifth extracted shape rather than a failed name-table parse. Its 71 conditional class-change rules cover 33 EBINIT units and expose unit/level/applied-slot predicates plus the selected title, deployment-cost delta, fourteen-stat bonuses, SKINIT skill awards, and state slot set. The shipped program contains 69 named promotions, 30 skill awards, and two level-independent empty-title Lily form-adjustment rules; EVOLVE directly queries those form effects while CALCCC/ADDEXP establish the normal promotion apply/report protocol. The input, working-output, and persistent destination globals are named as one class-change ABI in `vm-map/globals.toml`, and generated rules carry both raw address provenance and -the shared `semantic_fields` join. The next data-semantics inventory target is RTINIT or SCINIT, chosen by -which provides the clearer repeated boundary and consumer evidence. +the shared `semantic_fields` join. SCINIT is now the sixth shape: 2,179 paired assignments produce a +1,209-row SCJUMP decision-to-packed-scene registry plus authored chapter metadata, with all script ids +resolved and the full overwrite history retained. The next data-semantics target is RTINIT, whose 3,336 +writes span several sparse 20,000-cell movement/battle routine banks and first need structural segmentation. Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks: diff --git a/docs/scjump-progression.md b/docs/scjump-progression.md index 66c60e8..0861234 100644 --- a/docs/scjump-progression.md +++ b/docs/scjump-progression.md @@ -39,7 +39,7 @@ choices are not pure story flags. failures). This is the oracle that covers native-gated paths. Run: `py -3.11 -X utf8 tools/scjump_decode.py --verify`. -## The decision→scene boundary (resolved 2026-07-20) +## The decision→scene boundary and SCINIT registry (resolved 2026-07-23) A FIELD snippet does `lookup-array(ptr, 0x5f0ed, 0x62ccf)` then `u00428010(ptr)`, which the spec guessed was the scene resolver. **Correction (resolved 2026-07-20, via Ghidra):** `u00428010` (op `0x1a2`) snapshots that selected global integer array cell into the shared `SAVE.DAT` profile table, keyed by its @@ -49,6 +49,22 @@ dispatch**. The separate boundary is now located in `SYSTEM4.BIN`: on the normal resource-id table rooted at `G[0x87a57]` by `scjump_decision_out` (`G[0x62ccf]`), copies the selected raw SYS4INI id into `G[0x699]`, substitutes `0x22` (`SC0000.BIN`) when the result is zero, and executes `call-script G[0x699]` at SYSTEM4 offset `0x477`. The called scene returns to the still-live SYSTEM4 frame. +FIELD, SALLY, and TRAIN consume the same table for their scene transitions. + +`SCINIT.BIN` is the writer and authoritative static join. Its 2,179 source assignments alternate between +two equally indexed 10,000-cell arrays: + +- `G[0x87a57 + decision]` is the packed SYS4INI script resource id. Its 135 distinct shipped values all + resolve to numbered `SCxxxx.BIN` scripts through `build/callscript-names.json`. +- `G[0x8a167 + decision]` is authored chapter metadata. Source assignments form contiguous runs for + chapters 1 through 9, followed by an unassigned `-1` run. + +Repeated assignments leave 1,209 final decision rows. `tools/extract_init.py SCINIT` preserves every +source-ordered overwrite and emits the final registry to `build/data/SCINIT.json`, including resolved +script names and the independent SCJUMP chapter join. SCJUMP currently emits 847 distinct decision ids; +all have SCINIT rows, and 844 final authored chapter tags agree with the decoded control-flow chapter. +The three mismatches (250, 1001, and 1005) remain explicit as legacy/stale metadata rather than being +silently corrected. The primary resource-id column is the runtime dispatch authority. The captured initial New Game path sets decision zero in `GAMESTART.BIN`; SYSTEM4 consequently takes the `0x22` fallback and enters SC0000. Later decisions use the same mapping/call boundary. See @@ -56,5 +72,5 @@ The captured initial New Game path sets decision zero in `GAMESTART.BIN`; SYSTEM ## See also - `vm-map/globals.toml` — the named globals SCJUMP switches on (chapter_mode, progress counters, flags). -- `name-resolution.md §1` — call-script / native dispatch (the decision→scene boundary lives here too). +- `name-resolution.md §1` — packed call-script ids and the SCINIT decision-to-script join. - `docs/superpowers/specs/2026-07-07-scjump-decision-decode-design.md` — the design. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index b357a10..6ef980f 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -30,7 +30,7 @@ in the root: `SYS4INI.BIN` (272 KB) and `SYS4AB.BIN` (1.08 MB), for 51 root BINs - Numbered `SC0000`–`SC0880` (main chapters, largest files — up to 700 KB) and `SC1000`–`SC1690` (smaller; likely sub-events, endings, appendix content). - `SCJUMP.BIN` (778 KB) is almost certainly the master scene-dispatch/jump table. -- `SCINIT.BIN` (88 KB) initializes scenario state. +- `SCINIT.BIN` (88 KB) initializes the SCJUMP decision-to-scene-script registry and authored chapter tags. --- @@ -89,6 +89,12 @@ stat bonuses, skill awards, and persistent applied-state slot; EVOLVE directly q Lily form rules for movement preview. `extract_init.py` emits this as `rules` mode with raw guards and output addresses joined to EBINIT unit names, SKINIT skill names, and the curated class-change ABI. +SCINIT is a sixth paired-dispatch shape. Its 2,179 alternating writes populate a sparse decision-indexed +script-resource array at `0x87a57` and authored-chapter array at `0x8a167`, leaving 1,209 final rows. +All 135 packed resource ids resolve to numbered SC scripts. The generated view preserves 710 overwritten +decision ids and joins every one of SCJUMP's 847 live decision ids; 844 final chapter tags agree, while +three legacy/stale mismatches remain explicit. + ### Message/string tables (`*MES`) `ITMES` (64 KB — item text), `VIMES` (43 KB), `EIMES` (37 KB), `SKMES` (31 KB — skill text), `CIMES` (15 KB), `MAMES`, `INFOMES`, `MES` — where most translatable text diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 069af0b..348cb5c 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -55,8 +55,8 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited * |---|---|---|---| | `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_message_table.py` | Discover a repeated global-id dispatch chain such as ITMES/SKMES, reconstruct player-facing title/description lines (including furigana surface text and readings), and emit an ID-keyed message table with bytecode provenance. | `extract_message_table.py [OUTNAME]` | `.BIN` → `build/data/.json` | -| `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, statically evaluate direct and negative-value writes, and separate parallel `fields` from linked row-major `record_fields`; when flat global ranges overlap, a parallel base established by other records wins over a coincidental row-table cell calculation. Mixed tables recover selector-dispatched records, condition strings, scalars, preallocated buffer cells, consumer-confirmed strides, and length-prefixed footer arrays; STINIT additionally assembles confirmed buffers into `object_placements` and `enemy_spawns`, joins object type names and available descriptions from OBINIT, and exposes consumer-proven tagged payloads (teleport, treasure, card, initial faction, non-triggering hazard/barrier faction, and OBINIT-gated initial object state), preserves engine-dead type-27 writes under `ignored_payload_fields`, and exposes reinforcement schedules and first-clear-only enemy gates. Rules mode decodes CCINIT's source-ordered class-change predicates and output accumulators, joining EBINIT unit names, SKINIT skill names, titles, named stat bonuses, cost deltas, and applied-state slots. ITINIT and SKINIT join ITMES/SKMES messages. Top-level `field_semantics` maps raw keys to canonical global/column names, while each record's `semantic_fields` is the generated name-keyed join; complete footer copies map to a row and expose its values without the raw provenance wrapper. Raw keys and unresolved tagged payload cells remain provenance. OUTNAME accepts a stem or one `.json` suffix and rejects paths. Refreshes the generated data index. | `extract_init.py [OUTNAME] [--mode …]` | `
.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, matching `.BIN` when supported, and `build/globals.json` → `build/data/.json`, `build/data/README.md` | -| `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric/mixed/rules table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. Name tables also report message coverage; rule tables summarize covered units, titled and level-independent rules, threshold/slot distributions, and skill awards. `--message-query REGEX` shows every matching name/message beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py
[--build] [--limit N] [--message-query REGEX]` | `build/data/
.json` + corpus → stdout; with `--build`, `build/data/
-field-profile.{json,md}` | +| `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules / dispatch shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, statically evaluate direct and negative-value writes, and separate parallel `fields` from linked row-major `record_fields`; when flat global ranges overlap, a parallel base established by other records wins over a coincidental row-table cell calculation. Mixed tables recover selector-dispatched records, condition strings, scalars, preallocated buffer cells, consumer-confirmed strides, and length-prefixed footer arrays; STINIT additionally assembles confirmed buffers into `object_placements` and `enemy_spawns`, joins object type names and available descriptions from OBINIT, and exposes consumer-proven tagged payloads (teleport, treasure, card, initial faction, non-triggering hazard/barrier faction, and OBINIT-gated initial object state), preserves engine-dead type-27 writes under `ignored_payload_fields`, and exposes reinforcement schedules and first-clear-only enemy gates. Rules mode decodes CCINIT's source-ordered class-change predicates and output accumulators, joining EBINIT unit names, SKINIT skill names, titles, named stat bonuses, cost deltas, and applied-state slots. Dispatch mode decodes SCINIT's paired decision-to-packed-scene and authored-chapter arrays, preserving source-order overwrites, resolving SYS4INI script names, and cross-checking chapter tags against decoded SCJUMP paths. ITINIT and SKINIT join ITMES/SKMES messages. Top-level `field_semantics` maps raw keys to canonical global/column names, while each record's `semantic_fields` is the generated name-keyed join; complete footer copies map to a row and expose its values without the raw provenance wrapper. Raw keys and unresolved tagged payload cells remain provenance. OUTNAME accepts a stem or one `.json` suffix and rejects paths. Refreshes the generated data index. | `extract_init.py
[OUTNAME] [--mode …]` | `
.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, `build/callscript-names.json` and `build/scjump-decisions.json` for SCINIT, matching `.BIN` when supported, and `build/globals.json` → `build/data/.json`, `build/data/README.md` | +| `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric/mixed/rules/dispatch table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. Name tables also report message coverage; rule tables summarize covered units, titled and level-independent rules, threshold/slot distributions, and skill awards; dispatch tables summarize assignments, overwrites, script-name resolution, and SCJUMP chapter agreement. `--message-query REGEX` shows every matching name/message beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py
[--build] [--limit N] [--message-query REGEX]` | `build/data/
.json` + corpus → stdout; with `--build`, `build/data/
-field-profile.{json,md}` | | `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based, mixed selector-dispatched, and CCINIT rule extraction; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; and field/message/rule profiling. | run each directly | — | | `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}` | diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index eccbf22..47edc25 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -53,11 +53,12 @@ - [x] **2.0 — Project structure.** Established `docs/`, `build/{disasm,text,data,scripts-json}/`, `godot/`; game install stays read-only in place. Also relaxed the loader magic check to the `SYS4` family (`SYS4424` patch scripts now parse — was silently skipping 5 scripts). - [x] **2.1 — Text corpora.** `tools/extract_phase2.py` → 481/481 scripts: full disassembly (`build/disasm/*.asm`), per-script strings, `build/text/dialogue.jsonl` (**30,057 show-text lines** — the translation corpus), `build/text/strings.jsonl` (38,449 strings tagged by source opcode), `build/manifest.json`. -- [x] **2.2 — `*INIT` data and rule sources → JSON.** `tools/extract_init.py` auto-detects shape (`name`/`numeric`/`footer`/`mixed`/`rules`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions)** [name: sparse one-based name/description/fields], **CGINIT (379 CG entries)** [numeric: index-keyed columns], **MPINIT (1472 map records)** [footer: length-prefixed arrays], **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays], and **CCINIT (71 class-change rules over 33 units)** [rules: unit/level/state predicates plus title/cost/stat/skill effects]. Validated; see `build/data/README.md`. Raw addresses remain bytecode provenance; confirmed semantics come from `vm-map/globals.toml`. +- [x] **2.2 — `*INIT` data and rule sources → JSON.** `tools/extract_init.py` auto-detects shape (`name`/`numeric`/`footer`/`mixed`/`rules`/`dispatch`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions)** [name: sparse one-based name/description/fields], **CGINIT (379 CG entries)** [numeric: index-keyed columns], **MPINIT (1472 map records)** [footer: length-prefixed arrays], **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays], **CCINIT (71 class-change rules over 33 units)** [rules: unit/level/state predicates plus title/cost/stat/skill effects], and **SCINIT (1,209 final decision rows)** [dispatch: scene resource plus authored chapter metadata and overwrite history]. Validated; see `build/data/README.md`. Raw addresses remain bytecode provenance; confirmed semantics come from `vm-map/globals.toml`. - [x] **2.3 — Extract `STINIT`'s 74 sparse stage records.** The mixed mode identifies the dominant `scjump_progress_a` dispatch, recovers 29 preallocated buffer layouts (including six consumer-confirmed row strides), and keeps four condition strings, six scalars, fixed-buffer writes, and all 1,396 footer-array copies separated by stage id. Profiles supply population/value and direct-consumer evidence. Header/map semantics cover conditions, BGM, turn/replay/clear-reward settings, and map texture overrides. The object join assembles 2,312 placements with type/position/gates, OBINIT names and available descriptions, 604 reinforcement schedules, and typed initial-faction, teleport, treasure, card-list, non-triggering-faction, and initial-object-state payloads; FIELD's dedicated special-spawn path proves the final three populated type-27 tagged writes are engine-dead, so they remain visible as ignored provenance rather than unresolved semantics. The 30-cell enemy family contributes 1,378 joined spawns with unit/faction, placement, difficulty/story gates, level scaling, weighted selection, difficulty-specific movement/battle routine sets, and 485 first-clear-only gates. Raw address views remain alongside generated `semantic_fields`. - [x] **2.3a — Extract `CCINIT`'s 71 class-change rules.** Rules mode preserves source-order guards and detects the unit id, minimum level, clear applied-state slot, selected title/level, deployment-cost delta, named fourteen-stat bonuses, awarded SKINIT skills, and state slot set by each rule. EBINIT/SKINIT definition joins and the common global semantic projection coexist with raw addresses. CALCCC/ADDEXP establish the apply/report ABI; EVOLVE explains the two level-independent empty-title Lily rules. The generated profile covers 33 units, 69 titled rules, 30 skill awards, three used promotion slots, and 19 populated output fields. +- [x] **2.3b — Extract `SCINIT`'s scene-dispatch registry.** Dispatch mode recognizes 2,179 alternating writes to two 10,000-cell arrays, preserves all 710 overwritten decision ids, and emits 1,209 final rows. The primary column maps decisions to 135 packed SYS4INI ids, all resolved to numbered SC scripts; the parallel column is authored chapter metadata. All 847 live SCJUMP decisions join to rows and 844 final chapter tags agree with the independently decoded paths; three legacy/stale mismatches remain explicit. - [x] **2.4 — Partial global-var map BUILT + wired into the disassembler.** `tools/global_map.py` → `build/global-var-map.{json,md}` (16,354/49,435 globals labelled: string tables, `*INIT` field arrays, 122 record tables w/ strides, current-entity index pointers). `sys4load` renders the labels inline (`=rec[s30]`, `=current-entity-index?`). See `docs/name-resolution.md`. -- [ ] **2.5 — Grow the global-var map (future, incremental).** Static first: fold in `*MES` writers; label 2D record tables by their reader scripts. Then Frida to name *which stat* each field is. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Also deferred: `call-script` id→name resolution (engine-level — SCJUMP.BIN decode or Frida; see `docs/name-resolution.md` #1). +- [ ] **2.5 — Grow the global-var map (future, incremental).** Static first: segment RTINIT's sparse multi-table banks, fold in remaining `*MES` writers, and label 2D record tables by their reader scripts. Then Frida to name *which stat* each unresolved field is. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids and SCJUMP decision-to-scene dispatch are resolved; see `docs/name-resolution.md` #1 and `docs/scjump-progression.md`. ## Phase 3 — Name the unnamed opcodes *(top ~20 BEFORE Phase 4; the rest on demand)* diff --git a/tools/extract_init.py b/tools/extract_init.py index 82e8e6b..71494db 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Extract a *INIT data table to JSON. Auto-detects the table's shape. -*INIT scripts populate global arrays and work buffers with static game data. Five shapes seen: +*INIT scripts populate global arrays and work buffers with static game data. Six shapes seen: name — records keyed by a name string. Each record: set-string(name), static field writes, set-string(desc). Arrays indexed by record id in lockstep (+1/record). @@ -14,12 +14,14 @@ footer arrays for one runtime record. (STINIT stages) rules — conditional blocks select a unit promotion and add effects to shared output buffers. (CCINIT class changes) + dispatch—paired parallel arrays map a sparse decision id to a packed script resource id + and authored chapter metadata. (SCINIT scene dispatch) Records are {id, name?, desc?, fields:{"0x": value}} or, for footer tables, {id, global_addr, footer_off, values:[...]}. Column addresses are raw engine globals; confirmed names come from the generated engine global registry while raw keys remain provenance. -Usage: py -3.11 -X utf8 tools/extract_init.py
[OUTNAME] [--mode name|numeric|footer|mixed|rules] +Usage: py -3.11 -X utf8 tools/extract_init.py
[OUTNAME] [--mode name|numeric|footer|mixed|rules|dispatch] """ from __future__ import annotations import json @@ -165,6 +167,27 @@ def _class_change_guards(scr) -> list[dict]: return guards +def _paired_parallel_writes(scr) -> tuple[list[tuple], int] | None: + """Recognize alternating writes to two equally indexed parallel arrays.""" + writes = [] + for ins in scr.instructions: + write = _static_global_write(ins) + if write is not None and isinstance(write[1], int): + writes.append((ins.offset, *write)) + elif sys4load.display_label(ins.opcode) != "exit": + return None + if len(writes) < 200 or len(writes) % 2: + return None + span = writes[1][1] - writes[0][1] + if span <= 0: + return None + for index in range(0, len(writes), 2): + primary, secondary = writes[index:index + 2] + if secondary[1] - primary[1] != span: + return None + return writes, span + + def detect_mode(scr): ops = [ins.opcode for ins in scr.instructions] has_str = any(ins.opcode == SET_STRING and ins.args and ins.args[0][0] == T_GLOBAL_STRING @@ -181,6 +204,8 @@ def detect_mode(scr): return "mixed" if has_str: return "name" + if _paired_parallel_writes(scr): + return "dispatch" n_footer = ops.count(COPY_LOCAL_ARRAY) n_int = ops.count(MOV) + ops.count(COPY_TO_GLOBAL) return "footer" if n_footer >= max(4, n_int) else "numeric" @@ -676,6 +701,128 @@ def extract_numeric(scr): return records, {"primary_index_base": f"0x{base:x}", "record_span": n} +@cache +def callscript_names() -> dict[int, str]: + """Load the generated packed script-resource id join.""" + try: + data = json.loads( + (paths.BUILD / "callscript-names.json").read_text(encoding="utf8") + ) + except (OSError, json.JSONDecodeError): + return {} + return {int(key): value for key, value in data.items()} + + +@cache +def scjump_decision_chapters() -> tuple[dict[int, set[int]], int]: + """Load SCJUMP's generated decision sites as independent correlation evidence.""" + try: + data = json.loads( + (paths.BUILD / "scjump-decisions.json").read_text(encoding="utf8") + ) + except (OSError, json.JSONDecodeError): + return {}, 0 + chapters: dict[int, set[int]] = {} + for decision in data.get("decisions", []): + chapter = decision.get("chapter") + if isinstance(chapter, int): + chapters.setdefault(decision["decision"], set()).add(chapter) + return chapters, len(data.get("decisions", [])) + + +def extract_dispatch(scr): + """Extract SCINIT's decision -> scene-script registry without losing overwrites.""" + paired = _paired_parallel_writes(scr) + if paired is None: + return [], {} + writes, span = paired + primary_base = writes[0][1] + chapter_base = primary_base + span + names = callscript_names() + scjump_chapters, decision_site_count = scjump_decision_chapters() + records_by_id: dict[int, dict] = {} + assignment_count = 0 + + for index in range(0, len(writes), 2): + primary, chapter = writes[index:index + 2] + decision_id = primary[1] - primary_base + script_resource_id = primary[2] + assignment = { + "offset": f"0x{primary[0]:x}", + "script_resource_id": script_resource_id, + "script_name": names.get(script_resource_id, ""), + "authored_chapter": chapter[2], + } + record = records_by_id.setdefault(decision_id, { + "id": decision_id, + "assignments": [], + }) + record["assignments"].append(assignment) + assignment_count += 1 + + chapter_match_count = 0 + chapter_mismatches = [] + resolved_script_count = 0 + overwritten_record_count = 0 + conflicting_chapter_record_count = 0 + for decision_id, record in records_by_id.items(): + assignments = record["assignments"] + final = assignments[-1] + script_resource_id = final["script_resource_id"] + authored_chapter = final["authored_chapter"] + record.update({ + "name": final["script_name"], + "script_resource_id": script_resource_id, + "script_name": final["script_name"], + "authored_chapter": authored_chapter, + "assignment_count": len(assignments), + "fields": { + f"0x{primary_base:x}": script_resource_id, + f"0x{chapter_base:x}": authored_chapter, + }, + }) + if final["script_name"]: + resolved_script_count += 1 + if len(assignments) > 1: + overwritten_record_count += 1 + if len({assignment["authored_chapter"] for assignment in assignments}) > 1: + conflicting_chapter_record_count += 1 + if decision_id in scjump_chapters: + expected = sorted(scjump_chapters[decision_id]) + record["scjump_chapters"] = expected + matches = authored_chapter in scjump_chapters[decision_id] + record["authored_chapter_matches_scjump"] = matches + if matches: + chapter_match_count += 1 + else: + chapter_mismatches.append({ + "decision_id": decision_id, + "authored_chapter": authored_chapter, + "scjump_chapters": expected, + }) + + records = [records_by_id[key] for key in sorted(records_by_id)] + return records, { + "selector_global": "0x62ccf", + "script_resource_array_base": f"0x{primary_base:x}", + "authored_chapter_array_base": f"0x{chapter_base:x}", + "reserved_array_span": span, + "assignment_count": assignment_count, + "overwritten_record_count": overwritten_record_count, + "conflicting_chapter_record_count": conflicting_chapter_record_count, + "resolved_script_count": resolved_script_count, + "scjump_decision_site_count": decision_site_count, + "scjump_distinct_decision_count": len(scjump_chapters), + "scjump_joined_record_count": sum( + record["id"] in scjump_chapters for record in records + ), + "scjump_chapter_match_count": chapter_match_count, + "scjump_chapter_mismatches": sorted( + chapter_mismatches, key=lambda row: row["decision_id"] + ), + } + + def extract_footer(scr): records = [] for i, ins in enumerate(scr.instructions): @@ -1020,6 +1167,11 @@ def write_data_index(data_dir: Path) -> None: "eligibility, titles, deployment-cost and named stat deltas, awarded SKINIT skills, and", "the persistent state slot set by each class change. Raw output addresses remain beside", "the joined EBINIT unit and SKINIT skill names.", + "", + "Dispatch-mode tables preserve SCINIT's complete source-ordered assignment history", + "while exposing the final sparse decision-id registry. Packed resource ids join to", + "SYS4INI script names, authored chapter tags correlate with SCJUMP's decoded decision", + "sites, and legacy/stale chapter mismatches remain explicit.", "Use `tools/init_table_profile.py
--build` to generate value/population and", "direct-consumer evidence.", "", @@ -1059,6 +1211,7 @@ def main() -> int: "footer": extract_footer, "mixed": extract_mixed, "rules": extract_class_change_rules, + "dispatch": extract_dispatch, }[mode] recs, meta = extractor(scr) if mode == "name" and name in MESSAGE_TABLES: diff --git a/tools/init_table_profile.py b/tools/init_table_profile.py index d285465..8b327f2 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -37,7 +37,7 @@ def load_table(name: str) -> dict: if not path.exists(): raise SystemExit(f"missing extracted table: {path}") data = json.loads(path.read_text(encoding="utf8")) - if data.get("mode") not in {"name", "numeric", "mixed", "rules"}: + if data.get("mode") not in {"name", "numeric", "mixed", "rules", "dispatch"}: raise SystemExit(f"{name}: unsupported field-profiling mode {data.get('mode')!r}") return data @@ -86,6 +86,7 @@ def profile_columns(data: dict) -> list[dict]: "kind": ( "scalar-field" if data.get("mode") == "mixed" else "rule-output" if data.get("mode") == "rules" + else "dispatch-field" if data.get("mode") == "dispatch" else "parallel-array" ), "base": key, @@ -185,6 +186,25 @@ def profile_rules(data: dict) -> dict: } +def profile_dispatch(data: dict) -> dict: + """Summarize SCINIT's final registry and preserved assignment history.""" + if data.get("mode") != "dispatch": + return {} + return { + "assignment_count": data.get("assignment_count", 0), + "overwritten_record_count": data.get("overwritten_record_count", 0), + "conflicting_chapter_record_count": data.get( + "conflicting_chapter_record_count", 0 + ), + "resolved_script_count": data.get("resolved_script_count", 0), + "scjump_joined_record_count": data.get("scjump_joined_record_count", 0), + "scjump_chapter_match_count": data.get("scjump_chapter_match_count", 0), + "scjump_chapter_mismatch_count": len( + data.get("scjump_chapter_mismatches", []) + ), + } + + def profile_messages(data: dict) -> dict: """Summarize the joined player-facing message evidence.""" records = data["records"] @@ -307,6 +327,21 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str: f"- level-independent rules: {rule_profile['level_independent_rule_count']}", f"- awarded skills: {rule_profile['skill_award_count']}", ]) + elif dispatch_profile := profile_dispatch(data): + lines.extend([ + f"- source assignments: {dispatch_profile['assignment_count']}", + f"- overwritten decision ids: {dispatch_profile['overwritten_record_count']}", + f"- ids whose assignment history crosses chapter tags: " + f"{dispatch_profile['conflicting_chapter_record_count']}", + f"- packed script ids resolved: {dispatch_profile['resolved_script_count']}/" + f"{data['record_count']}", + f"- SCJUMP decisions joined: {dispatch_profile['scjump_joined_record_count']}", + f"- final authored chapters matching SCJUMP: " + f"{dispatch_profile['scjump_chapter_match_count']}/" + f"{dispatch_profile['scjump_joined_record_count']}", + f"- explicit chapter mismatches: " + f"{dispatch_profile['scjump_chapter_mismatch_count']}", + ]) else: lines.extend([ f"- player-facing messages: {message_profile['population']}/{data['record_count']} " @@ -367,8 +402,10 @@ def main() -> int: "array_cell_count": sum(row["kind"] == "array-cell" for row in rows), "footer_array_count": sum(row["kind"] == "footer-array" for row in rows), "rule_output_count": sum(row["kind"] == "rule-output" for row in rows), + "dispatch_field_count": sum(row["kind"] == "dispatch-field" for row in rows), "message_profile": messages, "rule_profile": profile_rules(data), + "dispatch_profile": profile_dispatch(data), "columns": sorted(rows, key=lambda row: ( int(row["base"], 16), row["stride"] or 0, row["column"] or 0 )), diff --git a/tools/scjump_decode.py b/tools/scjump_decode.py index 9d33403..b819176 100644 --- a/tools/scjump_decode.py +++ b/tools/scjump_decode.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Static decoder for SCJUMP.BIN's progression decision logic. Guarded DFS over its acyclic CFG extracts, per decision site, the (chapter_mode, path-condition guards) -> decision value rule. -Decision->scene resolution is native (u00428010) and out of scope. See -docs/superpowers/specs/2026-07-07-scjump-decision-decode-design.md. +Decision->scene resolution is the SCINIT registry at G[0x87a57], consumed by +SYSTEM4 and related scripts. See docs/scjump-progression.md. (no flag) -> build/scjump-decisions.json + build/scjump-decisions.md --verify synthesize a witness per decision, run SCJUMP in vm0, assert emitted 0x62ccf matches""" from __future__ import annotations @@ -187,7 +187,7 @@ def emit_json(decs, names) -> str: by_chapter = collections.defaultdict(list) for d in out_decs: by_chapter[d["chapter"]].append(d["decision"]) - meta = {"note": "GENERATED by tools/scjump_decode.py — do not edit. Decision->scene is native (u00428010), not resolved here.", + meta = {"note": "GENERATED by tools/scjump_decode.py; do not edit. Join decision ids to build/data/SCINIT.json for scene script resources and authored chapter metadata.", "decision_sites": len({d["site_offset"] for d in decs}), "distinct_decisions": len({d["decision"] for d in decs})} return json.dumps({"meta": meta, "decisions": out_decs, @@ -199,8 +199,8 @@ def emit_md(decs, names) -> str: L = ["", "# SCJUMP progression decisions (generated)", "", f"{len({d['site_offset'] for d in decs})} decision sites. Each rule: guards (all true along the " - "path) -> decision value written to `0x62ccf`. Decision->scene is native (`u00428010`), see " - "`docs/scjump-progression.md`.", ""] + "path) -> decision value written to `0x62ccf`. `build/data/SCINIT.json` joins decisions " + "to scene scripts and authored chapters; see `docs/scjump-progression.md`.", ""] by_ch = collections.defaultdict(list) for d in decs: by_ch[d["chapter"]].append(d) diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 14fe27f..9f50d59 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -309,6 +309,46 @@ def test_real_class_change_rules() -> None: "CCINIT accounts for all 30 awarded skills") +def test_real_scene_dispatch() -> None: + script = sys4load.load(extract_init.resolve("SCINIT")) + check(extract_init.detect_mode(script) == "dispatch", + "SCINIT auto-detects as paired scene dispatch arrays") + check( + extract_init.detect_mode( + sys4load.load(extract_init.resolve("RTINIT")) + ) == "numeric", + "RTINIT's multi-table writes do not false-positive as paired dispatch", + ) + records, meta = extract_init.extract_dispatch(script) + by_id = {record["id"]: record for record in records} + check(len(records) == 1209 and meta["assignment_count"] == 2179, + "SCINIT preserves all assignments and 1,209 final decision rows") + check(meta["script_resource_array_base"] == "0x87a57" + and meta["authored_chapter_array_base"] == "0x8a167" + and meta["reserved_array_span"] == 10000, + "SCINIT exposes its paired 10,000-cell array layout") + check(by_id[0]["script_resource_id"] == 34 + and by_id[0]["script_name"] == "SC0000.BIN" + and by_id[0]["authored_chapter"] == 1, + "SCINIT joins packed resource ids to scene names and chapter metadata") + check(by_id[1]["assignment_count"] == 3 + and [entry["authored_chapter"] for entry in by_id[1]["assignments"]] + == [1, 5, 5], + "SCINIT retains source-ordered overwrites rather than only the final cell") + check(meta["resolved_script_count"] == 1209 + and len({record["script_resource_id"] for record in records}) == 135, + "every final SCINIT row resolves to one of 135 numbered scene scripts") + check(meta["scjump_joined_record_count"] == 847 + and meta["scjump_chapter_match_count"] == 844 + and [row["decision_id"] for row in meta["scjump_chapter_mismatches"]] + == [250, 1001, 1005], + "SCINIT chapter tags cross-check against every live SCJUMP decision") + semantics = extract_init.field_semantics(records) + check(semantics["0x87a57"] == "scjump_scene_script_resource_ids" + and semantics["0x8a167"] == "scjump_authored_chapters", + "SCINIT's paired columns join to canonical semantic names") + + def test_real_message_tables() -> None: scripts = paths.scripts() expected = { @@ -483,6 +523,7 @@ if __name__ == "__main__": test_output_name_validation() test_real_mixed_table() test_real_class_change_rules() + test_real_scene_dispatch() test_real_message_tables() test_message_join() test_field_semantics() diff --git a/tools/test_init_table_profile.py b/tools/test_init_table_profile.py index 22815e6..fc28257 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -95,6 +95,31 @@ def main() -> int: assert rule_summary["class_change_slot_indices"] == {"0": 1, "1": 1} assert rule_summary["skill_award_count"] == 1 + dispatch_fixture = { + "table": "DISPATCH", + "mode": "dispatch", + "assignment_count": 4, + "overwritten_record_count": 1, + "conflicting_chapter_record_count": 1, + "resolved_script_count": 2, + "scjump_joined_record_count": 2, + "scjump_chapter_match_count": 1, + "scjump_chapter_mismatches": [{"decision_id": 2}], + "records": [ + {"id": 1, "name": "SC0000.BIN", "fields": {"0x100": 34, "0x200": 1}}, + {"id": 2, "name": "SC0010.BIN", "fields": {"0x100": 286, "0x200": 2}}, + ], + } + dispatch_rows = { + row["key"]: row for row in profile.profile_columns(dispatch_fixture) + } + assert dispatch_rows["0x100"]["kind"] == "dispatch-field" + dispatch_summary = profile.profile_dispatch(dispatch_fixture) + assert dispatch_summary["assignment_count"] == 4 + assert dispatch_summary["overwritten_record_count"] == 1 + assert dispatch_summary["scjump_chapter_match_count"] == 1 + assert dispatch_summary["scjump_chapter_mismatch_count"] == 1 + messages = profile.profile_messages(fixture) assert messages["population"] == 1 assert messages["coverage"] == 1 / 3 diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 4106750..5c7e340 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -800,6 +800,28 @@ source = "investigation" confidence = "high" depends_on = ["0x62ccf"] +[[global]] +address = "0x87a57" +name = "scjump_scene_script_resource_ids" +category = "data-table" +type = "int[10000]" +value_domain = "packed SYS4INI script resource id; shipped SCINIT values resolve to SCxxxx.BIN" +usage = "SCINIT's sparse decision-to-script registry. SYSTEM4, FIELD, SALLY, and TRAIN index it by scjump_decision_out, then either call the returned packed resource id or copy it to next_script_resource_id. SCINIT contains 1,209 final decision rows backed by 2,179 source assignments; all 135 distinct packed ids resolve to numbered SC scene scripts." +source = "investigation" +confidence = "high" +depends_on = ["0x62ccf", "0x699", "0x8a167"] + +[[global]] +address = "0x8a167" +name = "scjump_authored_chapters" +category = "data-table" +type = "int[10000]" +value_domain = "-1 or chapter 1..9" +usage = "SCINIT's parallel authored chapter metadata for each scjump_scene_script_resource_ids entry. Source assignments form contiguous chapter 1..9 runs followed by an unassigned -1 run. Of the 847 decision ids currently emitted by SCJUMP, 844 final tags agree with SCJUMP's independently decoded chapter paths; three retained mismatches are legacy/stale metadata. No shipped script reads this array directly." +source = "investigation" +confidence = "high" +depends_on = ["0x87a57", "0x3234", "0x62ccf"] + [[global]] address = "0x3234" name = "chapter_mode" @@ -937,11 +959,11 @@ address = "0x62ccf" name = "scjump_decision_out" category = "choice-output" type = "int" -value_domain = "?" -usage = "One of SCJUMP's output/decision globals (progression state machine writes it). Related to chapter_mode." -source = "inference" -confidence = "low" -depends_on = [] +value_domain = "0..1693 in shipped SCJUMP" +usage = "SCJUMP's selected progression decision id. Its chapter/flag decision tree writes one of 847 distinct values across 1,755 sites; SYSTEM4, FIELD, SALLY, and TRAIN use it to index scjump_scene_script_resource_ids and dispatch the corresponding SCxxxx scene script." +source = "investigation" +confidence = "high" +depends_on = ["0x3234", "0x87a57"] [[global]] address = "0xeff75"