From 02e1db9fa48716e75581cd370d3eca72927f4c6f Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 18:14:19 -0400 Subject: [PATCH] Decode MAINIT and message infrastructure --- docs/global-reference.md | 23 ++--- docs/name-resolution.md | 31 +++++-- docs/phase-a-slice-plan.md | 12 +++ docs/phase-b-framework.md | 7 ++ docs/script-inventory.md | 15 ++-- docs/tools-reference.md | 12 +-- docs/vm-mapping-plan.md | 2 +- tools/extract_init.py | 91 ++++++++++++++++++- tools/extract_message_table.py | 6 ++ tools/test_extract_init.py | 155 +++++++++++++++++++++++++++++++++ tools/test_globals.py | 16 ++++ vm-map/globals.toml | 140 ++++++++++++++++++++++++----- 12 files changed, 461 insertions(+), 49 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 4c4de88..b9870ba 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5042 globals (268 curated, 4774 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5045 globals (277 curated, 4768 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -21,6 +21,8 @@ |---|---|---|---|---| | `0x2052e` | scjump_progress_b | med | inference | Second SCJUMP switch input (1223 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. | | `0x4dfbc` | scjump_progress_a | med | inference | Dominant SCJUMP switch input (1609 comparison reads) — a per-chapter story-progress counter/position the progression machine branches on. INFERENCE from SCJUMP usage; confirm exact meaning via a listing/playthrough. | +| `0x665d6` | modal_message_line_count | high | investigation | Number of populated strings in modal_message_lines. Dozens of menu/gameplay producers append at lines[count] and increment this value; MES renders the resulting non-selecting modal and clears the count, while SBUNKI consumes the same buffer as selectable options. | +| `0x665e2` | modal_annotation_count | high | investigation | Number of populated modal_annotation_texts entries. MES and SBUNKI iterate this many annotations and clear it on dismissal; no direct shipped producer was found. | | `0xcc9f2` | usable_action_min_range | high | investigation | CALCSCOPE initializes this from the equipped/default attack's minimum range and widens it for usable offensive and healing skills. It is the lower bound shared by the offensive and healing action-scope tables. | | `0xcc9f3` | usable_action_max_range | high | investigation | CALCSCOPE initializes this from the equipped/default attack's maximum range and widens it for usable offensive and healing skills. ATSEEK bounds its action-range flood fill with this value; RTN_M051/052 and RTN_M061 use it as their upper scan bound. | | `0xe6c5d` | scjump_progress_d | med | inference | SCJUMP switch input (168 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. | @@ -63,6 +65,8 @@ | `0x57357` | unit_deployment_cost_adjustments | high | investigation | Persistent per-unit delta added to unit_deployment_cost_base. CALCCC increments it by class_change_deployment_cost_delta, while SETCH, SETEN, ADDEXP, and deployment scripts include it when constructing or updating runtime command cost. | | `0x573bb` | unit_class_change_state | high | investigation | Persistent ten-slot class-change state for each playable unit. CALCCC copies the current unit's row to class_change_state_work before invoking the rule scripts and persists the updated row; CCINIT requires the rule's slot to be zero so each promotion is applied once. Columns: 0=promotion_slot_1, 1=promotion_slot_2, 2=promotion_slot_3, 3=promotion_slot_4, 4=promotion_slot_5, 5=promotion_slot_6, 6=promotion_slot_7, 7=promotion_slot_8, 8=promotion_slot_9, 9=promotion_slot_10. | | `0x5f0ed` | scene_decision_seen_flags | high | investigation | Persistent seen-state for progression decisions. Numbered scene scripts set the current scjump_decision_out cell after playback and synchronize it through the persistence opcode; INFOVO uses VIINIT's one-based prerequisites to unlock glossary topics. | +| `0x665e3` | modal_annotation_horizontal_cells | high | investigation | Per-annotation horizontal anchor input shared by MES and SBUNKI. Each renderer multiplies the cell by 21 pixels and offsets it by half the annotation's rendered width. | +| `0x66647` | modal_annotation_row_offsets | high | investigation | Per-annotation vertical row offset shared by MES and SBUNKI. Renderers multiply it by 30 pixels; a negative value additionally shifts the annotation left by one 21-pixel cell. | | `0x66716` | unit_voice_asset_ids | high | investigation | EBINIT per-unit voice bank for 116 voiced characters and variants. FIELD directly selects column 0 before WARPD, column 1 when an acting unit takes chest/treasure contents, and column 3 after occupying, losing, or sealing a stage objective. BTL selects the acting unit's columns 7..9 for ordinary attacks, 10 for an ordinary critical, odd columns 11/13/15 for skill uses, and even columns 12/14/16 for critical skills; the three variants follow its six-step exchange selector. If the target survives positive damage, BTL selects target columns 19/20/21 with 60/30/10 percent weights; column 22 belongs to a target reduced to zero HP, while actor column 23 is selected for a finishing blow. SHOWGROW selects column 24 after level gain. Populated columns 4..6 and 17..18 have no reachable selector in the shipped script corpus and are retained as explicit unused authoring slots; columns 17 and 18 duplicate columns 15 and 16 in all 116 populated rows. Columns: 0=warp, 1=treasure_capture, 3=objective_interaction, 4=unused_slot_4, 5=unused_slot_5, 6=unused_slot_6, 7=normal_attack_1, 8=normal_attack_2, 9=normal_attack_3, 10=critical_normal_attack, 11=skill_use_1, 12=critical_skill_1, 13=skill_use_2, 14=critical_skill_2, 15=skill_use_3, 16=critical_skill_3, 17=unused_slot_17, 18=unused_slot_18, 19=damage_reaction_1, 20=damage_reaction_2, 21=damage_reaction_3, 22=defeated, 23=finishing_blow, 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. | | `0x6e7fe` | unit_map_sprite_asset_ids | high | investigation | Five-context unit sprite-sheet table for 251 units. Even columns 0/2/4 are compact CP*AA-style presentations for normal, alternate-condition, and special-condition states; columns 1/3 are the matching normal/alternate full directional CP*AB sheets used by FIELD. INFOCH/INFOEN and DRAWENP reuse the compact representation. Columns: 0=normal_compact, 1=normal_directional, 2=alternate_compact, 3=alternate_directional, 4=special_compact. | @@ -193,6 +197,8 @@ | `0x14cbd8` | battle_routine_forbidden_story_flag_ids | high | investigation | RTINIT battle bank 19. BTRTN subtracts one and rejects the step when the referenced story flag is set. | | `0x152877` | battle_entity_indices | high | investigation | The two runtime entity rows participating in BTL. battle_actor_side_index and battle_target_side_index select these cells before BTL and CALCDMG access HP, unit definitions, skills, animation state, and voice banks. Columns: 0=side_0, 1=side_1. | | `0x152879` | battle_selected_skill_ids | high | investigation | Selected skill for each battle side. BTL displays a positive entry through the skill-name table and selects skill-use voice columns; CALCDMG uses the same id for skill parameters. A zero entry follows the equipped/default ordinary-attack path. Columns: 0=side_0, 1=side_1. | +| `0x1561f6` | magic_action_information_handler_script_ids | high | investigation | MAINIT handler column indexed by current_magic_action_id. MAGIC and STUDY call the selected packed script id; every shipped action routes to MAMES, whose text dispatcher has authored descriptions for ids 1..9 only. | +| `0x15a097` | information_message_handler_script_ids | high | investigation | INFOMES's 32-row, four-column handler registry. INIT2 populates row zero with CIMES (0x334a), EIMES (0x334b), and VIMES (0x334c); INFOMES scans rows in order for the current tab, calls nonzero handlers, and stops after information_message_handled is set. Columns: 0=character_handler, 1=enemy_handler, 2=glossary_handler, 3=reserved_handler. | | `0x15a118` | character_profile_unit_ids | high | investigation | CIINIT's profile-to-unit join. INFOCH uses the selected unit id for reveal state, map sprite, short unit descriptions, species, and sex while CIMES remains keyed by the enclosing profile id. | | `0x15a17c` | character_profile_portrait_asset_ids | high | investigation | Optional CIINIT portrait resource indexed by character profile id. INFOCH draws it when nonzero and otherwise falls back to the backing unit's map-sprite asset. | | `0x15a1e0` | character_profile_portrait_x_offsets | high | investigation | INFOCH adds the selected cell to the centered portrait x coordinate. The 100-cell spacing to the adjacent CIINIT arrays and the direct indexed reader establish the reserved per-profile placement column. | @@ -211,8 +217,6 @@ | `0x69e` | — | low | auto-shape | array | | `0x6fe` | — | low | auto-shape | array | | `0x712` | — | low | auto-shape | array | -| `0x7db` | — | low | auto-shape | array | -| `0x7e5` | — | low | auto-shape | array | | `0x2690` | — | low | auto-shape | array | | `0x26a4` | — | low | auto-shape | array | | `0x26b5` | — | low | auto-shape | array | @@ -281,8 +285,6 @@ | `0x66442` | — | low | auto-shape | array | | `0x6650a` | — | low | auto-shape | array | | `0x665d8` | — | low | auto-shape | array | -| `0x665e3` | — | low | auto-shape | array | -| `0x66647` | — | low | auto-shape | array | | `0x8fb41` | — | low | auto-shape | array | | `0xa5eb9` | — | low | auto-shape | array | | `0xaac78` | — | low | auto-shape | array | @@ -379,7 +381,6 @@ | `0x15619c` | — | low | auto-shape | array | | `0x1561ba` | — | low | auto-shape | array | | `0x1561d8` | — | low | auto-shape | array | -| `0x1561f6` | — | low | auto-shape | array | | `0x156214` | — | low | auto-shape | array | | `0x1565fc` | — | low | auto-shape | array | | `0x157d6c` | — | low | auto-shape | array | @@ -404,6 +405,8 @@ | `0x152617` | current_stage_enemy_spawn_slot | high | investigation | Current STINIT enemy-template slot. FIELD selects slots 1..29 while materializing stage units, SETEN records the slot on the runtime entity and reads every parallel enemy buffer through it, and ADDEN uses slot 0 for its special generated unit. | | `0x15261a` | battle_actor_side_index | high | investigation | Index of the acting side in BTL's two-entry battle arrays. CALCDMG derives battle_target_side_index as 1 minus this value; BTL uses the actor's unit-definition row for attack, skill, critical, and finishing-blow voices. | | `0x15261b` | battle_target_side_index | high | investigation | Index of the target side in BTL's two-entry battle arrays. CALCDMG sets it to 1 - battle_actor_side_index; BTL applies battle_hp_delta to this side and uses its unit-definition row for damage and defeated voices. | +| `0x1560e7` | current_magic_action_id | high | investigation | Shared selected magic/research/growth action id. MAGIC and STUDY index MAINIT's parallel columns with it, FIELD and USEMAGIC consume the selected action, and MAMES dispatches ids 1..9 to their untitled help descriptions. | +| `0x15a095` | information_tab_index | high | investigation | Selected INFO screen tab. INFO dispatches the five tab scripts with it; INFOMES uses values 0..2 as the column of information_message_handler_script_ids for INFOCH, INFOEN, and INFOVO. | | `0x15a117` | current_character_profile_id | high | investigation | INFOCH's selected character-information row. It indexes CIINIT's name, unit, portrait, and placement arrays; CIMES compares the same id against all 24 profiles to dispatch the character biography. | | `0x15a2a8` | current_glossary_topic_id | high | investigation | INFOVO's selected glossary/help topic. It indexes the VIINIT title and presentation arrays, then VIMES compares it against all 65 shipped topic ids to dispatch the full player-facing explanation. | | `0x15a759` | current_enemy_encyclopedia_unit_id | high | investigation | INFOEN's selected enemy-encyclopedia unit id. It indexes EBINIT presentation and reveal arrays, seeds a temporary runtime entity for stat display, and EIMES compares it against 192 sparse unit ids to dispatch summary and strategy lines. | @@ -443,20 +446,19 @@ | `0x624be` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 8 scripts; compared against [0]; writers=['MENU.BIN']. | | `0x6642c` | route_branch | med | inference | Route/branch selector: sole writer is BUNKI.BIN (分岐 = branching), enum 0..7, branch-read in 36 scenes. Selects a story branch/route. INFERENCE from the BUNKI writer + enum — confirm exact routes via a listing/sweep. | | `0x665d3` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [1, 2, 4, 8, 32, 64, 128, 65536]; writers=['ALCHEMY.BIN', 'BUNKI.BIN', 'DEBUGADV.BIN', 'EVOLVE.BIN']. | -| `0x665d6` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against []; writers=['MES.BIN', 'SBUNKI.BIN']. | | `0x665d7` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 5 scripts; compared against [1, 3]; writers=['ADDSKILL.BIN', 'SBUNKI.BIN']. | | `0xaba5c` | — | med | auto-shape | TODO: confirm. Branch-read in 136 scenes / 149 scripts; compared against [0, 1]; writers=['DEBUGADV.BIN', 'DEBUGADV2.BIN', 'SC0000.BIN', 'SC0010.BIN']. | | `0xaba5e` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 5 scripts; compared against [2]; writers=['DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']. | | `0xccc21` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0]; writers=['CHMENU.BIN']. | -| `0x1560e7` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0, 1, 2, 3, 4, 5, 6, 7]; writers=['MAGIC.BIN', 'SELACT.BIN', 'SUMMON.BIN']. | | `0x15a094` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 6 scripts; compared against [0]; writers=['ALCHEMY.BIN', 'EVOLVE.BIN', 'SELSTAGE.BIN', 'STUDY.BIN']. | -| `0x15a095` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 6 scripts; compared against [0]; writers=['INFO.BIN', 'INFOCH.BIN', 'MENU.BIN']. | ## string-table | address | name | conf | source | usage | |---|---|---|---|---| | `0x27e` | unit_class_titles | high | investigation | Persistent per-unit class/title string table. CALCCC writes the selected class_change_title_output into the current unit's cell; character and status presentation scripts read the resulting title. | +| `0x7db` | modal_message_lines | high | investigation | Shared ten-string modal buffer. Producers append messages or menu options at modal_message_line_count; MES measures and draws each string, and SBUNKI reuses the same entries for an interactive selection list. | +| `0x7e5` | modal_annotation_texts | high | investigation | Optional small-font annotations rendered by MES and SBUNKI after the primary modal lines. No direct shipped producer was found; the paired count and placement arrays expose a reserved/extensible annotation ABI. | | `0x26b4` | class_change_title_output | high | investigation | CCINIT writes the title selected by each eligible class-change rule. CALCCC copies it to unit_class_titles for a successful promotion, and ADDEXP includes the same string in the level-up notification. | | `0x26f1` | object_type_names | high | investigation | OBINIT writes the authoritative object names. FIELD, SETOBJ, and DRAWOBJ use STINIT's object type id to select these definitions; extract_init joins the names to stage object placements. | | `0x2755` | object_type_descriptions | high | investigation | OBINIT writes the short object descriptions displayed by the field object-information path. extract_init joins populated descriptions to STINIT object placements by type id. | @@ -464,6 +466,7 @@ | `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. | +| `0x45b9` | magic_action_names | high | investigation | MAINIT's one-based action-name array: eleven shipped records occupy ids 1..11 in a reserved 30-cell span. MAGIC, STUDY, and EVOLVE render the selected entry. | | `0x45d7` | character_profile_names | high | investigation | CIINIT writes the character-information screen's 24 display names. INFOCH lists them by profile id, masks unrevealed rows, and uses the same id to select the backing unit metadata and CIMES biography. | | `0x463b` | glossary_topic_titles | high | investigation | VIINIT writes the sparse glossary title table. INFOVO renders the selected title, and extract_init joins all 65 populated ids to VIMES's full help text. | | `0x276` | — | med | auto-shape | string-table (written by SC0130) | @@ -3671,6 +3674,7 @@ | `0x6cc` | adv_hover_read_message_skip | high | investigation | Pointer-hover flag for the standard ADV read-message-only Skip button at (750,572). See adv_hover_history. | | `0x6cd` | adv_hover_hide_window | high | investigation | Pointer-hover flag for the standard ADV Hide-window button at (772,572). See adv_hover_history. | | `0x62425` | adv_hide_window_enabled | high | investigation | Native ADV-scheduler permission for the standard Hide Window action. After op 0x199 enters the registered yield-A handler, every standard ADV scene calls HIDEWIN.BIN only while this value is nonzero. No script writes it and the complete boot-to-SC0000 VM-write capture does not contain it, so it is native-owned inherited state rather than saved-game or script boot data. The Godot scene bootstrap mirrors the original enabled value 1. | +| `0x15a096` | information_message_handled | high | investigation | INFOMES clears this before walking handler rows and stops when it becomes one. CIMES, EIMES, and VIMES set it after rendering a matching character, enemy, or glossary message, implementing a first-handler-wins extension chain. | ## unknown @@ -5081,7 +5085,6 @@ | `0x1571b4` | — | med | auto-shape | record-table[stride 2] | | `0x158154` | — | med | auto-shape | record-table[stride 4] | | `0x1590f4` | — | med | auto-shape | record-table[stride 4] | -| `0x15a097` | — | med | auto-shape | record-table[stride 4] | | `0x15a75b` | — | med | auto-shape | record-table[stride 21] | | `0x15a785` | — | med | auto-shape | record-table[stride 3] | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index fed5a2a..fb95f6c 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -252,6 +252,26 @@ draws the selected unit metadata and optional portrait, and then reaches CIMES t router. The selected `current_character_profile_id` is therefore the exact join key, not the backing unit id. CIMES supplies one untitled multiline `biography` for every one of the 24 CIINIT profiles. +MAINIT is the corresponding 30-cell magic-action registry. Eleven one-based rows name six field magic +actions, three research actions, and two growth rituals, with ten parallel integer columns (two entirely +zero in the shipped initializer). The dedicated extractor preserves the whole reserved layout rather than +mistaking the one consecutive name column for an eleven-cell table. Its final column contains packed +information-handler script ids; every populated row points to `MAMES.BIN`. MAMES dispatches on +`current_magic_action_id` and supplies one untitled `description` for ids 1 through 9. The growth rituals +at ids 10 and 11 intentionally have no MAMES branch, so the generated join records nine matches and +`init_ids_without_message: [10, 11]` instead of fabricating text. + +The remaining similarly named scripts are infrastructure, not definition-linked message tables. +INFOMES has no inline strings: it clears `information_message_handled`, scans a 32-by-4 packed-script +registry at `information_message_handler_script_ids` for the current INFO tab, calls each nonzero entry, +and stops when a handler claims the request. INIT2 installs CIMES, EIMES, and VIMES in row zero's +character/enemy/glossary columns; column 3 and rows 1 through 31 are reserved extension capacity. MES is +the generic non-selecting modal renderer used throughout menus and gameplay. Callers append strings to +`modal_message_lines` and increment `modal_message_line_count`; MES measures and draws those strings, +waits for dismissal, and clears the buffer. MES and SBUNKI also share an optional 100-entry annotation +text/horizontal-anchor/row-offset ABI, although no direct producer populates that secondary buffer in the +shipped scripts. + Confirmed row-column meanings are no longer prose-only. The relevant `globals.toml` entries carry a machine-readable `columns` map; `globals_build.py` preserves it in `build/globals.json`, and `extract_init.py` emits a top-level `field_semantics` mapping while retaining raw address/stride/column keys @@ -649,20 +669,19 @@ are *not* story flags — the miner over-tags them; they are recategorized `unkn The v1 map labels *shapes and tables*; the next increments add *meaning*, cheapest first: -1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, EBINIT, STINIT, CCINIT, SCINIT, and RTINIT now have +1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, EBINIT, STINIT, CCINIT, SCINIT, RTINIT, and MAINIT now have machine-readable investigation surfaces and semantic joins; EBINIT's populated schema is fully named, STINIT's joined object/enemy payloads are decoded, and CCINIT's 71 class-change rules expose predicates and effects. SCINIT closes the progression decision-to-scene join, and RTINIT's twenty movement/battle banks are structurally decoded with every populated movement-parameter cell classified and all 1,043 shipped movement steps joined to provider behavior. With RTINIT's used-provider surface closed and - ITMES/SKMES/VIMES/EIMES/CIMES joined, resume the remaining `*MES` writers and other reader-proven 2D tables; + ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and INFOMES/MES classified, resume other reader-proven 2D tables; never assign one universal meaning to a parameter bank whose meaning varies by provider selector. -2. **Classify the remaining message infrastructure beyond the completed ITMES/SKMES/VIMES/EIMES/CIMES - joins**, starting with the smaller non-ID-dispatch MAMES/INFOMES/MES scripts and then 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 +2. **Label 2D record tables by their readers** — cross-reference which scripts read each `rec[sN]` table and infer purpose from context (e.g. RECOVER's 30-wide tables ↔ a status/recovery system). Static, medium effort. +3. **Classify other `set-string`/`copy-to-global` writers** not covered by the completed INIT/MES + set when their data flow reaches a current implementation need. 4. **Name *which stat* each field is (Frida).** The one step needing live tools: change a known value in-game (take damage, gain XP), watch which global moves → definitive `field@X = "HP"`. Reserve for the fields that matter; this is the last mile. diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 9d5c29c..d0224c5 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -2794,3 +2794,15 @@ the bounded fallback for legacy whole-screen host transitions or an unobserved a `run-godot.cmd` no longer forces `-PerfLog` or a renderer switch, fulfilling the temporary-launch cleanup; `run-godot.ps1 -PerfLog` remains available for future targeted captures. The original SC0000 burst and DEBUGMAP animated-unit workload are both visually accepted and comfortably within frame budget. + +## Data-semantics sidebar: MAINIT/MAMES and message infrastructure (2026-07-23) + +The current static-decoding slice closes the three remaining small MES-named targets. `extract_init.py` +now models MAINIT as eleven one-based magic/research/growth actions in a reserved 30-cell layout and joins +nine untitled MAMES descriptions; growth ritual ids 10 and 11 are explicitly unmatched. INFOMES is +classified as a text-free 32-by-4, first-handler-wins INFO extension registry initialized with +CIMES/EIMES/VIMES. MES is classified as the shared non-selecting modal renderer over caller-populated +line and optional annotation buffers. Their stable globals are curated in `vm-map/globals.toml`, and +structural regressions protect the registry initialization, indirect dispatch, buffer reads, and cleanup. + +**Next:** return to reader-proven 2D tables, beginning with RECOVER's 30-wide status/recovery structures. diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index 218e545..18f7239 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -738,6 +738,13 @@ The dispatch keys establish `current_item_id`, `current_skill_id`, `current_glos `current_enemy_encyclopedia_unit_id`, and `current_character_profile_id` as high-confidence shared index slots. +The follow-up closes the three smaller MES-named scripts. MAINIT now extracts eleven records in its +reserved 30-cell layout and joins MAMES descriptions for ids 1 through 9; its two growth rituals have no +authored MAMES branch. INFOMES is a text-free 32-by-4 first-handler-wins extension registry initialized +with CIMES/EIMES/VIMES, while MES renders and clears a generic caller-populated modal line buffer plus an +optional annotation buffer shared with SBUNKI. These are stable runtime ABIs, not three additional +translatable table shapes. + VIINIT is now extracted as a sparse 200-by-3 glossary definition table: 65 populated names, one required one-based seen-scene decision for every topic, and a second alternative prerequisite for thirteen topics. INFOVO evaluates those prerequisites against persistent scene-decision seen flags before listing a topic. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 787771b..fccb166 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -117,13 +117,16 @@ instantiated steps to join. text), `CIMES` (15 KB), `MAMES`, `INFOMES`, `MES` — where most translatable text outside scenes lives. The generated message join covers ITMES's 287 item records, SKMES's 131 skill records, VIMES's complete 65-topic VIINIT glossary, EIMES's 192 sparse EBINIT enemy-commentary -records, and CIMES's complete 24-profile CIINIT character registry. ITMES/SKMES use fallthrough message -bodies; VIMES/EIMES/CIMES place all guards first and branch to later bodies. VIMES records retain title +records, CIMES's complete 24-profile CIINIT character registry, and nine of MAINIT's eleven magic actions. +ITMES/SKMES/MAMES use fallthrough message bodies; VIMES/EIMES/CIMES place all guards first and branch to +later bodies. VIMES records retain title plus multiline description, EIMES's untitled two-line records expose `summary` and `strategy`, and -CIMES exposes one untitled multiline `biography`. INFOCH uses each CIINIT profile id to join its displayed -name, backing unit definition, optional portrait, and CIMES text. The smaller MAMES/INFOMES/MES scripts -need separate structural classification; INFOMES is already known to be the information-screen handler -router rather than a text body. +CIMES exposes one untitled multiline `biography`; MAMES exposes one untitled action `description`. +INFOCH uses each CIINIT profile id to join its displayed name, backing unit definition, optional portrait, +and CIMES text. INFOMES itself contains no text: it walks a 32-by-4 first-handler-wins registry whose +shipped row zero routes the character, enemy, and glossary tabs to CIMES/EIMES/VIMES. MES is the generic +modal renderer for caller-populated line buffers, with a shared optional annotation ABI also consumed by +SBUNKI. Full data-flow details live in `name-resolution.md`. ### Battle system `BTL` (61 KB — main battle loop), `BTRTN`, `ROUND`, `AIM`, `ATSEEK`, `MVSEEK`, diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 3e40d35..738fe5e 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -54,16 +54,18 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited * | Tool | Purpose | Run | Reads → Writes | |---|---|---|---| | `extract_phase2.py` | Batch: disassembly + text corpora for every script. | `extract_phase2.py` | corpus → `build/disasm/*.asm`, `build/text/{dialogue.jsonl,strings.jsonl,*.strings.txt}`, `build/manifest.json` | -| `extract_message_table.py` | Discover repeated global-id dispatch chains in either fallthrough-body (ITMES/SKMES) 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`. | `extract_message_table.py [OUTNAME]` | `.BIN` → `build/data/.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. 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, `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/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. Name tables report message coverage for title/description, summary/strategy, and biography 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}` | +| `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. 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, and RTINIT banked extraction; 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 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, -whose up-to-three unlock writes precede each title, and CIINIT's 24 populated character-information -profiles within four reserved 100-cell columns. Matching message joins are ITINIT→ITMES, -SKINIT→SKMES, VIINIT→VIMES, EBINIT→EIMES, and CIINIT→CIMES; only the EBINIT join is intentionally sparse. +CIINIT's 24 populated character-information profiles within four reserved 100-cell columns, and MAINIT's +eleven populated magic-action rows within a reserved 30-cell layout. Matching message joins are +ITINIT→ITMES, SKINIT→SKMES, VIINIT→VIMES, EBINIT→EIMES, CIINIT→CIMES, and MAINIT→MAMES. EBINIT is +intentionally sparse across unrelated unit definitions; MAINIT intentionally lacks MAMES bodies for growth +ritual ids 10 and 11. ## VM diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index cbcaf35..2baa1fa 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -59,7 +59,7 @@ - [x] **2.3b — Extract `SCINIT`'s scene-dispatch registry.** Dispatch mode recognizes 2,179 alternating writes to two 10,000-cell arrays, preserves all 710 overwritten decision ids, and emits 1,209 final rows. The primary column maps decisions to 135 packed SYS4INI ids, all resolved to numbered SC scripts; the parallel column is authored chapter metadata. All 847 live SCJUMP decisions join to rows and 844 final chapter tags agree with the independently decoded paths; three legacy/stale mismatches remain explicit. - [x] **2.3c — Extract `RTINIT`'s routine-step banks.** Banked mode recognizes twenty parallel 1000-by-20 tables and emits 172 sparse routine-set rows with all 3,336 source assignments and 3,307 final cells. Movement banks join all 1,043 steps to all 19 used RTN_M providers (22 dispatchable); battle banks join fourteen steps to all four RTN_B providers. Activation percentages, progress gates, and required/forbidden story flags are consumer-proven; six empty banks remain explicit. RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061 add selector-specific progress, randomized roaming, object-slot, coordinate, enemy/ally, treasure and object search, Healing Feather, waypoint, faction-terrain, retreat, normal-attack routing, offensive target/action selection, and immediate allied-healing semantics. All 977 populated movement-parameter cells are classified: 974 semantic inputs and three M001/M008 cells proven unread; thirteen unwritten defaults are projected separately while the generic raw banks remain intact. - [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: with RTINIT closed at 1,043/1,043 movement steps and ITMES/SKMES/VIMES/EIMES/CIMES joined to their definitions, classify the smaller remaining `*MES` scripts and label other 2D record tables by their readers. Then use Frida only for semantics that 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 five 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; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. The next evidence-dense slice labels other 2D record tables by their readers, beginning with RECOVER's 30-wide status/recovery tables. Then use Frida only for semantics that 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. ## 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 9ea720f..ec2c9e5 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -496,6 +496,7 @@ MESSAGE_TABLES = { "CIINIT": "CIMES", "EBINIT": "EIMES", "ITINIT": "ITMES", + "MAINIT": "MAMES", "SKINIT": "SKMES", "VIINIT": "VIMES", } @@ -507,6 +508,22 @@ CHARACTER_PROFILE_PORTRAIT_X_ARRAY_BASE = 0x15A1E0 CHARACTER_PROFILE_PORTRAIT_Y_ARRAY_BASE = 0x15A244 CHARACTER_PROFILE_RECORD_SPAN = 100 +MAGIC_ACTION_NAME_ARRAY_BASE = 0x45B9 +MAGIC_ACTION_INTEGER_ARRAY_BASES = ( + 0x1560E8, + 0x156106, + 0x156124, + 0x156142, + 0x156160, + 0x15617E, + 0x15619C, + 0x1561BA, + 0x1561D8, + 0x1561F6, +) +MAGIC_ACTION_HANDLER_ARRAY_BASE = 0x1561F6 +MAGIC_ACTION_RECORD_SPAN = 30 + VOCABULARY_NAME_ARRAY_BASE = 0x463B VOCABULARY_RECORD_TABLE_BASE = 0x15A2A9 VOCABULARY_RECORD_STRIDE = 3 @@ -1260,6 +1277,76 @@ def extract_character_profiles(scr): } +def extract_magic_actions(scr): + """Extract MAINIT's action-id keyed magic/research/growth registry. + + MAINIT has only one consecutive string column, so the generic name-table + span heuristic cannot see its reserved 30-cell stride. Its ten integer + columns are equally spaced consumers of the same action id. + """ + records = [] + by_id = {} + for ins in scr.instructions: + if ( + ins.opcode != SET_STRING + or len(ins.args) < 2 + or ins.args[0][0] != T_GLOBAL_STRING + ): + continue + record_id = ins.args[0][1] - MAGIC_ACTION_NAME_ARRAY_BASE + if not (1 <= record_id < MAGIC_ACTION_RECORD_SPAN): + raise ValueError( + f"{scr.path.name}: magic-action name outside reserved id span: " + f"0x{ins.args[0][1]:x}" + ) + text = scr.strings.get(ins.args[1][1], (None,))[0] + record = {"id": record_id, "name": text, "fields": {}} + records.append(record) + by_id[record_id] = record + + for ins in scr.instructions: + write = _static_global_write(ins) + if write is None: + continue + destination, value = write + matched = False + for base in MAGIC_ACTION_INTEGER_ARRAY_BASES: + record_id = destination - base + if 0 <= record_id < MAGIC_ACTION_RECORD_SPAN: + if record_id not in by_id: + raise ValueError( + f"{scr.path.name}: integer write for unnamed magic " + f"action id {record_id}" + ) + _store_unique( + by_id[record_id]["fields"], + f"0x{base:x}", + value, + record_id, + ) + matched = True + break + if not matched: + raise ValueError( + f"{scr.path.name}: unexpected integer write 0x{destination:x}" + ) + + return records, { + "schema": "magic-actions", + "name_array_base": f"0x{MAGIC_ACTION_NAME_ARRAY_BASE:x}", + "name_write_base": f"0x{MAGIC_ACTION_NAME_ARRAY_BASE + 1:x}", + "first_record_id": 1, + "record_span": MAGIC_ACTION_RECORD_SPAN, + "integer_array_bases": [ + f"0x{base:x}" for base in MAGIC_ACTION_INTEGER_ARRAY_BASES + ], + "handler_script_array_base": ( + f"0x{MAGIC_ACTION_HANDLER_ARRAY_BASE:x}" + ), + "implicit_default": 0, + } + + @cache def object_type_definitions() -> dict[int, dict]: """Load OBINIT's authoritative display and state-row metadata by object type id.""" @@ -1999,7 +2086,7 @@ def write_data_index(data_dir: Path) -> None: "Linked row-major fields are stored separately in `record_fields`, keyed as", "`base/stride/column` from corpus-observed `lookup-array-2d` consumers.", "Where a matching `*MES` dispatcher exists, `message` preserves its player-facing", - "layout-specific text (title/description, summary/strategy, or biography), furigana,", + "layout-specific text (title/description, summary/strategy, biography, or description-only), furigana,", "and bytecode dispatch offset separately from the", "short description stored by the INIT script.", "Top-level `field_semantics` maps raw array/row-column keys to canonical machine-readable", @@ -2076,6 +2163,8 @@ def main() -> int: extractor = extract_vocabulary elif mode == "name" and name == "CIINIT": extractor = extract_character_profiles + elif mode == "name" and name == "MAINIT": + extractor = extract_magic_actions recs, meta = extractor(scr) if mode == "name" and name in MESSAGE_TABLES: message_name = MESSAGE_TABLES[name] diff --git a/tools/extract_message_table.py b/tools/extract_message_table.py index 6f203ee..cb48674 100644 --- a/tools/extract_message_table.py +++ b/tools/extract_message_table.py @@ -19,6 +19,7 @@ Usage: py -3.11 -X utf8 tools/extract_message_table.py VIMES py -3.11 -X utf8 tools/extract_message_table.py EIMES py -3.11 -X utf8 tools/extract_message_table.py CIMES + py -3.11 -X utf8 tools/extract_message_table.py MAMES """ from __future__ import annotations @@ -41,6 +42,7 @@ BRANCH_SENTINEL = 0xFFFFFFFF MESSAGE_LAYOUTS = { "CIMES.BIN": "character-biography", "EIMES.BIN": "enemy-commentary", + "MAMES.BIN": "description", } @@ -117,6 +119,10 @@ def _message_body( message = { "biography": "\n".join(lines), } + elif layout == "description": + message = { + "description": "\n".join(lines), + } elif layout == "enemy-commentary": message = { "summary": lines[0], diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 781caad..d1e6ba5 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -136,6 +136,39 @@ def test_character_profiles() -> None: ) +def test_magic_actions() -> None: + scripts = paths.scripts() + records, meta = extract_init.extract_magic_actions( + sys4load.load(scripts["MAINIT.BIN"]) + ) + by_id = {record["id"]: record for record in records} + check(len(records) == 11, "MAINIT extracts all 11 magic/research actions") + check( + meta["record_span"] == 30 + and meta["name_array_base"] == "0x45b9" + and meta["handler_script_array_base"] == "0x1561f6", + "MAINIT exposes its reserved span and handler column", + ) + check( + by_id[1]["name"] == "闇の治癒" + and by_id[1]["fields"]["0x156142"] == 10, + "MAINIT action 1 keeps its name and authored cost-like field", + ) + check( + all( + record["fields"]["0x1561f6"] == 0x31A6 + for record in records + ), + "MAINIT routes every action to the MAMES information handler", + ) + check( + "0x1561d8" not in by_id[9]["fields"] + and by_id[10]["fields"]["0x1561d8"] == 15 + and by_id[11]["fields"]["0x1561d8"] == 40, + "MAINIT preserves the sparse growth-ritual threshold column", + ) + + def test_static_negative_write() -> None: class Instruction: opcode = extract_init.SUB @@ -613,6 +646,7 @@ def test_real_message_tables() -> None: "VIMES.BIN": (0x15A2A8, 65, "branch-target"), "EIMES.BIN": (0x15A759, 192, "branch-target"), "CIMES.BIN": (0x15A117, 24, "branch-target"), + "MAMES.BIN": (0x1560E7, 9, "fallthrough"), } for name, (selector, count, dispatch_layout) in expected.items(): records, meta = extract_message_table.extract_messages( @@ -685,6 +719,106 @@ def test_real_message_tables() -> None: "CIMES records the biography-only message layout", ) + magic_messages, magic_meta = extract_message_table.extract_messages( + sys4load.load(scripts["MAMES.BIN"]) + ) + magic = {record["id"]: record for record in magic_messages} + check( + magic_meta["message_layout"] == "description" + and "title" not in magic[1] + and len(magic[1]["description"]) > 0, + "MAMES exposes complete untitled action descriptions", + ) + + +def test_message_infrastructure() -> None: + scripts = paths.scripts() + info = sys4load.load(scripts["INFOMES.BIN"]) + lookups = [ + instruction + for instruction in info.instructions + if sys4load.display_label(instruction.opcode) == "lookup-array-2d" + ] + check( + not info.strings + and len(lookups) == 2 + and all( + (extract_init.T_GLOBAL_INT, 0x15A097) in instruction.args + and (extract_init.T_GLOBAL_INT, 0x15A095) in instruction.args + and (extract_init.T_IMM, 4) in instruction.args + for instruction in lookups + ), + "INFOMES is a text-free 32x4 tab-handler registry walker", + ) + check( + any( + sys4load.display_label(instruction.opcode) == "call-script" + and instruction.args[0][0] != extract_init.T_IMM + for instruction in info.instructions + ), + "INFOMES invokes registry entries through an indirect call-script", + ) + init2 = sys4load.load(scripts["INIT2.BIN"]) + initial_handlers = { + destination: value + for instruction in init2.instructions + if (write := extract_init._static_global_write(instruction)) is not None + for destination, value in [write] + if 0x15A097 <= destination <= 0x15A099 + } + check( + initial_handlers + == {0x15A097: 0x334A, 0x15A098: 0x334B, 0x15A099: 0x334C}, + "INIT2 installs CIMES, EIMES, and VIMES in handler row zero", + ) + check( + any( + instruction.args + and instruction.args[0] == (extract_init.T_GLOBAL_INT, 0x15A096) + and extract_init._static_global_write(instruction) == (0x15A096, 0) + for instruction in info.instructions + ), + "INFOMES clears the first-handler-wins completion flag", + ) + + modal = sys4load.load(scripts["MES.BIN"]) + labels = { + sys4load.display_label(instruction.opcode) + for instruction in modal.instructions + } + references = { + operand + for instruction in modal.instructions + for operand in instruction.args + } + check( + "show-text" not in labels + and "draw-string" in labels + and { + (extract_init.T_GLOBAL_STRING, 0x7DB), + (extract_init.T_GLOBAL_INT, 0x665D6), + } <= references, + "MES is a generic renderer for caller-populated modal lines", + ) + check( + { + (extract_init.T_GLOBAL_STRING, 0x7E5), + (extract_init.T_GLOBAL_INT, 0x665E2), + (extract_init.T_GLOBAL_INT, 0x665E3), + (extract_init.T_GLOBAL_INT, 0x66647), + } <= references, + "MES retains the optional annotation text and placement ABI", + ) + modal_writes = { + write + for instruction in modal.instructions + if (write := extract_init._static_global_write(instruction)) is not None + } + check( + (0x665D6, 0) in modal_writes and (0x665E2, 0) in modal_writes, + "MES clears both modal buffers after dismissal", + ) + def test_message_join() -> None: scripts = paths.scripts() @@ -757,6 +891,25 @@ def test_message_join() -> None: "CIINIT records expose CIMES biography text by profile id", ) + magic_actions, _ = extract_init.extract_magic_actions( + sys4load.load(scripts["MAINIT.BIN"]) + ) + magic_meta = extract_init.join_messages( + magic_actions, sys4load.load(scripts["MAMES.BIN"]) + ) + magic_by_id = {record["id"]: record for record in magic_actions} + check( + magic_meta["joined_count"] == 9 + and magic_meta["init_ids_without_message"] == [10, 11] + and not magic_meta["message_ids_without_init"], + "MAINIT and MAMES form the expected sparse 9-of-11 action join", + ) + check( + magic_by_id[1]["message"]["description"] + and "title" not in magic_by_id[1]["message"], + "MAINIT records expose MAMES descriptions by action id", + ) + def test_field_semantics() -> None: scripts = paths.scripts() @@ -865,6 +1018,7 @@ def test_field_semantics() -> None: if __name__ == "__main__": test_real_name_tables() test_character_profiles() + test_magic_actions() test_static_negative_write() test_output_name_validation() test_real_mixed_table() @@ -872,6 +1026,7 @@ if __name__ == "__main__": test_real_scene_dispatch() test_real_routine_banks() test_real_message_tables() + test_message_infrastructure() test_message_join() test_field_semantics() if FAILS: diff --git a/tools/test_globals.py b/tools/test_globals.py index 3e3e708..ef51df6 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -73,6 +73,22 @@ def test_load_and_lint(): == "character_profile_portrait_asset_ids" and entries[0x45d7]["name"] == "character_profile_names", "CIINIT/CIMES character-profile state is curated") + check(entries[0x1560e7]["name"] == "current_magic_action_id" + and entries[0x45b9]["name"] == "magic_action_names" + and entries[0x1561f6]["name"] + == "magic_action_information_handler_script_ids", + "MAINIT/MAMES action state is curated") + check(entries[0x15a095]["name"] == "information_tab_index" + and entries[0x15a096]["name"] == "information_message_handled" + and entries[0x15a097]["name"] + == "information_message_handler_script_ids", + "INFOMES handler registry state is curated") + check(entries[0x7db]["name"] == "modal_message_lines" + and entries[0x665d6]["name"] == "modal_message_line_count" + and entries[0x7e5]["name"] == "modal_annotation_texts" + and entries[0x665e3]["name"] + == "modal_annotation_horizontal_cells", + "MES modal-buffer state is curated") check(entries[0x5f0ed]["name"] == "scene_decision_seen_flags", "glossary prerequisite seen-state is curated") check(entries[0x20543]["name"] == "tile_faction_traversal_masks", diff --git a/vm-map/globals.toml b/vm-map/globals.toml index bff07c0..416fee8 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -2219,14 +2219,69 @@ depends_on = [] [[global]] address = "0x665d6" -name = "" -category = "story-flag" +name = "modal_message_line_count" +category = "counter" type = "int" -value_domain = "?" -usage = "TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against []; writers=['MES.BIN', 'SBUNKI.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +value_domain = "0..10" +usage = "Number of populated strings in modal_message_lines. Dozens of menu/gameplay producers append at lines[count] and increment this value; MES renders the resulting non-selecting modal and clears the count, while SBUNKI consumes the same buffer as selectable options." +source = "investigation" +confidence = "high" +depends_on = ["0x7db"] + +[[global]] +address = "0x7db" +name = "modal_message_lines" +category = "string-table" +type = "string[10]" +value_domain = "runtime UI text" +usage = "Shared ten-string modal buffer. Producers append messages or menu options at modal_message_line_count; MES measures and draws each string, and SBUNKI reuses the same entries for an interactive selection list." +source = "investigation" +confidence = "high" +depends_on = ["0x665d6"] + +[[global]] +address = "0x7e5" +name = "modal_annotation_texts" +category = "string-table" +type = "string[100]" +value_domain = "optional runtime UI annotations" +usage = "Optional small-font annotations rendered by MES and SBUNKI after the primary modal lines. No direct shipped producer was found; the paired count and placement arrays expose a reserved/extensible annotation ABI." +source = "investigation" +confidence = "high" +depends_on = ["0x665e2", "0x665e3", "0x66647"] + +[[global]] +address = "0x665e2" +name = "modal_annotation_count" +category = "counter" +type = "int" +value_domain = "0..100" +usage = "Number of populated modal_annotation_texts entries. MES and SBUNKI iterate this many annotations and clear it on dismissal; no direct shipped producer was found." +source = "investigation" +confidence = "high" +depends_on = ["0x7e5"] + +[[global]] +address = "0x665e3" +name = "modal_annotation_horizontal_cells" +category = "data-table" +type = "int[100]" +value_domain = "horizontal anchor cells" +usage = "Per-annotation horizontal anchor input shared by MES and SBUNKI. Each renderer multiplies the cell by 21 pixels and offsets it by half the annotation's rendered width." +source = "investigation" +confidence = "high" +depends_on = ["0x7e5", "0x665e2"] + +[[global]] +address = "0x66647" +name = "modal_annotation_row_offsets" +category = "data-table" +type = "int[100]" +value_domain = "signed row offset" +usage = "Per-annotation vertical row offset shared by MES and SBUNKI. Renderers multiply it by 30 pixels; a negative value additionally shifts the annotation left by one 21-pixel cell." +source = "investigation" +confidence = "high" +depends_on = ["0x7e5", "0x665e2"] [[global]] address = "0x665d7" @@ -2382,14 +2437,36 @@ depends_on = [] [[global]] address = "0x1560e7" -name = "" -category = "story-flag" +name = "current_magic_action_id" +category = "index-pointer" type = "int" -value_domain = "one of {0, 1, 2, 3, 4, 5, 6, 7}" -usage = "TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0, 1, 2, 3, 4, 5, 6, 7]; writers=['MAGIC.BIN', 'SELACT.BIN', 'SUMMON.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +value_domain = "MAINIT action id 0..11; 0 means none" +usage = "Shared selected magic/research/growth action id. MAGIC and STUDY index MAINIT's parallel columns with it, FIELD and USEMAGIC consume the selected action, and MAMES dispatches ids 1..9 to their untitled help descriptions." +source = "investigation" +confidence = "high" +depends_on = ["0x45b9", "0x1561f6"] + +[[global]] +address = "0x45b9" +name = "magic_action_names" +category = "string-table" +type = "string[30]" +value_domain = "MAINIT magic, research, and growth-ritual names" +usage = "MAINIT's one-based action-name array: eleven shipped records occupy ids 1..11 in a reserved 30-cell span. MAGIC, STUDY, and EVOLVE render the selected entry." +source = "investigation" +confidence = "high" +depends_on = ["0x1560e7"] + +[[global]] +address = "0x1561f6" +name = "magic_action_information_handler_script_ids" +category = "data-table" +type = "int[30]" +value_domain = "packed script id; shipped ids 1..11 all equal 0x31a6 (MAMES.BIN)" +usage = "MAINIT handler column indexed by current_magic_action_id. MAGIC and STUDY call the selected packed script id; every shipped action routes to MAMES, whose text dispatcher has authored descriptions for ids 1..9 only." +source = "investigation" +confidence = "high" +depends_on = ["0x1560e7", "0x45b9"] [[global]] address = "0x15a094" @@ -2404,14 +2481,37 @@ depends_on = [] [[global]] address = "0x15a095" -name = "" -category = "story-flag" +name = "information_tab_index" +category = "index-pointer" +type = "int" +value_domain = "-1 or 0..4; 0 characters, 1 enemies, 2 glossary, 3 affinity, 4 items" +usage = "Selected INFO screen tab. INFO dispatches the five tab scripts with it; INFOMES uses values 0..2 as the column of information_message_handler_script_ids for INFOCH, INFOEN, and INFOVO." +source = "investigation" +confidence = "high" +depends_on = ["0x15a097"] + +[[global]] +address = "0x15a096" +name = "information_message_handled" +category = "ui-toggle" type = "int" value_domain = "{0,1}" -usage = "TODO: confirm. Branch-read in 0 scenes / 6 scripts; compared against [0]; writers=['INFO.BIN', 'INFOCH.BIN', 'MENU.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +usage = "INFOMES clears this before walking handler rows and stops when it becomes one. CIMES, EIMES, and VIMES set it after rendering a matching character, enemy, or glossary message, implementing a first-handler-wins extension chain." +source = "investigation" +confidence = "high" +depends_on = ["0x15a097"] + +[[global]] +address = "0x15a097" +name = "information_message_handler_script_ids" +category = "data-table" +type = "int[32][4]" +columns = { "0" = "character_handler", "1" = "enemy_handler", "2" = "glossary_handler", "3" = "reserved_handler" } +value_domain = "packed script id; zero means no handler" +usage = "INFOMES's 32-row, four-column handler registry. INIT2 populates row zero with CIMES (0x334a), EIMES (0x334b), and VIMES (0x334c); INFOMES scans rows in order for the current tab, calls nonzero handlers, and stops after information_message_handled is set." +source = "investigation" +confidence = "high" +depends_on = ["0x15a095", "0x15a096"] [[global]] address = "0x4dfbc"