From 9e5fa9407349496296217f2040bc2350a354b652 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 09:07:58 -0400 Subject: [PATCH] Decode STINIT tagged object payloads --- docs/global-reference.md | 10 +++++----- docs/name-resolution.md | 41 ++++++++++++++++++++++++++------------ docs/phase-b-framework.md | 11 +++++----- docs/script-inventory.md | 9 +++++---- docs/tools-reference.md | 2 +- docs/vm-mapping-plan.md | 2 +- tools/extract_init.py | 40 +++++++++++++++++++++++++++---------- tools/test_extract_init.py | 37 ++++++++++++++++++++++++++++++++-- vm-map/globals.toml | 41 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 151 insertions(+), 42 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 494def4..624b67d 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5030 globals (181 curated, 4849 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5030 globals (185 curated, 4845 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -31,6 +31,7 @@ | `0x65ce` | skill_acquired_flags | high | investigation | Persistent acquired-skill flags. ADDSKILL sets the selected skill after resolving the unit's equipped-skill slots; FORT checks the flag before granting a skill; CHMENU combines it with skill_change_catalog_eligible to build the available skill-change catalog. | | `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. | +| `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. | | `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. | | `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. | @@ -112,6 +113,8 @@ | `0xe7325` | stage_object_tile_x | high | investigation | X coordinate for each current-stage object slot. DRAWOBJ converts it to centered map-space pixels; FIELD and CALCOCC use it with stage_object_tile_y for object interaction and occupancy. | | `0xe7357` | stage_object_tile_y | high | investigation | Y coordinate for each current-stage object slot. DRAWOBJ converts it to centered map-space pixels; FIELD and CALCOCC use it with stage_object_tile_x for object interaction and occupancy. | | `0xe7389` | stage_object_type_id | high | investigation | Object-definition id for each current-stage object slot. SETOBJ decides whether the slot exists, while DRAWOBJ and FIELD use the id to select shared object graphics, dimensions, animation, collision, and behavior metadata. | +| `0xe741f` | stage_object_reinforcement_interval_turns | high | investigation | Per-object reinforcement schedule. FIELD compares the current turn and the object's runtime spawn count against this interval to calculate due weighted enemy spawns; object type 27 uses the same value as its one-shot trigger turn. | +| `0xe7451` | stage_object_reinforcement_spawn_limit | high | investigation | Maximum number of scheduled units materialized through a stage object. FIELD stops the object's reinforcement path once its runtime spawn counter reaches this value; object type 27 stores one for its one-shot special spawn. | | `0xe7483` | stage_object_difficulty_mask | high | investigation | Per-object difficulty inclusion mask. SETOBJ uses check-bit with difficulty_index and rejects the object slot when the selected difficulty bit is absent. | | `0xe74b5` | stage_object_required_story_flags | high | investigation | Seven positive prerequisites per stage object. SETOBJ subtracts one from every populated id and suppresses the object unless the corresponding story_event_flags cell equals 1. Columns: 0=required_flag_1, 1=required_flag_2, 2=required_flag_3, 3=required_flag_4, 4=required_flag_5, 5=required_flag_6, 6=required_flag_7. | | `0xe7613` | stage_object_forbidden_story_flags | high | investigation | Five negative prerequisites per stage object. SETOBJ subtracts one from every populated id and suppresses the object when the corresponding story_event_flags cell equals 1. Columns: 0=forbidden_flag_1, 1=forbidden_flag_2, 2=forbidden_flag_3, 3=forbidden_flag_4, 4=forbidden_flag_5. | @@ -120,6 +123,7 @@ | `0xe777b` | stage_enemy_spawn_object_slot | high | investigation | Optional object-placement anchor for an enemy spawn. FIELD resolves nonzero entries through stage_object_type_id and uses the matching stage_object_tile_x/Y location; zero selects stage_enemy_spawn_tile_x/Y directly. | | `0xe7799` | stage_enemy_faction_id | high | investigation | Per-spawn faction/team selector. FIELD and ADDEN copy it into the runtime unit's faction field, use it to choose the entity-index band, and test the same value against map occupancy masks. | | `0xe77b7` | stage_enemy_difficulty_mask | high | investigation | Per-spawn difficulty inclusion mask. FIELD uses check-bit with difficulty_index and rejects a slot when the selected difficulty bit is absent. | +| `0xe77d5` | stage_enemy_first_clear_only | high | investigation | Per-spawn replay gate. FIELD rejects a populated enemy slot when this value is 2 and stage_clear_state for the current stage is 1; zero/default slots remain eligible on cleared-stage replays. | | `0xe77f3` | stage_enemy_random_selection_weight | high | investigation | Weight for stage-enemy alternatives. FIELD groups eligible nonzero-weight slots by their stage_enemy_spawn_object_slot, sums the weights, makes a weighted random choice within each eligible group, and directly materializes zero-weight slots. | | `0xe7811` | stage_enemy_unit_id | high | investigation | Unit-definition id for each current-stage enemy slot. FIELD rejects zero slots, while FIELD and ADDEN pass populated ids to SETEN to copy the corresponding EBINIT definition into a runtime entity. | | `0xe782f` | stage_enemy_min_level | high | investigation | Scenario-specific minimum level for each stage enemy. After applying party-level auto-scaling, SETEN raises the runtime level to this floor before applying the maximum-level clamp. | @@ -172,7 +176,6 @@ | `0x6868` | — | low | auto-shape | array | | `0x6930` | — | low | auto-shape | array | | `0x6994` | — | low | auto-shape | array | -| `0x7684` | — | low | auto-shape | array | | `0x7a6c` | — | low | auto-shape | array | | `0x204fb` | — | low | auto-shape | array | | `0x2052f` | — | low | auto-shape | array | @@ -276,10 +279,7 @@ | `0xe730e` | — | low | auto-shape | array | | `0xe73bb` | — | low | auto-shape | array | | `0xe73ed` | — | low | auto-shape | array | -| `0xe741f` | — | low | auto-shape | array | -| `0xe7451` | — | low | auto-shape | array | | `0xe770d` | — | low | auto-shape | array | -| `0xe77d5` | — | low | auto-shape | array | | `0xe7e8d` | — | low | auto-shape | array | | `0xe8275` | — | low | auto-shape | array | | `0xebd0d` | — | low | auto-shape | array | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index c48ef03..d442fb2 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -269,7 +269,21 @@ the shared fallback. DRAWOBJ converts `0xe7325` and `0xe7357` to map-space coord SETOBJ/DRAWOBJ/FIELD use `0xe7389` to index shared object definitions. They are object tile X, tile Y, and type id. SETOBJ tests the `{3,4,7}` masks in `0xe7483` against GAMESTART's three-way `difficulty_index`, then applies seven required and five forbidden one-based ids from `0xe74b5`/`0xe7613` against the shared -`story_event_flags` bank. +`story_event_flags` bank. FIELD's turn loop establishes `0xe741f` and `0xe7451` as each object's +reinforcement interval and spawn limit. Type 27 uses the same pair for a one-shot special spawn. + +The intervening `0xe73bb`/`0xe73ed` pair is deliberately not assigned one global name: FIELD dispatches it +by `stage_object_type_id`, making it a tagged payload. The generated join decodes only consumer-proven +variants: + +- types 1--4: `initial_faction_id` in the first cell; +- types 6 and 36: teleport `destination_tile_x` / `destination_tile_y`; +- types 7 and 8: treasure `item_id` / `item_quantity`, passed to ADDITEM; +- type 28: `card_generation_list_id`, passed to CDINIT. + +This accounts for 220 initial-owner values, 229 teleport destinations, 626 treasure pairs, and 246 card +list ids. Another 185 placements across object types 11, 17--21, 25--27 retain one raw payload cell because +their type-specific meaning is not yet separable from the consumer behavior. The enemy pass follows the separate 30-cell family through FIELD, SETEN, ADDEN, MVRTN, and BTRTN. Slot zero is reserved for ADDEN's synthesized special-unit path; the stage table populates slots 1 through 29. @@ -279,21 +293,22 @@ three-bit difficulty mask in `0xe77b7`, uses `0xe77f3` as a weighted-random alte the seven required plus five forbidden story flags in `0xe793d`/`0xe7a0f`. SETEN proves `0xe782f`, `0xe784d`, and `0xe786b` are the scenario level floor, cap, and party-level scaling divisor. Finally, the three-value footer rows in `0xe7889` and optional `0xe78e3` become difficulty-specific movement and -battle routine-set ids selected by MVRTN/BTRTN. +battle routine-set ids selected by MVRTN/BTRTN. The final `0xe77d5` gate is also resolved: +STAGECLEAR writes `stage_clear_state[current_stage] = 1`, and FIELD suppresses a spawn when that state is +set and the spawn's cell equals 2. The joined view exposes all 485 populated cases as +`first_clear_only: true`. Generated INIT records now retain their raw `fields`/`record_fields`/buffer keys and additionally expose a flat `semantic_fields` projection joined through the top-level `field_semantics` map. For STINIT, the four confirmed parallel buffers plus both prerequisite tables are also assembled into 2,312 `object_placements` across 66 stages. Each placement contains its slot, type, tile coordinates, difficulty -mask, and populated positive/negative story prerequisites. The four still-unresolved object-parameter -buffers remain attached under `unknown_fields`, so this convenience view loses no evidence or invents names. +mask, populated positive/negative story prerequisites, optional reinforcement schedule, and the decoded +type-tagged payload variants above. The remaining 185 undecoded payload cells stay attached under +`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 object-linked placement data when present, difficulty and story gates, level rules, random-selection -weight, and movement/battle routine rows. Raw footer metadata stays in `footer_arrays`, while its -`semantic_fields` value is the copied row itself. The one remaining spawn-mode cell (`0xe77d5`, always 2 -when populated) stays under `unknown_fields`: FIELD ties it to already-cleared-stage state, but the precise -player-facing nuance is not yet strong enough to name. The next STINIT batch is the four type-specific -object parameters plus that isolated spawn-mode gate. +weight, movement/battle routine rows, and `first_clear_only` replay gating. Raw footer metadata stays in +`footer_arrays`, while its `semantic_fields` value is the copied row itself. ### The curated registry — `vm-map/globals.toml` (2026-07-07) @@ -341,10 +356,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 STINIT's mixed stage records now have machine-readable investigation surfaces, including joined object - placements and enemy spawns. Next trace STINIT's four remaining specialized object parameters and its - isolated spawn-mode gate only where type-specific/runtime branches distinguish them. Isolate EBINIT's - remaining voice/action slots under the same rule. Preserve explicit joins and do not infer meaning from - column position alone. + placements and enemy spawns. STINIT's universal object schedule, four consumer-proven tagged payload + variants, and first-clear enemy gate are decoded; next trace the 185 remaining raw object payloads only + where their type-specific runtime branches distinguish them. Isolate EBINIT's remaining voice/action + 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, …) 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 e08f029..4f222d6 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -752,11 +752,12 @@ footer-array copies. AIM/FIELD/DRAWCHP consumers establish the four condition sl and turn-limit outcome. Further passes establish target clear turns, performance reward, replay behavior, the 20-slot map-texture override list, object type/tile/difficulty fields, and seven required plus five forbidden story prerequisites. Generated records now carry a joined `semantic_fields` view and assemble -2,312 object placements across 66 stages while retaining four unresolved type-specific parameters by raw -address. The separate 30-cell enemy family is also decoded: FIELD/SETEN/ADDEN establish unit/faction, +2,312 object placements across 66 stages. FIELD resolves the universal reinforcement interval/limit and +type-tagged initial-faction, teleport, treasure, and card-list payloads; 185 payload cells on other object +types remain raw. The separate 30-cell enemy family is also decoded: FIELD/SETEN/ADDEN establish unit/faction, direct or object-linked placement, difficulty/story gates, level 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; only one replay-linked spawn-mode field remains raw. +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: @@ -766,8 +767,8 @@ Once the natural spine and first gameplay loop are trustworthy, broaden in indep - Unit statistics, equipment, inventory, skills, magic, heroine forms, and progression. - Combat resolution presentation, enemy turns/AI services, and win/loss transitions. - A full chapter of ADV and the scene types encountered between gameplay segments. -- Remaining STINIT specialized object-parameter semantics and the isolated spawn-mode gate, plus other data - schemas when their runtime consumers make them necessary. +- Remaining STINIT tagged payloads on object types 11, 17--21, and 25--27, plus other data schemas when + their runtime consumers make them necessary. The high-level Phase B direction remains canonical in `docs/remake-architecture-and-roadmap.md`; this file only provides the execution framework. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 5d84405..eef1239 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -72,10 +72,11 @@ buffer rather than parallel per-id arrays. Its generated records preserve four m six scalars, cells in 29 preallocated buffers, and all 1,396 length-prefixed footer-array copies. Confirmed stage field meanings and the evidence workflow live in `docs/name-resolution.md`. The first semantic join now assembles the confirmed type/X/Y/difficulty and story-prerequisite buffers into 2,312 object placements -across 66 stages. 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 alternatives, and -difficulty-specific movement/battle routine sets. Unknown type-specific object parameters and the isolated -enemy spawn-mode field remain raw. +across 66 stages. It also exposes 604 reinforcement schedules and consumer-proven tagged payload variants +for initial factions, teleport destinations, treasure item/count pairs, and card-generation lists; 185 +other type-specific 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 +alternatives, difficulty-specific movement/battle routine sets, and 485 first-clear-only replay gates. ### Message/string tables (`*MES`) `ITMES` (64 KB — item text), `VIMES` (43 KB), `EIMES` (37 KB), `SKMES` (31 KB — skill diff --git a/docs/tools-reference.md b/docs/tools-reference.md index f3eebe7..f6f646b 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -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_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 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`. 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 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 matching `.BIN` when supported + `build/globals.json` → `build/data/.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`, including type-tagged teleport/treasure/card/initial-faction payloads, 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
[OUTNAME] [--mode …]` | `
.BIN` plus matching `.BIN` when supported + `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 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
[--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 and mixed selector-dispatched INIT extraction, MES reconstruction/joins, footer-array accounting and semantic projection, STINIT object/enemy joins, 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}` | diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index 136d4fa..1b526a9 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -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.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)** [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; four parallel object fields plus 7 required/5 forbidden story prerequisites assemble into 2,312 object placements. The 30-cell enemy family now contributes 1,378 joined spawns with unit/faction, placement, difficulty/story gates, level scaling, weighted selection, and difficulty-specific movement/battle routine sets. 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, 604 reinforcement schedules, and typed initial-faction, teleport, treasure, and card-list payloads while preserving 185 unresolved tagged 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`. - [ ] **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). diff --git a/tools/extract_init.py b/tools/extract_init.py index 413c310..ae700a2 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -564,8 +564,9 @@ def attach_stage_object_placements(records: list[dict]) -> None: "tile_x": "0xe7325", "tile_y": "0xe7357", "difficulty_mask": "0xe7483", + "reinforcement_interval_turns": "0xe741f", + "reinforcement_spawn_limit": "0xe7451", } - unknown_bases = ("0xe73bb", "0xe73ed", "0xe741f", "0xe7451") for record in records: fields = record.get("array_fields", {}) objects = [] @@ -593,11 +594,27 @@ def attach_stage_object_placements(records: list[dict]) -> None: obj["required_story_flags"] = required if forbidden: obj["forbidden_story_flags"] = forbidden - unknown = { - base: fields[f"{base}/{slot}"] - for base in unknown_bases - if f"{base}/{slot}" in fields + payload = { + base: fields[key] + for base in ("0xe73bb", "0xe73ed") + if (key := f"{base}/{slot}") in fields } + type_id = obj["type_id"] + if type_id in (1, 2, 3, 4) and "0xe73bb" in payload: + obj["initial_faction_id"] = payload.pop("0xe73bb") + elif type_id in (6, 36): + if "0xe73bb" in payload: + obj["destination_tile_x"] = payload.pop("0xe73bb") + if "0xe73ed" in payload: + obj["destination_tile_y"] = payload.pop("0xe73ed") + elif type_id in (7, 8): + if "0xe73bb" in payload: + obj["item_id"] = payload.pop("0xe73bb") + if "0xe73ed" in payload: + obj["item_quantity"] = payload.pop("0xe73ed") + elif type_id == 28 and "0xe73bb" in payload: + obj["card_generation_list_id"] = payload.pop("0xe73bb") + unknown = payload if unknown: obj["unknown_fields"] = unknown objects.append(obj) @@ -624,7 +641,6 @@ def attach_stage_enemy_spawns(records: list[dict]) -> None: "movement_routine_set_ids": ("0xe7889", 3), "battle_routine_set_ids": ("0xe78e3", 3), } - unknown_bases = ("0xe77d5",) for record in records: fields = record.get("array_fields", {}) footer_arrays = record.get("footer_arrays", {}) @@ -665,11 +681,13 @@ def attach_stage_enemy_spawns(records: list[dict]) -> None: spawn["required_story_flags"] = required if forbidden: spawn["forbidden_story_flags"] = forbidden - unknown = { - base: fields[f"{base}/{slot}"] - for base in unknown_bases - if f"{base}/{slot}" in fields - } + unknown = {} + if (mode_key := f"0xe77d5/{slot}") in fields: + mode = fields[mode_key] + if mode == 2: + spawn["first_clear_only"] = True + else: + unknown["0xe77d5"] = mode if unknown: spawn["unknown_fields"] = unknown spawns.append(spawn) diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index aa41b31..1065aeb 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -113,13 +113,41 @@ def test_real_mixed_table() -> None: "tile_x": 13, "tile_y": 1, "difficulty_mask": 7, - "unknown_fields": {"0xe73bb": 1}, + "initial_faction_id": 1, }, "STINIT joins confirmed object buffers into one placement record") + stage1_slot6 = next( + obj for obj in records[0]["object_placements"] if obj["slot"] == 6 + ) + check(stage1_slot6["item_id"] == 222 + and stage1_slot6["item_quantity"] == 1, + "STINIT object payloads decode by object type") + stage1_slot5 = next( + obj for obj in records[0]["object_placements"] if obj["slot"] == 5 + ) + check(stage1_slot5["reinforcement_interval_turns"] == 10 + and stage1_slot5["reinforcement_spawn_limit"] == 3, + "STINIT object placements expose reinforcement schedules") + stage1_slot8 = next( + obj for obj in records[0]["object_placements"] if obj["slot"] == 8 + ) + check(stage1_slot8["card_generation_list_id"] == 1, + "STINIT card objects expose their generation-list id") + stage1_slot9 = next( + obj for obj in records[0]["object_placements"] if obj["slot"] == 9 + ) + check(stage1_slot9["destination_tile_x"] == 13 + and stage1_slot9["destination_tile_y"] == 7, + "STINIT teleport payloads expose destination coordinates") stage2_slot3 = next( obj for obj in records[1]["object_placements"] if obj["slot"] == 3 ) check(stage2_slot3["required_story_flags"] == [902], "STINIT object placements join positive story prerequisites") + all_objects = [ + obj for record in records for obj in record["object_placements"] + ] + check(sum("unknown_fields" in obj for obj in all_objects) == 185, + "STINIT preserves unresolved tagged object payloads as raw evidence") extract_init.attach_stage_enemy_spawns(records) first_spawn = records[0]["enemy_spawns"][0] check(first_spawn == { @@ -133,7 +161,7 @@ def test_real_mixed_table() -> None: "object_slot": 2, "movement_routine_set_ids": [1, 1, 1], "forbidden_story_flags": [11], - "unknown_fields": {"0xe77d5": 2}, + "first_clear_only": True, }, "STINIT joins confirmed enemy buffers into one spawn record") stage1_slot2 = records[0]["enemy_spawns"][1] check(stage1_slot2["random_selection_weight"] == 1 @@ -141,6 +169,11 @@ def test_real_mixed_table() -> None: "STINIT preserves weighted object-linked enemy alternatives") check(sum(len(record["enemy_spawns"]) for record in records) == 1378, "STINIT assembles every populated enemy spawn slot") + check(sum( + spawn.get("first_clear_only", False) + for record in records + for spawn in record["enemy_spawns"] + ) == 485, "STINIT exposes every first-clear-only enemy gate") def test_real_message_tables() -> None: diff --git a/vm-map/globals.toml b/vm-map/globals.toml index f4779be..56fb725 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -1702,6 +1702,16 @@ usage = "Shared one-based story/event flag bank. Progression, recruitment, item, source = "investigation" confidence = "high" +[[global]] +address = "0x7684" +name = "stage_clear_state" +category = "data-table" +type = "int[]" +value_domain = "{0,1}; 1 means the stage has been cleared" +usage = "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." +source = "investigation" +confidence = "high" + [[global]] address = "0xe7303" name = "stage_target_clear_turns" @@ -1772,6 +1782,26 @@ usage = "Object-definition id for each current-stage object slot. SETOBJ decides source = "investigation" confidence = "high" +[[global]] +address = "0xe741f" +name = "stage_object_reinforcement_interval_turns" +category = "data-table" +type = "int[50]" +value_domain = "positive turn interval, 5..80 in the shipped stages; zero disables scheduled spawning" +usage = "Per-object reinforcement schedule. FIELD compares the current turn and the object's runtime spawn count against this interval to calculate due weighted enemy spawns; object type 27 uses the same value as its one-shot trigger turn." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xe7451" +name = "stage_object_reinforcement_spawn_limit" +category = "data-table" +type = "int[50]" +value_domain = "positive spawn limit, 1..20 in the shipped stages; zero disables scheduled spawning" +usage = "Maximum number of scheduled units materialized through a stage object. FIELD stops the object's reinforcement path once its runtime spawn counter reaches this value; object type 27 stores one for its one-shot special spawn." +source = "investigation" +confidence = "high" + [[global]] address = "0xe7483" name = "stage_object_difficulty_mask" @@ -1860,6 +1890,17 @@ source = "investigation" confidence = "high" depends_on = ["0x32f1"] +[[global]] +address = "0xe77d5" +name = "stage_enemy_first_clear_only" +category = "data-table" +type = "int[30]" +value_domain = "{0,2}; 2 suppresses the spawn after the stage has been cleared" +usage = "Per-spawn replay gate. FIELD rejects a populated enemy slot when this value is 2 and stage_clear_state for the current stage is 1; zero/default slots remain eligible on cleared-stage replays." +source = "investigation" +confidence = "high" +depends_on = ["0x7684"] + [[global]] address = "0xe77f3" name = "stage_enemy_random_selection_weight"