diff --git a/docs/global-reference.md b/docs/global-reference.md index 9c81746..f89eb18 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5054 globals (403 curated, 4651 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5054 globals (417 curated, 4637 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -235,10 +235,22 @@ | `0xe78e3` | stage_enemy_battle_routine_set_ids | high | investigation | Optional three-column battle-AI routine override per stage enemy. SETEN copies a populated row into the runtime unit or falls back to the unit definition, and BTRTN selects the difficulty_index column as its battle routine table row. Columns: 0=difficulty_0, 1=difficulty_1, 2=difficulty_2. | | `0xe793d` | stage_enemy_required_story_flags | high | investigation | Seven positive prerequisites per stage enemy. FIELD subtracts one from every populated id and suppresses the spawn 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. | | `0xe7a0f` | stage_enemy_forbidden_story_flags | high | investigation | Five negative prerequisites per stage enemy. FIELD subtracts one from every populated id and suppresses the spawn 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. | +| `0xe7e8d` | stage_unlock_group_ids | high | investigation | FIELD propagates an unlocked current stage to every other stage whose nonzero group id matches. The shipped groups bind route variants 32..34, 35..36, 71..72, and 82..83. | +| `0xe8275` | stage_main_progression_flags | high | investigation | When returning after a clear, FORT scans uncleared stage ids in descending order and auto-selects the first available row marked one. Side missions and all eight EX dungeons leave the flag zero. | +| `0xe865d` | stage_forbidden_story_flag_ids | high | investigation | FORT and SELSTAGE reject a stage when any populated cell resolves to story flag value one. The scripts subtract one before indexing story_event_flags, establishing the stored one-based id convention. Columns: 0=forbidden_flag_1, 1=forbidden_flag_2, 2=forbidden_flag_3, 3=forbidden_flag_4, 4=forbidden_flag_5, 5=forbidden_flag_6, 6=forbidden_flag_7. | +| `0xea1b5` | stage_required_story_flag_ids | high | investigation | FORT and SELSTAGE require every populated cell to resolve to story flag value one. Every shipped stage carries at least one gate, including common progression flags in columns two and three. 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. | +| `0xebd0d` | stage_display_number_major | high | investigation | FORT, SELSTAGE, AIM, STAGECLEAR, and management screens render this as the major half of the stage number. A negative value renders EX, while both number components zero identify an event-only row. | +| `0xec0f5` | stage_display_number_minor | high | investigation | Rendered after stage_display_number_major as the minor stage number. FORT treats a row with both components zero as a direct event and dispatches its entry SCJUMP decision without entering FIELD. | | `0xec4dd` | stage_map_min_tile_x | high | investigation | STINIT2's inclusive left map bound indexed by stage id. FIELD, DRAWMAP, DRAWMINIMAP, CALCOCC, and movement providers use it with stage_map_max_tile_x; terrain-grid accesses multiply the coordinate by two. | | `0xec8c5` | stage_map_max_tile_x | high | investigation | STINIT2's inclusive right map bound indexed by stage id. Consumers pair it with stage_map_min_tile_x for iteration, camera/minimap limits, random placement, and the doubled-coordinate terrain-atlas copy. | | `0xeccad` | stage_map_min_tile_y | high | investigation | STINIT2's inclusive top map bound indexed by stage id. FIELD and all map readers pair it with stage_map_max_tile_y and multiply it by two when addressing the half-tile terrain grid. | | `0xed095` | stage_map_max_tile_y | high | investigation | STINIT2's inclusive bottom map bound indexed by stage id. The maximum shipped value 800 explains MPINIT's final authored doubled grid row at Y 1600. | +| `0xed47d` | stage_minimap_atlas_origin_y | high | investigation | FIELD and SELSTAGE copy this to the minimap drawing origin. SELSTAGE subtracts it from stage_map_min_tile_y to crop the selected stage's vertically packed minimap strip and labels the following 150 atlas rows. | +| `0xed865` | stage_clear_base_spendable_point_rewards | high | investigation | STAGECLEAR multiplies this base award by the turn/party-performance percentage, divides by 100, adds any card bonus, and credits the capped shared_spendable_points balance. | +| `0xee035` | stage_scjump_decision_ids | high | investigation | FORT dispatches column zero before entering a stage or for an event-only row. FIELD and a few scene return paths dispatch column one after clear and column two after failure or forced-retreat outcomes; all 174 populated references resolve through SCINIT. Columns: 0=entry, 1=clear, 2=failure. | +| `0xeebed` | stage_extra_dungeon_flags | high | investigation | Marks the eight shipped EX dungeons. FORT, SELSTAGE, and management screens combine it with stage_clear_state when deciding whether to show and apply the stage's post-clear coin rewards. | +| `0xeefd5` | stage_clear_coin_quantities | high | investigation | FORT and SELSTAGE render the three clear-reward quantities beside coin icons. Management screens grant the same values as item ids 91..93 after a cleared or EX-marked stage. Columns: 0=bronze_coin_item_91, 1=silver_coin_item_92, 2=gold_coin_item_93. | +| `0xefb8d` | stage_loader_script_ids | high | investigation | FIELD indirectly calls the selected STINIT2 row before initializing the map. Every shipped stage points to the shared STINIT selector program, which then populates stage-specific objects, enemies, rules, textures, and audio. | | `0xeff78` | movement_routine_provider_selectors | high | investigation | RTINIT movement bank 0. MVRTN indexes it by current_routine_set_id and routine_step_index, resolves the selector through its RTN_M001..018/051..053/061 provider table, and call-scripts the selected movement routine. | | `0xf4d98` | movement_routine_activation_percents | high | investigation | RTINIT movement bank 1. After all other step gates pass, MVRTN executes the provider only when random-modulo-100 is below this value. | | `0xf9bb8` | movement_routine_parameter_1 | high | investigation | RTINIT movement bank 2. Its meaning is tagged by movement_routine_provider_selectors: RTN_M004 uses a stage_object_slot_index; RTN_M005/011/012 use destination_tile_x; RTN_M006/007/015 use maximum_target_route_steps; RTN_M010 uses resource_index (0=HP, 1=SP, 2=FS; shipped cells are unwritten/default zero); RTN_M013 uses target_faction_filter; and RTN_M014 uses maximum_threat_route_steps. The one RTN_M001 and one RTN_M008 authored cells are never read by those providers. | @@ -335,7 +347,6 @@ | `0x69e` | — | low | auto-shape | array | | `0x6fe` | — | low | auto-shape | array | | `0x712` | — | low | auto-shape | array | -| `0x27bd` | — | low | auto-shape | array | | `0x3276` | — | low | auto-shape | array | | `0x328a` | — | low | auto-shape | array | | `0x329e` | — | low | auto-shape | array | @@ -402,14 +413,6 @@ | `0xe730e` | — | low | auto-shape | array | | `0xe73ed` | — | low | auto-shape | array | | `0xe770d` | — | low | auto-shape | array | -| `0xe7e8d` | — | low | auto-shape | array | -| `0xe8275` | — | low | auto-shape | array | -| `0xebd0d` | — | low | auto-shape | array | -| `0xec0f5` | — | low | auto-shape | array | -| `0xed47d` | — | low | auto-shape | array | -| `0xed865` | — | low | auto-shape | array | -| `0xeebed` | — | low | auto-shape | array | -| `0xefb8d` | — | low | auto-shape | array | | `0x15287b` | — | low | auto-shape | array | | `0x15287d` | — | low | auto-shape | array | | `0x15287f` | — | low | auto-shape | array | @@ -514,6 +517,8 @@ | `0x27ba` | stage_victory_condition_2 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders nonempty values after stage_victory_condition_1, and FIELD copies the slot into the current mission-condition display. | | `0x27bb` | stage_defeat_condition_1 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders this line first in the defeat-condition section, and FIELD copies it into the current mission-condition display. | | `0x27bc` | stage_defeat_condition_2 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders nonempty values after stage_defeat_condition_1, and FIELD copies the slot into the current mission-condition display. | +| `0x27bd` | stage_display_names | high | investigation | FORT and SELSTAGE enumerate this sparse catalog and render the selected title, while FIELD copies the active title into its shared display buffer. STINIT2 reserves 1,000 ids even though the shipped rows end at 170. | +| `0x2ba5` | stage_description_lines | high | investigation | SELSTAGE multiplies stage_clear_state by three and renders the corresponding three-column half of the selected STINIT2 row. The explicit 1,000-by-6 geometry prevents these strings from being mistaken for additional stage records. Columns: 0=uncleared_line_1, 1=uncleared_line_2, 2=uncleared_line_3, 3=cleared_line_1, 4=cleared_line_2, 5=cleared_line_3. | | `0x4315` | card_definition_names | high | investigation | Card names populated by CDINIT2. FIELD indexes this table with current_card_id when presenting the card selected from CDINIT's weighted generation list. | | `0x4379` | card_definition_result_messages | high | investigation | Short player-facing result text paired with card_definition_names. FIELD displays the selected card's row after applying its CDINIT2 effect. | | `0x43dd` | name_entry_character_palette | high | investigation | INPUTNAME's five 70-cell character pages: row 0 hiragana, row 1 katakana, row 2 full-width Latin letters, row 3 Arabic/Kanji/Roman/circled numerals, and row 4 symbols. Cursor slots 70..74 select the page; selecting a populated cell copies it into the seven-character name buffer. | @@ -3750,7 +3755,6 @@ | `0x1fba` | — | high | auto-shape | item-desc-table | | `0x23a2` | — | high | auto-shape | skill-name-table | | `0x24ce` | — | high | auto-shape | skill-desc-table | -| `0x2ba5` | — | med | auto-shape | record-table[stride 6] | | `0x3239` | — | med | auto-shape | record-table[stride 3] | | `0x32f0` | — | med | auto-shape | current-entity-index? | | `0x3ebe` | — | med | auto-shape | record-table[stride 10] | @@ -5087,10 +5091,6 @@ | `0xe710e` | — | low | auto-shape | obinit-field? | | `0xe7172` | — | low | auto-shape | obinit-field? | | `0xe71d6` | — | med | auto-shape | record-table[stride 3] | -| `0xe865d` | — | med | auto-shape | record-table[stride 7] | -| `0xea1b5` | — | med | auto-shape | record-table[stride 7] | -| `0xee035` | — | med | auto-shape | record-table[stride 3] | -| `0xeefd5` | — | med | auto-shape | record-table[stride 3] | | `0x15261f` | — | med | auto-shape | record-table[stride 300] | | `0x152878` | — | low | auto-shape | index/counter? | | `0x15288b` | — | low | auto-shape | index/counter? | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index 48f8f79..0eeab2f 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -688,9 +688,11 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape through SCINIT and GAMESTART's restored story flags. CDINIT/CDINIT2 close FIELD's nine weighted generation lists and 81 typed card effects, including story gates and item/event/condition/visual joins. BTANINIT/BTANINIT2 close 122 battle-animation timelines and 202 visual/audio/hit-pulse - effects, with skill and weapon-class selection joined. With RTINIT's used-provider surface closed - and ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and INFOMES/MES classified, audit STINIT2's - remaining stage-definition columns next; + effects, with skill and weapon-class selection joined. STINIT2 closes 74 real stage rows (correcting + 247 description strings formerly misread as records), six pre/post-clear text slots, progression and + story gates, map/minimap geometry, rewards, and 174 resolved entry/clear/failure transitions. With + RTINIT's used-provider surface closed and ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and INFOMES/MES + classified, use live/native observation for STINIT2's sole script-unconsumed `0xedc4d` column next; never assign one universal meaning to a parameter bank whose meaning varies by provider selector. 2. **Label remaining 2D record tables by their readers** — cross-reference which scripts read each `rec[sN]` table and infer purpose from context, preserving reserved rows and sparse cells. RECOVER's diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 5072e65..f6c381e 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3193,3 +3193,30 @@ unreferenced authored surfaces. **Next:** audit STINIT2's remaining generic 321-row stage-definition surface. MPINIT already proves its tile-bound columns; the next pass should join the rest of its stage metadata to FIELD and the stage-selection/initialization consumers. + +## Data-semantics sidebar: STINIT2 stage definitions (2026-07-23) + +STINIT2's former generic 321-record view was structurally wrong: 247 of those “records” were +description strings. The dedicated schema classifies all 1,634 instructions into 74 sparse stage +names, 296 strings in a reserved `1000 × 6` description matrix, 1,263 numeric writes, and one exit. +SELSTAGE indexes the text matrix as three uncleared lines followed by three cleared lines. + +The 74 real rows divide into 66 mapped stages and eight event-only stages. The schema preserves +numbered, `EVENT`, and `EX` presentation; 49 main-progression flags; the eight EX flags; four +multi-stage unlock groups; and seven-column required/forbidden story-flag rows. FORT proves the +availability checks and descending main-stage auto-selection, while FIELD proves unlock-group +propagation. + +Map rows join the four already-proven tile bounds to doubled terrain-atlas coordinates and the +minimap atlas Y origin. Clear rewards expose the performance-scaled base spendable-point award and +the three item-quantity columns for bronze, silver, and gold coins. Every populated entry, clear, +and failure transition resolves through SCINIT (174/174), and every stage-loader resource resolves +to the shared `STINIT.BIN` selector (74/74). + +Regressions protect the corrected record geometry, complete instruction accounting, all six text +slots, display kinds, story gates, unlock groups, map/minimap coordinates, rewards, SCJUMP joins, +and loader joins. The 66 populated values at `0xedc4d` have no script consumer and remain explicit +as unresolved provenance. + +**Next:** use live/native observation only to settle STINIT2's unconsumed `0xedc4d` column; the +remaining script-visible stage-definition surface is closed. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 0ead06a..a736b4d 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -50,7 +50,7 @@ Large, table-like scripts — prime candidates for data extraction: `RTINIT` (67 KB — movement/battle routine-step banks), `CCINIT` (41 KB — class-change rule program), `SKINIT` (37 KB — skills), `CDINIT` (31 KB — weighted card-generation lists), `CDINIT2` (card definitions and effects), `BTANINIT` (105 KB — battle-effect definitions), `BTANINIT2` - (six-slot battle-animation timelines) + (six-slot battle-animation timelines), `STINIT2` (74 stage definitions plus six-line descriptions) - Smaller: `AFINIT`, `ALINIT`, `CIINIT`, `CNINIT`, `CTINIT`, `CVINIT`, `ILINIT`, `LAINIT`, `MAINIT`, `OBINIT` (46 object definitions), `SPINIT`, `TRINIT`, `VIINIT` @@ -173,6 +173,15 @@ anchor flags, surface geometry and offsets, sprite-atlas timing, sound delay, an All populated visual/audio resources resolve through SYS4INI; sixteen authored effects are unreferenced by BTANINIT2. +STINIT2 is a sparse 1,000-row stage catalog with 74 shipped definitions, not the generic extractor's +former 321 records. The other 247 apparent records were part of a `1000 × 6` description matrix; +all 296 populated lines divide into three uncleared and three cleared slots. The numeric surface +contains main-progression/EX flags, seven required and seven forbidden story gates, shared unlock +groups, display numbers, map/minimap geometry, performance-scaled point and bronze/silver/gold coin +rewards, and entry/clear/failure SCJUMP decisions. All 174 transitions resolve through SCINIT and +all 74 loader ids resolve to STINIT.BIN. The populated `0xedc4d` column has no script reader and +remains explicitly unresolved. + RTINIT is a seventh banked shape. Its 3,336 writes populate twenty parallel `1000 × 20` banks: ten movement banks consumed by MVRTN/RTN_M providers and ten battle banks consumed by BTRTN/RTN_B providers. The generated 172 routine-set records assemble 1,043 movement steps and fourteen battle steps, join diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 28f144e..a1c61e5 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -55,9 +55,9 @@ 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 repeated global-id dispatch chains in either fallthrough-body (ITMES/SKMES/MAMES) or branch-target-body (VIMES/EIMES/CIMES) form, reconstruct player-facing text and furigana, preserve guard/body offsets, and emit an ID-keyed message table. Title-bearing tables expose `title`/`description`; EIMES exposes its untitled lines as `summary`/`strategy`; CIMES exposes one untitled multiline `biography`; MAMES exposes untitled `description` text. | `extract_message_table.py [OUTNAME]` | `.BIN` → `build/data/.json` | -| `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules / dispatch / banked 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. Dedicated name-mode schemas recover VIINIT's pre-title glossary prerequisites and CIINIT's profile name, backing-unit, optional portrait, and reserved placement columns. Footer-mode MPINIT recovers its sparse 53-column doubled-coordinate terrain atlas, joins 66 STINIT2 stage rectangles, and resolves terrain ids through LAINIT. 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. Banked mode decodes RTINIT's twenty parallel 1000-by-20 movement/battle routine banks, retains all assignments and overwrites, assembles final per-slot steps, resolves provider selectors to RTN_M/RTN_B scripts, and applies selector-scoped schemas where proven. Current schemas cover all nineteen used providers, RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061, including progress-only, randomized roaming, object-slot, coordinate, enemy/ally, treasure and Magic Pillar search, Healing Feather, waypoint, faction-terrain, retreat, reachable normal-attack routing, immediate offensive target/action selection, and immediate allied healing. Parameterless behavior schemas publish an empty `parameter_fields` mapping while still joining `provider_behavior`, target selection, action selection, and completion rules. Provider defaults such as M004's unwritten slot 0, M010's zero/HP resource index, and M013's any-foreign-faction filter are projected explicitly and counted separately from populated source cells. Authored cells proven unread by M001/M008 remain under per-step `ignored_movement_parameters` and contribute to top-level `ignored_movement_parameter_count`; they are not silently discarded or assigned invented meanings. Raw provider banks remain intact, and top-level `movement_provider_parameter_schemas` documents the projection. ITINIT, SKINIT, VIINIT, EBINIT, and CIINIT join their matching MES text. 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, STINIT2/LAINIT for MPINIT, CDINIT2/STINIT for CDINIT, ITINIT/SCINIT/ILINIT and `build/callscript-names.json` for CDINIT2, BTANINIT/SKINIT/ITINIT and `build/callscript-names.json` for BTANINIT2, BTANINIT2 and `build/callscript-names.json` for BTANINIT, `build/callscript-names.json` for RTINIT/SCINIT, `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/footer/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. MPINIT's footer specialization summarizes atlas geometry, implicit-zero rows, terrain ids, stage-rectangle joins, sharing, and border cells; CDINIT summarizes selector/list sizes, CDINIT2 joins and story gates, STINIT references, and the runtime scan/clear geometry; CDINIT2 summarizes card-type and effect populations plus item/event/condition/visual join coverage; BTANINIT/BTANINIT2 summarize effect modes/resources, six-slot timeline geometry, delays, duration, skill/weapon joins, and unreferenced authoring. Name tables report message coverage for title/description, summary/strategy, biography, and description-only layouts; 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; banked tables summarize populated/reserved banks, movement/battle steps and providers, selector-specific semantic coverage, and overwrite conflicts. `--message-query REGEX` searches every supported message field 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, CCINIT rule, SCINIT dispatch, RTINIT banked, ILINIT condition-matrix, CNINIT unit-name/voice-family, CGINIT gallery, ALINIT alchemy-recipe, AFINIT affinity/progression, CTINIT name-entry-palette, CVINIT character-voice, TRINIT training-action, CDINIT card-generation, CDINIT2 card-effect, BTANINIT/BTANINIT2 battle-animation, and MPINIT terrain-atlas extraction; RECOVER ABI validation; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked/map/training/card/battle-animation profiling. | run each directly | — | +| `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules / dispatch / banked 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. Dedicated name-mode schemas recover VIINIT's pre-title glossary prerequisites, CIINIT's profile registry, and STINIT2's corrected 74-row stage catalog plus six-line pre/post-clear text matrix, availability gates, geometry, rewards, SCJUMP joins, and loader resources. Footer-mode MPINIT recovers its sparse 53-column doubled-coordinate terrain atlas, joins 66 STINIT2 stage rectangles, and resolves terrain ids through LAINIT. 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. Banked mode decodes RTINIT's twenty parallel 1000-by-20 movement/battle routine banks, retains all assignments and overwrites, assembles final per-slot steps, resolves provider selectors to RTN_M/RTN_B scripts, and applies selector-scoped schemas where proven. Current schemas cover all nineteen used providers, RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061, including progress-only, randomized roaming, object-slot, coordinate, enemy/ally, treasure and Magic Pillar search, Healing Feather, waypoint, faction-terrain, retreat, reachable normal-attack routing, immediate offensive target/action selection, and immediate allied healing. Parameterless behavior schemas publish an empty `parameter_fields` mapping while still joining `provider_behavior`, target selection, action selection, and completion rules. Provider defaults such as M004's unwritten slot 0, M010's zero/HP resource index, and M013's any-foreign-faction filter are projected explicitly and counted separately from populated source cells. Authored cells proven unread by M001/M008 remain under per-step `ignored_movement_parameters` and contribute to top-level `ignored_movement_parameter_count`; they are not silently discarded or assigned invented meanings. Raw provider banks remain intact, and top-level `movement_provider_parameter_schemas` documents the projection. ITINIT, SKINIT, VIINIT, EBINIT, and CIINIT join their matching MES text. 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, ITINIT/SCINIT and `build/callscript-names.json` for STINIT2, STINIT2/LAINIT for MPINIT, CDINIT2/STINIT for CDINIT, ITINIT/SCINIT/ILINIT and `build/callscript-names.json` for CDINIT2, BTANINIT/SKINIT/ITINIT and `build/callscript-names.json` for BTANINIT2, BTANINIT2 and `build/callscript-names.json` for BTANINIT, `build/callscript-names.json` for RTINIT/SCINIT, `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/footer/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. MPINIT's footer specialization summarizes atlas geometry, implicit-zero rows, terrain ids, stage-rectangle joins, sharing, and border cells; STINIT2 summarizes corrected record/text geometry, mapped/event/main/EX populations, story gates, SCJUMP/loader joins, rewards, and its unresolved column; CDINIT summarizes selector/list sizes, CDINIT2 joins and story gates, STINIT references, and the runtime scan/clear geometry; CDINIT2 summarizes card-type and effect populations plus item/event/condition/visual join coverage; BTANINIT/BTANINIT2 summarize effect modes/resources, six-slot timeline geometry, delays, duration, skill/weapon joins, and unreferenced authoring. Name tables report message coverage for title/description, summary/strategy, biography, and description-only layouts; 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; banked tables summarize populated/reserved banks, movement/battle steps and providers, selector-specific semantic coverage, and overwrite conflicts. `--message-query REGEX` searches every supported message field 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, CCINIT rule, SCINIT dispatch, RTINIT banked, ILINIT condition-matrix, CNINIT unit-name/voice-family, CGINIT gallery, ALINIT alchemy-recipe, AFINIT affinity/progression, CTINIT name-entry-palette, CVINIT character-voice, TRINIT training-action, CDINIT card-generation, CDINIT2 card-effect, BTANINIT/BTANINIT2 battle-animation, STINIT2 stage-definition, and MPINIT terrain-atlas extraction; RECOVER ABI validation; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked/map/training/card/battle-animation/stage 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}` | `extract_init.py` also has name-mode specializations for VIINIT's sparse 200-row glossary table, @@ -149,6 +149,14 @@ resolve through SYS4INI. The schema records BTL's movie/sprite playback and acto contract, the engine-dead authored atlas-row count, and sixteen definitions not referenced by BTANINIT2. +Name-mode STINIT2 emits 74 sparse stage definitions rather than the generic view's 321 apparent +records. It recognizes the separate `string[1000]` title array and `string[1000][6]` description +matrix, retaining three uncleared and three cleared lines per stage. The schema joins numbered, +event-only, and EX presentation; main-progression, unlock-group, and seven-column story gates; +tile/doubled-grid/minimap geometry; point and bronze/silver/gold coin rewards; all 174 +entry/clear/failure decisions through SCINIT; and all 74 loader ids to STINIT.BIN. The 66 populated +cells at `0xedc4d` remain raw and explicitly unresolved because no script reads the column. + Footer-mode MPINIT classifies all 1,472 copies as fifty-cell rows within one stride-53 terrain atlas. It derives grid Y from each destination, retains 127 implicit-zero gaps, and joins STINIT2's inclusive tile bounds after the consumer-proven two-times coordinate conversion. The output contains 66 named diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index a76071b..a143fc6 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -53,7 +53,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 and rule sources → JSON.** `tools/extract_init.py` auto-detects shape (`name`/`numeric`/`footer`/`mixed`/`rules`/`dispatch`/`banked`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions), CIINIT (24 character-information profiles)** [name: sparse one-based name/description/fields, with CIINIT's dedicated profile registry], **CGINIT (851 sparse gallery images)** [numeric: full image, optional save/stage preview, thumbnail sheet/slot, and variant ordinal in a reserved 2,000-row layout], **ALINIT (107 sparse alchemy recipes)** [numeric: output, level/story gates, point cost, and four paired ingredient slots in a reserved 1,000-row layout], **AFINIT (13 authored affinity rows plus tuning/facility curves)** [name/footer specialization], **CTINIT (five 70-cell name-entry pages)** [string-matrix specialization], **CVINIT (13 character-voice settings)** [numeric: preview assets, slot-to-unit joins, and inverse suppression-setting map], **LAINIT (20 shipped terrain definitions)** [name: sparse strings, topology/rendering, combat-stat, traversal-skill, and texture-fallback joins], **MPINIT (1,472 sparse terrain rows joined to 66 stage maps)** [footer: 53-column doubled-coordinate atlas], **SPINIT (eight 15-slot H-scene gallery pages)** [numeric: INIT2 thumbnail-sheet and SYS4INI scene joins], **TRINIT (21 training actions)** [name/string-matrix: eligibility, cost, stat/alignment/progress effects, rewards, and ten event slots], **CDINIT (nine weighted card-generation lists)** [numeric/dispatch specialization], **CDINIT2 (81 card definitions)** [name: story gates, typed effects, and item/event/condition/visual joins], **BTANINIT (202 battle effects)** [numeric/dispatch: visual/audio/atlas/hit-pulse work records], **BTANINIT2 (122 battle-animation timelines)** [numeric: six effect ids, delays, duration, and skill/weapon joins], **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], **SCINIT (1,209 final decision rows)** [dispatch: scene resource plus authored chapter metadata and overwrite history], and **RTINIT (172 routine sets)** [banked: twenty movement/battle step banks, provider joins, and overwrite history]. 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`/`banked`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions), CIINIT (24 character-information profiles)** [name: sparse one-based name/description/fields, with CIINIT's dedicated profile registry], **CGINIT (851 sparse gallery images)** [numeric: full image, optional save/stage preview, thumbnail sheet/slot, and variant ordinal in a reserved 2,000-row layout], **ALINIT (107 sparse alchemy recipes)** [numeric: output, level/story gates, point cost, and four paired ingredient slots in a reserved 1,000-row layout], **AFINIT (13 authored affinity rows plus tuning/facility curves)** [name/footer specialization], **CTINIT (five 70-cell name-entry pages)** [string-matrix specialization], **CVINIT (13 character-voice settings)** [numeric: preview assets, slot-to-unit joins, and inverse suppression-setting map], **LAINIT (20 shipped terrain definitions)** [name: sparse strings, topology/rendering, combat-stat, traversal-skill, and texture-fallback joins], **MPINIT (1,472 sparse terrain rows joined to 66 stage maps)** [footer: 53-column doubled-coordinate atlas], **SPINIT (eight 15-slot H-scene gallery pages)** [numeric: INIT2 thumbnail-sheet and SYS4INI scene joins], **TRINIT (21 training actions)** [name/string-matrix: eligibility, cost, stat/alignment/progress effects, rewards, and ten event slots], **CDINIT (nine weighted card-generation lists)** [numeric/dispatch specialization], **CDINIT2 (81 card definitions)** [name: story gates, typed effects, and item/event/condition/visual joins], **BTANINIT (202 battle effects)** [numeric/dispatch: visual/audio/atlas/hit-pulse work records], **BTANINIT2 (122 battle-animation timelines)** [numeric: six effect ids, delays, duration, and skill/weapon joins], **STINIT2 (74 stage definitions)** [name/string-matrix: descriptions, gates, geometry, rewards, and flow joins], **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], **SCINIT (1,209 final decision rows)** [dispatch: scene resource plus authored chapter metadata and overwrite history], and **RTINIT (172 routine sets)** [banked: twenty movement/battle step banks, provider joins, 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. @@ -68,8 +68,9 @@ - [x] **2.3k — Extract `CDINIT`'s card-generation lists.** Numeric/dispatch specialization classifies all 1,565 instructions into nine selectors and 383 weighted candidates, joins every card to CDINIT2, and links seven live selectors to 246 STINIT card objects. FIELD proves the turn-scaled cumulative selection rule, 100-slot scan, 50-slot clear prefix, and two-column effective required-story-gate contract. - [x] **2.3l — Extract `CDINIT2`'s card definitions.** Name specialization classifies all 558 instructions into 81 cards inside a reserved 100-row registry. FIELD proves six type-specific effect paths and the engine-dead third required-flag column; ITINIT, SCINIT, ILINIT, and SYS4INI resolve all item, event, condition, and visual references, while STAGECLEAR proves the deferred point-award accumulator. - [x] **2.3m — Extract `BTANINIT`/`BTANINIT2`'s battle-animation registry.** BTANINIT2 specialization classifies all 1,018 instructions into 122 sparse six-effect timelines, 573 effect references, paired start delays, and 111 complete durations. BTANINIT dispatch specialization classifies all 4,472 instructions into 202 visual/audio/hit-pulse definitions. BTL proves movie/sprite playback, blend, anchor, geometry, sound, and pulse semantics; SKINIT/ITINIT resolve skill and weapon-class selection. +- [x] **2.3n — Extract `STINIT2`'s stage-definition registry.** Name/string-matrix specialization corrects the generic 321-record view to 74 sparse stages plus 296 strings in six pre/post-clear description slots, classifying all 1,634 instructions. FORT, FIELD, SELSTAGE, and STAGECLEAR prove main/EX flags, seven-column story gates, unlock groups, numbered/event presentation, map/minimap geometry, base point and three-coin rewards, and the loader contract. All 174 entry/clear/failure decisions resolve through SCINIT and all 74 loader ids resolve to STINIT; only the script-unconsumed `0xedc4d` column remains unnamed. - [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: RTINIT is closed at 1,043/1,043 movement steps; the RECOVER/ILINIT condition ABI, ALINIT recipes, AFINIT affinity/progression tables, CTINIT name palette, CVINIT character-voice registry, LAINIT terrain definitions, MPINIT stage-terrain atlas, SPINIT H-scene gallery, TRINIT training actions, CDINIT/CDINIT2 card selection/effects, and BTANINIT/BTANINIT2 battle animations are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, audit STINIT2's remaining stage-definition columns and consumers, then use Frida only for semantics static consumers cannot settle. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids, SCJUMP decision-to-scene dispatch, the shipped RTINIT movement-provider join, and the six completed message joins are resolved. +- [ ] **2.5 — Grow the global-var map (future, incremental).** Static first: RTINIT is closed at 1,043/1,043 movement steps; the RECOVER/ILINIT condition ABI, ALINIT recipes, AFINIT affinity/progression tables, CTINIT name palette, CVINIT character-voice registry, LAINIT terrain definitions, MPINIT stage-terrain atlas, SPINIT H-scene gallery, TRINIT training actions, CDINIT/CDINIT2 card selection/effects, BTANINIT/BTANINIT2 battle animations, and STINIT2's complete script-visible stage surface are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, use live/native observation for STINIT2's sole script-unconsumed `0xedc4d` column, then continue remaining consumer-led 2D table naming. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids, SCJUMP decision-to-scene dispatch, the shipped RTINIT movement-provider join, and the six completed message joins are resolved. ## 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 593582f..26baad3 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -47,6 +47,11 @@ occupy three reserved 1,000-row arrays for six effect ids, six start delays, and one total duration. BTANINIT dispatches 202 effect ids into BTL's six-slot visual/audio/hit-pulse work record. +STINIT2 is a special name-mode registry: 74 stages occupy a sparse 1,000-row +catalog with six description lines (three before clear and three after), +availability/story gates, map and minimap geometry, entry/clear/failure +SCJUMP decisions, clear rewards, and a shared STINIT stage-loader reference. + 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. @@ -678,6 +683,40 @@ MAP_STAGE_MAX_X = 0xEC8C5 MAP_STAGE_MIN_Y = 0xECCAD MAP_STAGE_MAX_Y = 0xED095 +STAGE_DEFINITION_NAME_BASE = 0x27BD +STAGE_DESCRIPTION_BASE = 0x2BA5 +STAGE_DEFINITION_CAPACITY = 1000 +STAGE_DESCRIPTION_STRIDE = 6 +STAGE_DESCRIPTION_COLUMNS = ( + "uncleared_line_1", + "uncleared_line_2", + "uncleared_line_3", + "cleared_line_1", + "cleared_line_2", + "cleared_line_3", +) +STAGE_DEFINITION_ARRAYS = { + "unlock_group_id": (0xE7E8D, 1), + "main_progression_flag": (0xE8275, 1), + "forbidden_story_flag_ids": (0xE865D, 7), + "required_story_flag_ids": (0xEA1B5, 7), + "display_number_major": (0xEBD0D, 1), + "display_number_minor": (0xEC0F5, 1), + "map_min_tile_x": (MAP_STAGE_MIN_X, 1), + "map_max_tile_x": (MAP_STAGE_MAX_X, 1), + "map_min_tile_y": (MAP_STAGE_MIN_Y, 1), + "map_max_tile_y": (MAP_STAGE_MAX_Y, 1), + "minimap_atlas_origin_y": (0xED47D, 1), + "clear_base_spendable_point_reward": (0xED865, 1), + "unresolved_parameter_0xedc4d": (0xEDC4D, 1), + "scjump_decision_ids": (0xEE035, 3), + "extra_dungeon_flag": (0xEEBED, 1), + "clear_coin_quantities": (0xEEFD5, 3), + "stage_loader_script_id": (0xEFB8D, 1), +} +STAGE_SCJUMP_COLUMNS = ("entry", "clear", "failure") +STAGE_CLEAR_COIN_ITEM_IDS = (91, 92, 93) + TERRAIN_NAME_BASE = 0x26B5 TERRAIN_EFFECT_DESCRIPTION_BASE = 0x26D3 TERRAIN_TEXTURE_SLOT_BASE = 0xE6AA4 @@ -5804,38 +5843,465 @@ def extract_card_generation_lists(scr): } +def extract_stage_definitions(scr): + """Extract STINIT2's sparse stage catalog and six-line text matrix.""" + records_by_id: dict[int, dict] = {} + numeric_cells = { + field_name: {} + for field_name in STAGE_DEFINITION_ARRAYS + } + classified_offsets = set() + string_write_count = 0 + static_write_count = 0 + + def record_for(stage_id: int) -> dict: + if not (1 <= stage_id < STAGE_DEFINITION_CAPACITY): + raise ValueError( + f"{scr.path.name}: stage id {stage_id} outside reserved " + f"1..{STAGE_DEFINITION_CAPACITY - 1} range" + ) + return records_by_id.setdefault(stage_id, { + "id": stage_id, + "name": "", + "string_fields": {}, + "fields": {}, + "record_fields": {}, + }) + + for ins in scr.instructions: + if ( + ins.opcode == SET_STRING + and len(ins.args) >= 2 + and ins.args[0][0] == T_GLOBAL_STRING + ): + destination = ins.args[0][1] + text = scr.strings.get(ins.args[1][1], (None,))[0] + if ( + STAGE_DEFINITION_NAME_BASE < destination + < STAGE_DEFINITION_NAME_BASE + + STAGE_DEFINITION_CAPACITY + ): + stage_id = destination - STAGE_DEFINITION_NAME_BASE + record = record_for(stage_id) + if record["name"]: + raise ValueError( + f"{scr.path.name}: duplicate stage name for id " + f"{stage_id}" + ) + record["name"] = text + else: + relative = destination - STAGE_DESCRIPTION_BASE + if not ( + STAGE_DESCRIPTION_STRIDE + <= relative + < STAGE_DEFINITION_CAPACITY + * STAGE_DESCRIPTION_STRIDE + ): + raise ValueError( + f"{scr.path.name}: unexpected string write " + f"0x{destination:x}" + ) + stage_id, column = divmod( + relative, STAGE_DESCRIPTION_STRIDE + ) + record = record_for(stage_id) + _store_unique( + record["string_fields"], + ( + f"0x{STAGE_DESCRIPTION_BASE:x}/" + f"{STAGE_DESCRIPTION_STRIDE}/{column}" + ), + text, + stage_id, + ) + string_write_count += 1 + classified_offsets.add(ins.offset) + continue + + write = _static_global_write(ins) + if write is not None: + destination, value = write + matches = [] + for field_name, (base, stride) in ( + STAGE_DEFINITION_ARRAYS.items() + ): + relative = destination - base + if ( + stride <= relative + < STAGE_DEFINITION_CAPACITY * stride + ): + stage_id, column = divmod(relative, stride) + matches.append( + (field_name, base, stride, stage_id, column) + ) + if len(matches) != 1: + raise ValueError( + f"{scr.path.name}: numeric destination " + f"0x{destination:x} matched {matches}" + ) + field_name, base, stride, stage_id, column = matches[0] + record = record_for(stage_id) + numeric_cells[field_name][(stage_id, column)] = value + key = ( + f"0x{base:x}" + if stride == 1 + else f"0x{base:x}/{stride}/{column}" + ) + target = ( + record["fields"] + if stride == 1 + else record["record_fields"] + ) + _store_unique(target, key, value, stage_id) + static_write_count += 1 + classified_offsets.add(ins.offset) + continue + + if sys4load.display_label(ins.opcode) == "exit": + classified_offsets.add(ins.offset) + + unclassified = [ + f"0x{ins.offset:x}" + for ins in scr.instructions + if ins.offset not in classified_offsets + ] + if unclassified: + raise ValueError( + f"{scr.path.name}: unclassified instructions " + + ", ".join(unclassified) + ) + + unnamed = sorted( + stage_id + for stage_id, record in records_by_id.items() + if not record["name"] + ) + if unnamed: + raise ValueError( + f"{scr.path.name}: numeric/text rows without names: {unnamed}" + ) + + item_records, _ = extract_name( + sys4load.load(resolve("ITINIT")) + ) + item_names = { + record["id"]: record["name"] for record in item_records + } + dispatch_records, _ = extract_dispatch( + sys4load.load(resolve("SCINIT")) + ) + dispatch_by_id = { + record["id"]: record for record in dispatch_records + } + resource_names = callscript_names() + + def values(field_name: str, stage_id: int) -> list[int]: + _, stride = STAGE_DEFINITION_ARRAYS[field_name] + return [ + numeric_cells[field_name].get((stage_id, column), 0) + for column in range(stride) + ] + + def scalar(field_name: str, stage_id: int) -> int: + return values(field_name, stage_id)[0] + + records = [] + scjump_reference_count = 0 + resolved_scjump_reference_count = 0 + resolved_loader_script_count = 0 + description_line_count = 0 + for stage_id in sorted(records_by_id): + record = records_by_id[stage_id] + descriptions = [ + record["string_fields"].get( + ( + f"0x{STAGE_DESCRIPTION_BASE:x}/" + f"{STAGE_DESCRIPTION_STRIDE}/{column}" + ), + "", + ) + for column in range(STAGE_DESCRIPTION_STRIDE) + ] + description_line_count += sum(bool(text) for text in descriptions) + record["descriptions"] = { + "uncleared": descriptions[:3], + "cleared": descriptions[3:], + } + + major = scalar("display_number_major", stage_id) + minor = scalar("display_number_minor", stage_id) + if major < 0: + display_kind = "extra" + elif not major and not minor: + display_kind = "event" + else: + display_kind = "numbered" + record["display_number"] = { + "kind": display_kind, + "major": major, + "minor": minor, + } + + required_flags = [ + value + for value in values( + "required_story_flag_ids", stage_id + ) + if value + ] + forbidden_flags = [ + value + for value in values( + "forbidden_story_flag_ids", stage_id + ) + if value + ] + record["availability"] = { + "main_progression": bool( + scalar("main_progression_flag", stage_id) + ), + "extra_dungeon": bool( + scalar("extra_dungeon_flag", stage_id) + ), + "unlock_group_id": scalar( + "unlock_group_id", stage_id + ), + "required_story_flag_ids": required_flags, + "forbidden_story_flag_ids": forbidden_flags, + } + + bounds = { + "min_x": scalar("map_min_tile_x", stage_id), + "max_x": scalar("map_max_tile_x", stage_id), + "min_y": scalar("map_min_tile_y", stage_id), + "max_y": scalar("map_max_tile_y", stage_id), + } + if all(bounds.values()): + record["map"] = { + "tile_bounds": bounds, + "grid_bounds": { + key: value * MAP_TILE_TO_GRID_SCALE + for key, value in bounds.items() + }, + "minimap_atlas_origin_y": scalar( + "minimap_atlas_origin_y", stage_id + ), + } + + scjump_ids = values("scjump_decision_ids", stage_id) + flow = {} + for column, role in enumerate(STAGE_SCJUMP_COLUMNS): + decision_id = scjump_ids[column] + if not decision_id: + continue + joined = dispatch_by_id.get(decision_id, {}) + flow[f"{role}_scjump_decision_id"] = decision_id + flow[f"{role}_script_name"] = joined.get( + "script_name", "" + ) + scjump_reference_count += 1 + if joined: + resolved_scjump_reference_count += 1 + loader_id = scalar("stage_loader_script_id", stage_id) + if loader_id: + loader_name = resource_names.get(loader_id, "") + flow["stage_loader_script_id"] = loader_id + flow["stage_loader_script_name"] = loader_name + if loader_name: + resolved_loader_script_count += 1 + record["flow"] = flow + + coin_quantities = values( + "clear_coin_quantities", stage_id + ) + record["clear_rewards"] = { + "base_spendable_points": scalar( + "clear_base_spendable_point_reward", stage_id + ), + "coins": [ + { + "item_id": item_id, + "item_name": item_names.get(item_id, ""), + "quantity": coin_quantities[column], + } + for column, item_id in enumerate( + STAGE_CLEAR_COIN_ITEM_IDS + ) + if coin_quantities[column] + ], + } + unresolved = scalar( + "unresolved_parameter_0xedc4d", stage_id + ) + if unresolved: + record["unresolved_parameter_0xedc4d"] = unresolved + records.append(record) + + field_counts = { + field_name: len(cells) + for field_name, cells in numeric_cells.items() + } + schema_semantics = { + f"0x{STAGE_DESCRIPTION_BASE:x}/" + f"{STAGE_DESCRIPTION_STRIDE}/{column}": ( + f"stage_description_{column_name}" + ) + for column, column_name in enumerate( + STAGE_DESCRIPTION_COLUMNS + ) + } + semantic_names = { + "unlock_group_id": "stage_unlock_group_ids", + "main_progression_flag": "stage_main_progression_flags", + "forbidden_story_flag_ids": ( + "stage_forbidden_story_flag_ids" + ), + "required_story_flag_ids": "stage_required_story_flag_ids", + "display_number_major": "stage_display_number_major", + "display_number_minor": "stage_display_number_minor", + "map_min_tile_x": "stage_map_min_tile_x", + "map_max_tile_x": "stage_map_max_tile_x", + "map_min_tile_y": "stage_map_min_tile_y", + "map_max_tile_y": "stage_map_max_tile_y", + "minimap_atlas_origin_y": "stage_minimap_atlas_origin_y", + "clear_base_spendable_point_reward": ( + "stage_clear_base_spendable_point_rewards" + ), + "scjump_decision_ids": "stage_scjump_decision_ids", + "extra_dungeon_flag": "stage_extra_dungeon_flags", + "clear_coin_quantities": "stage_clear_coin_quantities", + "stage_loader_script_id": "stage_loader_script_ids", + } + semantic_columns = { + "forbidden_story_flag_ids": tuple( + f"forbidden_flag_{column + 1}" + for column in range( + STAGE_DEFINITION_ARRAYS[ + "forbidden_story_flag_ids" + ][1] + ) + ), + "required_story_flag_ids": tuple( + f"required_flag_{column + 1}" + for column in range( + STAGE_DEFINITION_ARRAYS[ + "required_story_flag_ids" + ][1] + ) + ), + "scjump_decision_ids": STAGE_SCJUMP_COLUMNS, + "clear_coin_quantities": tuple( + f"{coin_name}_coin_item_{item_id}" + for coin_name, item_id in zip( + ("bronze", "silver", "gold"), + STAGE_CLEAR_COIN_ITEM_IDS, + ) + ), + } + for field_name, semantic_name in semantic_names.items(): + base, stride = STAGE_DEFINITION_ARRAYS[field_name] + if stride == 1: + schema_semantics[f"0x{base:x}"] = semantic_name + else: + column_names = semantic_columns[field_name] + for column, column_name in enumerate(column_names): + schema_semantics[ + f"0x{base:x}/{stride}/{column}" + ] = f"{semantic_name}.{column_name}" + + return records, { + "schema": "stage-definitions", + "reserved_record_count": STAGE_DEFINITION_CAPACITY, + "name_array_base": f"0x{STAGE_DEFINITION_NAME_BASE:x}", + "description_array_base": ( + f"0x{STAGE_DESCRIPTION_BASE:x}" + ), + "description_columns": list(STAGE_DESCRIPTION_COLUMNS), + "record_field_columns": sorted({ + key + for record in records + for key in record["record_fields"] + }, key=lambda key: tuple( + int(part, 0) for part in key.split("/") + )), + "string_field_columns": sorted({ + key + for record in records + for key in record["string_fields"] + }, key=lambda key: tuple( + int(part, 0) for part in key.split("/") + )), + "schema_field_semantics": schema_semantics, + "string_write_count": string_write_count, + "static_write_count": static_write_count, + "classified_instruction_count": len(classified_offsets), + "authored_numeric_cell_counts": field_counts, + "description_line_count": description_line_count, + "mapped_stage_count": sum("map" in record for record in records), + "event_only_stage_count": sum( + record["display_number"]["kind"] == "event" + for record in records + ), + "main_progression_stage_count": sum( + record["availability"]["main_progression"] + for record in records + ), + "extra_dungeon_stage_count": sum( + record["availability"]["extra_dungeon"] + for record in records + ), + "story_flag_gated_stage_count": sum( + bool(record["availability"]["required_story_flag_ids"]) + or bool( + record["availability"]["forbidden_story_flag_ids"] + ) + for record in records + ), + "scjump_reference_count": scjump_reference_count, + "resolved_scjump_reference_count": ( + resolved_scjump_reference_count + ), + "resolved_loader_script_count": resolved_loader_script_count, + "clear_coin_reward_cell_count": field_counts[ + "clear_coin_quantities" + ], + "unresolved_parameter_population": field_counts[ + "unresolved_parameter_0xedc4d" + ], + "consumer_contract": { + "FORT.BIN": ( + "enumerates named rows, applies required/forbidden story " + "flags, auto-selects available main-progression stages, " + "renders stage numbers and coin rewards, and dispatches the " + "entry SCJUMP decision" + ), + "FIELD.BIN": ( + "calls the selected row's STINIT loader, initializes map and " + "minimap geometry, propagates unlock groups, awards base " + "spendable points, and dispatches clear/failure decisions" + ), + "SELSTAGE.BIN": ( + "renders the six description lines selected by clear state, " + "draws the numbered/EVENT/EX labels and clear rewards, and " + "uses the minimap atlas origin to crop the selected map" + ), + }, + } + + def _map_stage_definitions() -> list[dict]: """Read the STINIT2 records that own all four terrain-atlas bounds.""" stage_scr = sys4load.load(resolve("STINIT2")) - stage_records, _ = extract_name(stage_scr) - bounds = ( - MAP_STAGE_MIN_X, - MAP_STAGE_MAX_X, - MAP_STAGE_MIN_Y, - MAP_STAGE_MAX_Y, - ) + stage_records, _ = extract_stage_definitions(stage_scr) definitions = [] for record in stage_records: - fields = record.get("fields", {}) - keys = [f"0x{address:x}" for address in bounds] - if not all(key in fields for key in keys): + if "map" not in record: continue - min_x, max_x, min_y, max_y = (fields[key] for key in keys) definitions.append({ "id": record["id"], "name": record.get("name", ""), - "tile_bounds": { - "min_x": min_x, - "max_x": max_x, - "min_y": min_y, - "max_y": max_y, - }, - "grid_bounds": { - "min_x": min_x * MAP_TILE_TO_GRID_SCALE, - "max_x": max_x * MAP_TILE_TO_GRID_SCALE, - "min_y": min_y * MAP_TILE_TO_GRID_SCALE, - "max_y": max_y * MAP_TILE_TO_GRID_SCALE, - }, + "tile_bounds": record["map"]["tile_bounds"], + "grid_bounds": record["map"]["grid_bounds"], }) return definitions @@ -6439,6 +6905,13 @@ def write_data_index(data_dir: Path) -> None: "duration. BTANINIT's paired schema decodes 202 effect ids into BTL's six-slot", "movie/sprite, blend, geometry, audio, and hit-pulse work record.", "", + "STINIT2's dedicated stage-definition schema exposes 74 sparse rows in a", + "reserved 1,000-stage catalog. It preserves six pre/post-clear description", + "slots, progression and story gates, numbered/EVENT/EX presentation, map and", + "minimap geometry, point/coin rewards, and all 174 SCINIT-resolved entry, clear,", + "and failure decisions. All rows resolve their shared STINIT loader reference;", + "the unconsumed 0xedc4d column remains explicit rather than receiving a guess.", + "", "MPINIT's dedicated terrain-atlas schema exposes 1,472 authored rows of a sparse", "53-column half-tile grid. It joins STINIT2's doubled tile-bound rectangles to 66", "stage definitions, preserves implicit-zero rows and raw footer provenance, and", @@ -6552,6 +7025,8 @@ def main() -> int: extractor = extract_battle_effect_definitions elif mode == "numeric" and name == "BTANINIT2": extractor = extract_battle_animations + elif mode == "name" and name == "STINIT2": + extractor = extract_stage_definitions elif mode == "numeric" and name == "SPINIT": extractor = extract_h_scene_gallery elif mode == "footer" and name == "MPINIT": diff --git a/tools/init_table_profile.py b/tools/init_table_profile.py index 06c5565..ce0f824 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -578,6 +578,49 @@ def profile_battle_animations(data: dict) -> dict: } +def profile_stage_definitions(data: dict) -> dict: + """Summarize STINIT2's stage catalog, text, gates, and flow joins.""" + if data.get("schema") != "stage-definitions": + return {} + return { + "stage_count": data.get("record_count", 0), + "reserved_record_count": data.get( + "reserved_record_count", 0 + ), + "description_line_count": data.get( + "description_line_count", 0 + ), + "mapped_stage_count": data.get("mapped_stage_count", 0), + "event_only_stage_count": data.get( + "event_only_stage_count", 0 + ), + "main_progression_stage_count": data.get( + "main_progression_stage_count", 0 + ), + "extra_dungeon_stage_count": data.get( + "extra_dungeon_stage_count", 0 + ), + "story_flag_gated_stage_count": data.get( + "story_flag_gated_stage_count", 0 + ), + "scjump_reference_count": data.get( + "scjump_reference_count", 0 + ), + "resolved_scjump_reference_count": data.get( + "resolved_scjump_reference_count", 0 + ), + "resolved_loader_script_count": data.get( + "resolved_loader_script_count", 0 + ), + "clear_coin_reward_cell_count": data.get( + "clear_coin_reward_cell_count", 0 + ), + "unresolved_parameter_population": data.get( + "unresolved_parameter_population", 0 + ), + } + + def profile_messages(data: dict) -> dict: """Summarize the joined player-facing message evidence.""" records = data["records"] @@ -752,6 +795,32 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str: f"- unjoined authored rows: " f"{animation_profile['unjoined_authored_animation_ids']}", ]) + elif stage_profile := profile_stage_definitions(data): + lines.extend([ + f"- stage definitions: {stage_profile['stage_count']}/" + f"{stage_profile['reserved_record_count']} rows", + f"- description lines: " + f"{stage_profile['description_line_count']} across six " + f"pre/post-clear slots", + f"- mapped/event-only stages: " + f"{stage_profile['mapped_stage_count']}/" + f"{stage_profile['event_only_stage_count']}", + f"- main-progression/EX stages: " + f"{stage_profile['main_progression_stage_count']}/" + f"{stage_profile['extra_dungeon_stage_count']}", + f"- story-flag-gated stages: " + f"{stage_profile['story_flag_gated_stage_count']}", + f"- SCJUMP joins: " + f"{stage_profile['resolved_scjump_reference_count']}/" + f"{stage_profile['scjump_reference_count']}", + f"- stage-loader joins: " + f"{stage_profile['resolved_loader_script_count']}/" + f"{stage_profile['stage_count']}", + f"- clear coin reward cells: " + f"{stage_profile['clear_coin_reward_cell_count']}", + f"- unresolved 0xedc4d cells: " + f"{stage_profile['unresolved_parameter_population']}", + ]) elif definition_profile := profile_card_definitions(data): lines.extend([ f"- card definitions: {definition_profile['card_count']}/" @@ -989,6 +1058,7 @@ def main() -> int: "training_action_profile": profile_training_actions(data), "card_generation_profile": profile_card_generation_lists(data), "card_definition_profile": profile_card_definitions(data), + "stage_definition_profile": profile_stage_definitions(data), "columns": sorted(rows, key=lambda row: ( int(row["base"], 16), row["stride"] or 0, row["column"] or 0 )), diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index adf6ae5..6dfa341 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -1973,6 +1973,118 @@ def test_battle_animations() -> None: ) +def test_stage_definitions() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["STINIT2.BIN"]) + check( + extract_init.detect_mode(script) == "name", + "STINIT2 remains compatible with name-mode auto-detection", + ) + records, meta = extract_init.extract_stage_definitions(script) + by_id = {record["id"]: record for record in records} + check( + len(records) == 74 + and meta["reserved_record_count"] == 1000 + and meta["mapped_stage_count"] == 66 + and meta["event_only_stage_count"] == 8, + "STINIT2 separates 74 stage rows into mapped and event-only records", + ) + check( + meta["string_write_count"] == 370 + and meta["description_line_count"] == 296 + and meta["static_write_count"] == 1263 + and meta["classified_instruction_count"] == 1634, + "STINIT2 classifies every name, description, numeric, and exit instruction", + ) + check( + by_id[1]["descriptions"]["uncleared"] + == [ + "庭園につながっている地下空洞。", + "浅い階層なので魔物も少ないようだ。", + "", + ] + and by_id[1]["descriptions"]["cleared"] + == [ + "庭園につながっている地下空洞。", + "浅い階層なので魔物も少ないようだ。", + "", + ], + "STINIT2 preserves the six pre/post-clear description slots", + ) + check( + by_id[1]["display_number"] + == {"kind": "numbered", "major": 0, "minor": 1} + and by_id[3]["display_number"]["kind"] == "event" + and by_id[160]["display_number"]["kind"] == "extra", + "STINIT2 distinguishes numbered, event-only, and EX display labels", + ) + check( + by_id[3]["flow"]["entry_scjump_decision_id"] == 150 + and by_id[3]["flow"]["entry_script_name"] == "SC0150.BIN" + and by_id[1]["flow"]["clear_script_name"] == "SC0010.BIN" + and by_id[1]["flow"]["failure_script_name"] == "SC0000.BIN" + and meta["resolved_scjump_reference_count"] == 174, + "STINIT2 resolves every entry, clear, and failure SCJUMP reference", + ) + check( + by_id[1]["flow"]["stage_loader_script_name"] == "STINIT.BIN" + and meta["resolved_loader_script_count"] == 74, + "STINIT2 resolves every shared stage-loader reference", + ) + check( + by_id[32]["availability"]["unlock_group_id"] == 32 + and by_id[33]["availability"]["unlock_group_id"] == 32 + and by_id[34]["availability"]["unlock_group_id"] == 32 + and meta["main_progression_stage_count"] == 49 + and meta["extra_dungeon_stage_count"] == 8, + "STINIT2 exposes shared unlock groups and progression/EX flags", + ) + check( + by_id[3]["availability"]["required_story_flag_ids"] + == [151, 23, 1743, 1886] + and by_id[3]["availability"]["forbidden_story_flag_ids"] + == [21] + and meta["story_flag_gated_stage_count"] == 74, + "STINIT2 preserves all seven-column story eligibility rows", + ) + check( + by_id[32]["map"]["tile_bounds"] + == {"min_x": 1, "max_x": 25, "min_y": 116, "max_y": 136} + and by_id[32]["map"]["grid_bounds"] + == {"min_x": 2, "max_x": 50, "min_y": 232, "max_y": 272} + and by_id[32]["map"]["minimap_atlas_origin_y"] == 66, + "STINIT2 joins tile, doubled-grid, and minimap atlas geometry", + ) + check( + by_id[167]["clear_rewards"]["base_spendable_points"] == 60 + and by_id[167]["clear_rewards"]["coins"] + == [ + { + "item_id": 91, + "item_name": "ブロンズコイン", + "quantity": 3, + }, + { + "item_id": 92, + "item_name": "シルバーコイン", + "quantity": 2, + }, + { + "item_id": 93, + "item_name": "ゴールドコイン", + "quantity": 1, + }, + ] + and meta["clear_coin_reward_cell_count"] == 64, + "STINIT2 resolves the spendable-point and three coin reward columns", + ) + check( + meta["unresolved_parameter_population"] == 66 + and by_id[167]["unresolved_parameter_0xedc4d"] == 8, + "STINIT2 preserves the one still-unresolved populated stage column", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -2194,6 +2306,7 @@ if __name__ == "__main__": test_card_definitions() test_battle_effect_definitions() test_battle_animations() + test_stage_definitions() test_map_terrain_atlas() test_condition_definitions() test_field_semantics() diff --git a/tools/test_globals.py b/tools/test_globals.py index a963525..9a88a67 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -113,6 +113,25 @@ def test_load_and_lint(): and entries[0x155b7a]["name"] == "battle_effect_atlas_row_counts" and entries[0x155baa]["columns"]["2"] == "pulse_3", "BTANINIT/BTANINIT2 battle-animation state is curated") + check(entries[0x27bd]["name"] == "stage_display_names" + and entries[0x2ba5]["columns"]["5"] == "cleared_line_3" + and entries[0xe7e8d]["name"] == "stage_unlock_group_ids" + and entries[0xe8275]["name"] == "stage_main_progression_flags" + and entries[0xe865d]["columns"]["6"] == "forbidden_flag_7" + and entries[0xea1b5]["columns"]["6"] == "required_flag_7", + "STINIT2 stage text and availability state is curated") + check(entries[0xebd0d]["name"] == "stage_display_number_major" + and entries[0xec0f5]["name"] == "stage_display_number_minor" + and entries[0xed47d]["name"] == "stage_minimap_atlas_origin_y" + and entries[0xed865]["name"] + == "stage_clear_base_spendable_point_rewards", + "STINIT2 stage numbering, minimap, and point rewards are curated") + check(entries[0xee035]["columns"] + == {"0": "entry", "1": "clear", "2": "failure"} + and entries[0xeebed]["name"] == "stage_extra_dungeon_flags" + and entries[0xeefd5]["columns"]["2"] == "gold_coin_item_93" + and entries[0xefb8d]["name"] == "stage_loader_script_ids", + "STINIT2 flow, EX, coin, and loader columns are curated") check(entries[0x15a095]["name"] == "information_tab_index" and entries[0x15a096]["name"] == "information_message_handled" and entries[0x15a097]["name"] diff --git a/tools/test_init_table_profile.py b/tools/test_init_table_profile.py index fc4e56a..21db1e2 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -414,6 +414,35 @@ def main() -> int: assert "- battle animations: 3/1000 rows" in rendered_animations assert "- full/auxiliary timelines: 2/1" in rendered_animations + stage_fixture = { + "table": "STAGES", + "mode": "name", + "schema": "stage-definitions", + "record_count": 5, + "reserved_record_count": 1000, + "description_line_count": 18, + "mapped_stage_count": 4, + "event_only_stage_count": 1, + "main_progression_stage_count": 3, + "extra_dungeon_stage_count": 1, + "story_flag_gated_stage_count": 5, + "scjump_reference_count": 12, + "resolved_scjump_reference_count": 12, + "resolved_loader_script_count": 5, + "clear_coin_reward_cell_count": 4, + "unresolved_parameter_population": 4, + "records": [{}, {}, {}, {}, {}], + } + stage_summary = profile.profile_stage_definitions(stage_fixture) + assert stage_summary["mapped_stage_count"] == 4 + assert stage_summary["resolved_scjump_reference_count"] == 12 + rendered_stages = profile.render_markdown( + stage_fixture, [], 40 + ) + assert "- stage definitions: 5/1000 rows" in rendered_stages + assert "- mapped/event-only stages: 4/1" in rendered_stages + assert "- SCJUMP joins: 12/12" in rendered_stages + training_fixture = { "table": "TRAINING", "mode": "name", diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 7805dd7..30d3e9a 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -3187,6 +3187,97 @@ source = "investigation" confidence = "high" depends_on = ["0xe6aa4", "0xe7311"] +[[global]] +address = "0x27bd" +name = "stage_display_names" +category = "string-table" +type = "string[1000]" +value_domain = "STINIT2 stage title indexed by sparse stage id; 74 populated shipped rows" +usage = "FORT and SELSTAGE enumerate this sparse catalog and render the selected title, while FIELD copies the active title into its shared display buffer. STINIT2 reserves 1,000 ids even though the shipped rows end at 170." +source = "investigation" +confidence = "high" +depends_on = ["0x4dfbc", "0x2ba5"] + +[[global]] +address = "0x2ba5" +name = "stage_description_lines" +category = "string-table" +type = "string[1000][6]" +columns = { "0" = "uncleared_line_1", "1" = "uncleared_line_2", "2" = "uncleared_line_3", "3" = "cleared_line_1", "4" = "cleared_line_2", "5" = "cleared_line_3" } +value_domain = "up to three player-facing lines before clear and three after clear; 296 populated shipped cells" +usage = "SELSTAGE multiplies stage_clear_state by three and renders the corresponding three-column half of the selected STINIT2 row. The explicit 1,000-by-6 geometry prevents these strings from being mistaken for additional stage records." +source = "investigation" +confidence = "high" +depends_on = ["0x27bd", "0x7684"] + +[[global]] +address = "0xe7e8d" +name = "stage_unlock_group_ids" +category = "data-table" +type = "int[1000]" +value_domain = "zero for no group; populated group ids are 32, 35, 71, and 82" +usage = "FIELD propagates an unlocked current stage to every other stage whose nonzero group id matches. The shipped groups bind route variants 32..34, 35..36, 71..72, and 82..83." +source = "investigation" +confidence = "high" +depends_on = ["0x4dfbc"] + +[[global]] +address = "0xe8275" +name = "stage_main_progression_flags" +category = "data-table" +type = "int[1000]" +value_domain = "{0,1}; 49 populated main-progression rows" +usage = "When returning after a clear, FORT scans uncleared stage ids in descending order and auto-selects the first available row marked one. Side missions and all eight EX dungeons leave the flag zero." +source = "investigation" +confidence = "high" +depends_on = ["0x7684", "0xe865d", "0xea1b5"] + +[[global]] +address = "0xe865d" +name = "stage_forbidden_story_flag_ids" +category = "data-table" +type = "int[1000][7]" +columns = { "0" = "forbidden_flag_1", "1" = "forbidden_flag_2", "2" = "forbidden_flag_3", "3" = "forbidden_flag_4", "4" = "forbidden_flag_5", "5" = "forbidden_flag_6", "6" = "forbidden_flag_7" } +value_domain = "one-based story_event_flags ids; zero means unused" +usage = "FORT and SELSTAGE reject a stage when any populated cell resolves to story flag value one. The scripts subtract one before indexing story_event_flags, establishing the stored one-based id convention." +source = "investigation" +confidence = "high" +depends_on = ["0x6d3", "0xea1b5"] + +[[global]] +address = "0xea1b5" +name = "stage_required_story_flag_ids" +category = "data-table" +type = "int[1000][7]" +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" } +value_domain = "one-based story_event_flags ids; zero means unused" +usage = "FORT and SELSTAGE require every populated cell to resolve to story flag value one. Every shipped stage carries at least one gate, including common progression flags in columns two and three." +source = "investigation" +confidence = "high" +depends_on = ["0x6d3", "0xe865d"] + +[[global]] +address = "0xebd0d" +name = "stage_display_number_major" +category = "data-table" +type = "int[1000]" +value_domain = "-1=EX label, 0=no major component, 1..10=numbered chapter" +usage = "FORT, SELSTAGE, AIM, STAGECLEAR, and management screens render this as the major half of the stage number. A negative value renders EX, while both number components zero identify an event-only row." +source = "investigation" +confidence = "high" +depends_on = ["0xec0f5"] + +[[global]] +address = "0xec0f5" +name = "stage_display_number_minor" +category = "data-table" +type = "int[1000]" +value_domain = "0 for event-only rows; 1..8 for numbered/EX stages" +usage = "Rendered after stage_display_number_major as the minor stage number. FORT treats a row with both components zero as a direct event and dispatches its entry SCJUMP decision without entering FIELD." +source = "investigation" +confidence = "high" +depends_on = ["0xebd0d", "0xee035"] + [[global]] address = "0xccc93" name = "stage_terrain_atlas" @@ -3253,6 +3344,74 @@ source = "investigation" confidence = "high" depends_on = ["0xeccad"] +[[global]] +address = "0xed47d" +name = "stage_minimap_atlas_origin_y" +category = "data-table" +type = "int[1000]" +value_domain = "minimap atlas Y origin; 59 populated shipped cells range from 10 to 726, zero uses the atlas origin" +usage = "FIELD and SELSTAGE copy this to the minimap drawing origin. SELSTAGE subtracts it from stage_map_min_tile_y to crop the selected stage's vertically packed minimap strip and labels the following 150 atlas rows." +source = "investigation" +confidence = "high" +depends_on = ["0xeccad", "0xed095"] + +[[global]] +address = "0xed865" +name = "stage_clear_base_spendable_point_rewards" +category = "data-table" +type = "int[1000]" +value_domain = "12..60 shared spendable points across 66 mapped stages" +usage = "STAGECLEAR multiplies this base award by the turn/party-performance percentage, divides by 100, adds any card bonus, and credits the capped shared_spendable_points balance." +source = "investigation" +confidence = "high" +depends_on = ["0x671b", "0x4dfbb"] + +[[global]] +address = "0xee035" +name = "stage_scjump_decision_ids" +category = "data-table" +type = "int[1000][3]" +columns = { "0" = "entry", "1" = "clear", "2" = "failure" } +value_domain = "SCINIT/SCJUMP decision id; zero means no transition" +usage = "FORT dispatches column zero before entering a stage or for an event-only row. FIELD and a few scene return paths dispatch column one after clear and column two after failure or forced-retreat outcomes; all 174 populated references resolve through SCINIT." +source = "investigation" +confidence = "high" +depends_on = ["0x62ccf", "0x4dfbc"] + +[[global]] +address = "0xeebed" +name = "stage_extra_dungeon_flags" +category = "data-table" +type = "int[1000]" +value_domain = "{0,1}; one for stage ids 160..167" +usage = "Marks the eight shipped EX dungeons. FORT, SELSTAGE, and management screens combine it with stage_clear_state when deciding whether to show and apply the stage's post-clear coin rewards." +source = "investigation" +confidence = "high" +depends_on = ["0x7684", "0xeefd5"] + +[[global]] +address = "0xeefd5" +name = "stage_clear_coin_quantities" +category = "data-table" +type = "int[1000][3]" +columns = { "0" = "bronze_coin_item_91", "1" = "silver_coin_item_92", "2" = "gold_coin_item_93" } +value_domain = "nonnegative ITINIT item quantities; 64 populated shipped cells" +usage = "FORT and SELSTAGE render the three clear-reward quantities beside coin icons. Management screens grant the same values as item ids 91..93 after a cleared or EX-marked stage." +source = "investigation" +confidence = "high" +depends_on = ["0x8c877", "0xeebed"] + +[[global]] +address = "0xefb8d" +name = "stage_loader_script_ids" +category = "data-table" +type = "int[1000]" +value_domain = "packed call-script resource id; all 74 shipped rows contain 12893=STINIT.BIN" +usage = "FIELD indirectly calls the selected STINIT2 row before initializing the map. Every shipped stage points to the shared STINIT selector program, which then populates stage-specific objects, enemies, rules, textures, and audio." +source = "investigation" +confidence = "high" +depends_on = ["0x4dfbc"] + [[global]] address = "0xe7311" name = "stage_map_texture_asset_overrides"