Decode STINIT initial object states

This commit is contained in:
gamer147
2026-07-23 09:47:22 -04:00
parent 229c77855a
commit d1565d8e18
9 changed files with 98 additions and 30 deletions

View File

@@ -1,7 +1,7 @@
<!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build --> <!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build -->
# Global Variable Reference (generated) # Global Variable Reference (generated)
5030 globals (187 curated, 4843 auto shape-inferred). Source of truth: `vm-map/globals.toml`. 5030 globals (189 curated, 4841 auto shape-inferred). Source of truth: `vm-map/globals.toml`.
## choice-output ## choice-output
@@ -32,6 +32,7 @@
| `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. | | `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. |
| `0x67a0` | party_slot_character_id | high | investigation | Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster. | | `0x67a0` | party_slot_character_id | high | investigation | Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster. |
| `0x7684` | stage_clear_state | high | investigation | Persistent per-stage completion state indexed by stage id. STAGECLEAR sets the current stage cell to 1, while FORT, SELSTAGE, and FIELD use zero versus one to distinguish an uncleared mission from a cleared replay. | | `0x7684` | stage_clear_state | high | investigation | Persistent per-stage completion state indexed by stage id. STAGECLEAR sets the current stage cell to 1, while FORT, SELSTAGE, and FIELD use zero versus one to distinguish an uncleared mission from a cleared replay. |
| `0x141a4` | stage_object_runtime_state | high | investigation | Per-stage, per-object-slot runtime state. FIELD initializes mode-1 objects from STINIT's first tagged payload on a fresh stage and updates capturable-object ownership; DRAWOBJ uses it as the vertical sprite-row index. Other consumers interpret it by object type. |
| `0x5660b` | skill_info_revealed_flags | high | investigation | Persistent skill-information visibility flags. ADDSKILL sets the selected skill, BTL marks every equipped skill when it is observed in combat, and INFOIT suppresses a skill's icon/handler-driven details until this flag is nonzero. This is broader than skill_acquired_flags. | | `0x5660b` | skill_info_revealed_flags | high | investigation | Persistent skill-information visibility flags. ADDSKILL sets the selected skill, BTL marks every equipped skill when it is observed in combat, and INFOIT suppresses a skill's icon/handler-driven details until this flag is nonzero. This is broader than skill_acquired_flags. |
| `0x66716` | unit_voice_asset_ids | high | investigation | EBINIT per-unit voice bank for 116 voiced characters and variants. Resolving the values through SYS4INI yields character OGG clips (for example Lily's row is LILA1381..1406); BTL and FIELD select mostly unresolved battle/map reaction slots, while SHOWGROW proves column 24 is the level-up reaction. Columns: 24=level_up. | | `0x66716` | unit_voice_asset_ids | high | investigation | EBINIT per-unit voice bank for 116 voiced characters and variants. Resolving the values through SYS4INI yields character OGG clips (for example Lily's row is LILA1381..1406); BTL and FIELD select mostly unresolved battle/map reaction slots, while SHOWGROW proves column 24 is the level-up reaction. Columns: 24=level_up. |
| `0x6dc46` | unit_status_art_asset_ids | high | investigation | Three-variant status/menu illustration table for 25 principal characters. The ids resolve to 456x420 CS character art; DRAWCHP selects column 0 above 50% HP, column 1 at 26..50%, and column 2 at 25% or below, then loads the art into the status-panel texture slot. Columns: 0=healthy, 1=wounded, 2=critical. | | `0x6dc46` | unit_status_art_asset_ids | high | investigation | Three-variant status/menu illustration table for 25 principal characters. The ids resolve to 456x420 CS character art; DRAWCHP selects column 0 above 50% HP, column 1 at 26..50%, and column 2 at 25% or below, then loads the art into the status-panel texture slot. Columns: 0=healthy, 1=wounded, 2=critical. |
@@ -103,6 +104,7 @@
| `0xaa8f2` | skill_proc_chance_percent | high | investigation | Probability for 14 passive skills. CALCDMG compares random-modulo 100 against this value; examples include Re-action 20, Double Action 100, Counter 10, and Resurrection 50. | | `0xaa8f2` | skill_proc_chance_percent | high | investigation | Probability for 14 passive skills. CALCDMG compares random-modulo 100 against this value; examples include Re-action 20, Double Action 100, Counter 10, and Resurrection 50. |
| `0xaaa1e` | skill_battle_animation_id | high | investigation | Populated for 101 combat skills. BTL and CALCDMG place this value in the battle-animation selector before calling BTANINIT; most skills reuse their own id, while related skills deliberately share an animation and passive reactions use ids 801..808. | | `0xaaa1e` | skill_battle_animation_id | high | investigation | Populated for 101 combat skills. BTL and CALCDMG place this value in the battle-animation selector before calling BTANINIT; most skills reuse their own id, while related skills deliberately share an animation and passive reactions use ids 801..808. |
| `0xaab4a` | skill_handler_script_id | high | investigation | SKINIT field for all 131 skills. CHMENU and INFOIT look it up and pass it directly to call-script; packed id 0x31ca resolves to SKMES.BIN, the shared per-skill text/behavior dispatcher. | | `0xaab4a` | skill_handler_script_id | high | investigation | SKINIT field for all 131 skills. CHMENU and INFOIT look it up and pass it directly to call-script; packed id 0x31ca resolves to SKMES.BIN, the shared per-skill text/behavior dispatcher. |
| `0xe6dee` | object_sprite_state_row_mode | high | investigation | OBINIT metadata indexed by object type. FIELD copies the type-tagged initial payload into stage_object_runtime_state only for mode 1; DRAWOBJ likewise multiplies that runtime state by the object sprite height to select source Y only for mode 1. |
| `0xe7302` | stage_bgm_id | high | investigation | STINIT's per-stage scalar loaded for all 74 records. FIELD passes the value directly to play-bgm when starting the stage. | | `0xe7302` | stage_bgm_id | high | investigation | STINIT's per-stage scalar loaded for all 74 records. FIELD passes the value directly to play-bgm when starting the stage. |
| `0xe7303` | stage_target_clear_turns | high | investigation | STINIT's target/par turn count. STAGECLEAR divides elapsed turns by this value to derive a performance multiplier; when replaying an already-cleared ordinary stage, FIELD also uses it as the forced-retreat turn limit. | | `0xe7303` | stage_target_clear_turns | high | investigation | STINIT's target/par turn count. STAGECLEAR divides elapsed turns by this value to derive a performance multiplier; when replaying an already-cleared ordinary stage, FIELD also uses it as the forced-retreat turn limit. |
| `0xe7304` | stage_clear_performance_bonus | med | investigation | Base stage-clear reward increment. STAGECLEAR multiplies it by the turn-performance percentage derived from stage_target_clear_turns, divides by 100, and adds the result to the capped persistent reward counter at 0x6719. The counter's player-facing resource name remains unresolved. | | `0xe7304` | stage_clear_performance_bonus | med | investigation | Base stage-clear reward increment. STAGECLEAR multiplies it by the turn-performance percentage derived from stage_target_clear_turns, divides by 100, and adds the result to the capped persistent reward counter at 0x6719. The counter's player-facing resource name remains unresolved. |
@@ -266,7 +268,6 @@
| `0xe6cc2` | — | low | auto-shape | array | | `0xe6cc2` | — | low | auto-shape | array |
| `0xe6d26` | — | low | auto-shape | array | | `0xe6d26` | — | low | auto-shape | array |
| `0xe6d8a` | — | low | auto-shape | array | | `0xe6d8a` | — | low | auto-shape | array |
| `0xe6dee` | — | low | auto-shape | array |
| `0xe6e52` | — | low | auto-shape | array | | `0xe6e52` | — | low | auto-shape | array |
| `0xe6eb6` | — | low | auto-shape | array | | `0xe6eb6` | — | low | auto-shape | array |
| `0xe6f1a` | — | low | auto-shape | array | | `0xe6f1a` | — | low | auto-shape | array |
@@ -3664,7 +3665,6 @@
| `0x6f70` | — | med | auto-shape | record-table[stride 14] | | `0x6f70` | — | med | auto-shape | record-table[stride 14] |
| `0x74e8` | — | med | auto-shape | record-table[stride 4] | | `0x74e8` | — | med | auto-shape | record-table[stride 4] |
| `0x7e54` | — | med | auto-shape | record-table[stride 50] | | `0x7e54` | — | med | auto-shape | record-table[stride 50] |
| `0x141a4` | — | med | auto-shape | record-table[stride 50] |
| `0x20543` | — | med | auto-shape | record-table[stride 27] | | `0x20543` | — | med | auto-shape | record-table[stride 27] |
| `0x341ab` | — | med | auto-shape | record-table[stride 53] | | `0x341ab` | — | med | auto-shape | record-table[stride 53] |
| `0x4e085` | — | med | auto-shape | record-table[stride 3] | | `0x4e085` | — | med | auto-shape | record-table[stride 3] |

View File

@@ -285,14 +285,22 @@ variants:
This accounts for 220 initial-owner values, 229 teleport destinations, 626 treasure pairs, and 246 card This accounts for 220 initial-owner values, 229 teleport destinations, 626 treasure pairs, and 246 card
list ids. The faction-gate branch resolves another 104 cells on populated types 18--21 and 25. Type 17 list ids. The faction-gate branch resolves another 104 cells on populated types 18--21 and 25. Type 17
(`針`, spikes) is explicitly outside FIELD's faction comparison, so its 16 populated values remain raw (`針`, spikes) is explicitly outside FIELD's faction comparison, so it does not borrow the neighboring
rather than borrowing the neighboring hazard meaning. In total, 81 placements across object types 11, hazard meaning.
17, 26, and 27 still retain one raw payload cell because their type-specific meaning is not yet separable
from the consumer behavior. A separate initialization/render path resolves the remaining state-row payloads. On a fresh stage, FIELD
copies the first tagged payload into `stage_object_runtime_state[stage][slot]` only when the object's
OBINIT `object_sprite_state_row_mode` equals 1. DRAWOBJ applies the same mode check and multiplies that
runtime state by the object's sprite height to select its vertical source row. The join therefore exposes
78 cells as `initial_object_state_id`: one door (type 11), 16 spikes (type 17), and 61 deployment flags
(type 26, `出撃の旗`). All deployment-flag values are 2, and all 63 enemies linked to those flags are also
faction 2, consistent with OBINIT's `敵の増援地点` description; because the spawn branch accepts type 26
without comparing those values, the field remains the directly proven object state rather than a guessed
faction id. Only three type-27 (`異界の門`) first-payload cells remain raw.
OBINIT is the authoritative object-definition table: 46 one-based records provide the type names, and 34 OBINIT is the authoritative object-definition table: 46 one-based records provide the type names, and 34
provide short player-facing effect descriptions used by the field object-information path. The STINIT join now provide short player-facing effect descriptions used by the field object-information path. The STINIT join
adds `type_name` to every placement and `type_description` when populated while retaining `type_id`; now adds `type_name` to every placement and `type_description` when populated while retaining `type_id`;
top-level `object_definition_table: "OBINIT"` records the join provenance. This is intentionally separate top-level `object_definition_table: "OBINIT"` records the join provenance. This is intentionally separate
from payload decoding: a known display label does not by itself establish the meaning of a tagged cell. from payload decoding: a known display label does not by itself establish the meaning of a tagged cell.
@@ -315,7 +323,7 @@ confirmed parallel buffers plus both prerequisite tables are also assembled into
`object_placements` across 66 stages. Each placement contains its slot, numeric type plus OBINIT `object_placements` across 66 stages. Each placement contains its slot, numeric type plus OBINIT
name/available description, tile coordinates, difficulty mask, populated positive/negative story name/available description, tile coordinates, difficulty mask, populated positive/negative story
prerequisites, optional reinforcement schedule, and the decoded type-tagged payload variants above. The prerequisites, optional reinforcement schedule, and the decoded type-tagged payload variants above. The
remaining 81 undecoded payload cells stay attached under remaining three undecoded type-27 payload cells stay attached under
`unknown_fields`, so this convenience view loses no evidence or invents names. `unknown_fields`, so this convenience view loses no evidence or invents names.
The same records now contain 1,378 joined `enemy_spawns` across 66 stages, with unit/faction, direct or The same records now contain 1,378 joined `enemy_spawns` across 66 stages, with unit/faction, direct or
object-linked placement data when present, difficulty and story gates, level rules, random-selection object-linked placement data when present, difficulty and story gates, level rules, random-selection
@@ -368,10 +376,10 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape
1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, the confirmed EBINIT row layouts, and 1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, the confirmed EBINIT row layouts, and
STINIT's mixed stage records now have machine-readable investigation surfaces, including joined object STINIT's mixed stage records now have machine-readable investigation surfaces, including joined object
placements and enemy spawns. STINIT's universal object schedule, four consumer-proven tagged payload placements and enemy spawns. STINIT's universal object schedule, consumer-proven tagged payload
families, faction-gated hazards/barriers, OBINIT definition join, and first-clear enemy gate are decoded; families, faction-gated hazards/barriers, initialized object states, OBINIT definition join, and
next trace the 81 remaining raw object payloads on types 11, 17, 26, and 27 only where their first-clear enemy gate are decoded; next trace the three remaining raw type-27 payloads only where its
type-specific runtime branches distinguish them. Isolate EBINIT's remaining voice/action special-spawn path distinguishes them. Isolate EBINIT's remaining voice/action
slots under the same rule. Preserve explicit joins and do not infer meaning from column position alone. slots under the same rule. 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, …) 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. and fold in other `set-string`/`copy-to-global` writers not covered by the `*INIT` set.

View File

@@ -754,11 +754,13 @@ the 20-slot map-texture override list, object type/tile/difficulty fields, and s
forbidden story prerequisites. Generated records now carry a joined `semantic_fields` view and assemble forbidden story prerequisites. Generated records now carry a joined `semantic_fields` view and assemble
2,312 object placements across 66 stages. FIELD resolves the universal reinforcement interval/limit and 2,312 object placements across 66 stages. FIELD resolves the universal reinforcement interval/limit and
type-tagged initial-faction, teleport, treasure, card-list, and non-triggering-faction payloads. OBINIT type-tagged initial-faction, teleport, treasure, card-list, and non-triggering-faction payloads. OBINIT
supplies authoritative names for all 46 object types and descriptions for 34; 81 payload cells on types 11, supplies authoritative names for all 46 object types and descriptions for 34. Its state-row mode and
17, 26, and 27 remain raw. The separate 30-cell enemy family is also decoded: FIELD/SETEN/ADDEN establish FIELD/DRAWOBJ initialization path resolve 78 initial object states on types 11, 17, and 26; only three
unit/faction, direct or object-linked placement, difficulty/story gates, level floor/cap/scaling, weighted type-27 payload cells remain raw. The separate 30-cell enemy family is also decoded:
selection, and three difficulty-specific movement/battle routine-set ids. The generated view assembles all 1,378 FIELD/SETEN/ADDEN establish unit/faction, direct or object-linked placement, difficulty/story gates, level
populated enemy slots across 66 stages, including 485 enemies suppressed after the stage's first clear. floor/cap/scaling, weighted selection, and three difficulty-specific movement/battle routine-set ids. The
generated view assembles all 1,378 populated enemy slots across 66 stages, including 485 enemies suppressed
after the stage's first clear.
Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks: Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks:
@@ -768,7 +770,7 @@ Once the natural spine and first gameplay loop are trustworthy, broaden in indep
- Unit statistics, equipment, inventory, skills, magic, heroine forms, and progression. - Unit statistics, equipment, inventory, skills, magic, heroine forms, and progression.
- Combat resolution presentation, enemy turns/AI services, and win/loss transitions. - Combat resolution presentation, enemy turns/AI services, and win/loss transitions.
- A full chapter of ADV and the scene types encountered between gameplay segments. - A full chapter of ADV and the scene types encountered between gameplay segments.
- Remaining STINIT tagged payloads on object types 11, 17, 26, and 27, plus other data schemas when - The three remaining STINIT tagged payloads on object type 27, plus other data schemas when
their runtime consumers make them necessary. their runtime consumers make them necessary.
The high-level Phase B direction remains canonical in `docs/remake-architecture-and-roadmap.md`; this file The high-level Phase B direction remains canonical in `docs/remake-architecture-and-roadmap.md`; this file

View File

@@ -75,8 +75,9 @@ now assembles the confirmed type/X/Y/difficulty and story-prerequisite buffers i
across 66 stages. It also exposes 604 reinforcement schedules and consumer-proven tagged payload variants across 66 stages. It also exposes 604 reinforcement schedules and consumer-proven tagged payload variants
for initial factions, teleport destinations, treasure item/count pairs, card-generation lists, and 104 for initial factions, teleport destinations, treasure item/count pairs, card-generation lists, and 104
hazard/barrier non-triggering faction gates. Every placement joins its type name and, where populated, its hazard/barrier non-triggering faction gates. Every placement joins its type name and, where populated, its
effect description from OBINIT's 46 definitions; 81 type-specific payload cells on types 11, 17, 26, and effect description from OBINIT's 46 definitions. OBINIT's state-row mode resolves another 78 initial
27 remain raw. A second join assembles all 1,378 populated enemy slots object states on types 11, 17, and 26; only three type-27 payload cells remain raw. A second join assembles
all 1,378 populated enemy slots
across the same 66 stages, including unit/faction, placement, difficulty/story gates, level rules, weighted across the same 66 stages, including unit/faction, placement, difficulty/story gates, level rules, weighted
alternatives, difficulty-specific movement/battle routine sets, and 485 first-clear-only replay gates. alternatives, difficulty-specific movement/battle routine sets, and 485 first-clear-only replay gates.

View File

@@ -55,7 +55,7 @@ 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_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 <MES> [OUTNAME]` | `<MES>.BIN``build/data/<OUTNAME>.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 <MES> [OUTNAME]` | `<MES>.BIN``build/data/<OUTNAME>.json` |
| `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer / mixed 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`. 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, and non-triggering hazard/barrier faction), reinforcement schedules, and first-clear-only enemy gates. 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 <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` plus OBINIT for STINIT, matching `<MES>.BIN` when supported, and `build/globals.json``build/data/<OUTNAME>.json`, `build/data/README.md` | | `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer / mixed 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`. 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), reinforcement schedules, and first-clear-only enemy gates. 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 <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` plus OBINIT for STINIT, matching `<MES>.BIN` when supported, and `build/globals.json``build/data/<OUTNAME>.json`, `build/data/README.md` |
| `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric/mixed table: message coverage, per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. `--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 <TABLE> [--build] [--limit N] [--message-query REGEX]` | `build/data/<TABLE>.json` + corpus → stdout; with `--build`, `build/data/<TABLE>-field-profile.{json,md}` | | `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric/mixed table: message coverage, per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. `--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 <TABLE> [--build] [--limit N] [--message-query REGEX]` | `build/data/<TABLE>.json` + corpus → stdout; with `--build`, `build/data/<TABLE>-field-profile.{json,md}` |
| `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based and mixed selector-dispatched INIT extraction, MES reconstruction/joins, footer-array accounting and semantic projection, OBINIT object-definition extraction, STINIT object/enemy joins and tagged-payload behavior, and field/message profiling. | run each directly | — | | `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based and mixed selector-dispatched INIT extraction, MES reconstruction/joins, footer-array accounting and semantic projection, OBINIT object-definition extraction, STINIT object/enemy joins and tagged-payload behavior, and field/message 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}` | | `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}` |

View File

@@ -54,7 +54,7 @@
- [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.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.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 tables → JSON.** `tools/extract_init.py` auto-detects table shape (`name`/`numeric`/`footer`/`mixed`) → **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], and **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays]. Validated; see `build/data/README.md`. Raw addresses remain bytecode provenance; confirmed semantics come from `vm-map/globals.toml`. - [x] **2.2 — `*INIT` data tables → JSON.** `tools/extract_init.py` auto-detects table shape (`name`/`numeric`/`footer`/`mixed`) → **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], and **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays]. 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, and non-triggering-faction payloads while preserving 81 unresolved tagged cells on types 11, 17, 26, and 27. 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.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 while preserving only three unresolved type-27 cells. 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.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`. - [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: 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).

View File

@@ -396,12 +396,15 @@ def extract_name(scr):
@cache @cache
def object_type_definitions() -> dict[int, dict]: def object_type_definitions() -> dict[int, dict]:
"""Load OBINIT's authoritative names and descriptions keyed by object type id.""" """Load OBINIT's authoritative display and state-row metadata by object type id."""
records, _ = extract_name(sys4load.load(resolve("OBINIT"))) records, _ = extract_name(sys4load.load(resolve("OBINIT")))
return { return {
record["id"]: { record["id"]: {
"name": record["name"], "name": record["name"],
**({"description": record["desc"]} if record.get("desc") else {}), **({"description": record["desc"]} if record.get("desc") else {}),
"uses_runtime_state_sprite_row": (
record.get("fields", {}).get("0xe6dee") == 1
),
} }
for record in records for record in records
} }
@@ -639,6 +642,10 @@ def attach_stage_object_placements(
obj["card_generation_list_id"] = payload.pop("0xe73bb") obj["card_generation_list_id"] = payload.pop("0xe73bb")
elif 18 <= type_id <= 25 and "0xe73bb" in payload: elif 18 <= type_id <= 25 and "0xe73bb" in payload:
obj["non_triggering_faction_id"] = payload.pop("0xe73bb") obj["non_triggering_faction_id"] = payload.pop("0xe73bb")
elif (definition
and definition["uses_runtime_state_sprite_row"]
and "0xe73bb" in payload):
obj["initial_object_state_id"] = payload.pop("0xe73bb")
unknown = payload unknown = payload
if unknown: if unknown:
obj["unknown_fields"] = unknown obj["unknown_fields"] = unknown

View File

@@ -166,13 +166,29 @@ def test_real_mixed_table() -> None:
for obj in all_objects for obj in all_objects
if "non_triggering_faction_id" in obj if "non_triggering_faction_id" in obj
} == {1, 2, 3}, "STINIT faction gates retain all observed faction ids") } == {1, 2, 3}, "STINIT faction gates retain all observed faction ids")
state_objects = [
obj for obj in all_objects if "initial_object_state_id" in obj
]
check(len(state_objects) == 78,
"STINIT state-row metadata decodes every remaining initialized object state")
check({
obj["type_id"] for obj in state_objects
} == {11, 17, 26}, "STINIT state-row payloads remain scoped to proven object types")
deployment_flags = [
obj for obj in state_objects if obj["type_id"] == 26
]
check(len(deployment_flags) == 61
and {obj["initial_object_state_id"] for obj in deployment_flags} == {2},
"STINIT deployment flags expose their initial object state")
spike = next( spike = next(
obj for obj in all_objects obj for obj in state_objects if obj["type_id"] == 17
if obj["type_id"] == 17 and "unknown_fields" in obj
) )
check(spike["unknown_fields"] == {"0xe73bb": 2}, check(spike["initial_object_state_id"] == 2
"STINIT spike payload remains raw because FIELD excludes type 17 from the gate") and "non_triggering_faction_id" not in spike,
check(sum("unknown_fields" in obj for obj in all_objects) == 81, "STINIT spikes expose state without inventing the excluded faction gate")
unknown_objects = [obj for obj in all_objects if "unknown_fields" in obj]
check(len(unknown_objects) == 3
and {obj["type_id"] for obj in unknown_objects} == {27},
"STINIT preserves unresolved tagged object payloads as raw evidence") "STINIT preserves unresolved tagged object payloads as raw evidence")
extract_init.attach_stage_enemy_spawns(records) extract_init.attach_stage_enemy_spawns(records)
first_spawn = records[0]["enemy_spawns"][0] first_spawn = records[0]["enemy_spawns"][0]
@@ -195,6 +211,20 @@ def test_real_mixed_table() -> None:
"STINIT preserves weighted object-linked enemy alternatives") "STINIT preserves weighted object-linked enemy alternatives")
check(sum(len(record["enemy_spawns"]) for record in records) == 1378, check(sum(len(record["enemy_spawns"]) for record in records) == 1378,
"STINIT assembles every populated enemy spawn slot") "STINIT assembles every populated enemy spawn slot")
linked_deployment_spawns = []
for record in records:
objects_by_slot = {
obj["slot"]: obj for obj in record["object_placements"]
}
linked_deployment_spawns.extend(
spawn
for spawn in record["enemy_spawns"]
if (object_slot := spawn.get("object_slot")) in objects_by_slot
and objects_by_slot[object_slot]["type_id"] == 26
)
check(len(linked_deployment_spawns) == 63
and {spawn["faction_id"] for spawn in linked_deployment_spawns} == {2},
"STINIT deployment flags correlate with all linked enemy-faction spawns")
check(sum( check(sum(
spawn.get("first_clear_only", False) spawn.get("first_clear_only", False)
for record in records for record in records

View File

@@ -1622,6 +1622,26 @@ usage = "OBINIT writes the short object descriptions displayed by the field obje
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
[[global]]
address = "0xe6dee"
name = "object_sprite_state_row_mode"
category = "data-table"
type = "int[100]"
value_domain = "{1,2}; 1 selects a vertical sprite row from runtime object state, 2 fixes source Y at row 0"
usage = "OBINIT metadata indexed by object type. FIELD copies the type-tagged initial payload into stage_object_runtime_state only for mode 1; DRAWOBJ likewise multiplies that runtime state by the object sprite height to select source Y only for mode 1."
source = "investigation"
confidence = "high"
[[global]]
address = "0x141a4"
name = "stage_object_runtime_state"
category = "data-table"
type = "int[][50]"
value_domain = "type-dependent small integer state; initial values observed 1..3"
usage = "Per-stage, per-object-slot runtime state. FIELD initializes mode-1 objects from STINIT's first tagged payload on a fresh stage and updates capturable-object ownership; DRAWOBJ uses it as the vertical sprite-row index. Other consumers interpret it by object type."
source = "investigation"
confidence = "high"
[[global]] [[global]]
address = "0x27b9" address = "0x27b9"
name = "stage_victory_condition_1" name = "stage_victory_condition_1"