From e84f61d6709fb9afec7a7ba2ee3f574965849f5d Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 17:39:08 -0400 Subject: [PATCH] Join VIMES and EIMES semantics --- docs/global-reference.md | 32 +++++++------ docs/name-resolution.md | 39 +++++++++++----- docs/phase-b-framework.md | 22 ++++++--- docs/script-inventory.md | 10 +++- docs/tools-reference.md | 8 +++- docs/vm-mapping-plan.md | 2 +- tools/extract_init.py | 73 +++++++++++++++++++++++++++++ tools/extract_message_table.py | 79 ++++++++++++++++++++++++++------ tools/init_table_profile.py | 42 ++++++++++++----- tools/test_extract_init.py | 76 ++++++++++++++++++++++++++++-- tools/test_globals.py | 11 +++++ tools/test_init_table_profile.py | 22 +++++++++ vm-map/globals.toml | 67 +++++++++++++++++++++++++++ 13 files changed, 416 insertions(+), 67 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 4e589e6..bfb1ac8 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5039 globals (256 curated, 4783 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5041 globals (262 curated, 4779 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -59,8 +59,10 @@ | `0x5660b` | skill_info_revealed_flags | high | investigation | Persistent skill-information visibility flags. ADDSKILL sets the selected skill, BTL marks every equipped skill when it is observed in combat, and INFOIT suppresses a skill's icon/handler-driven details until this flag is nonzero. This is broader than skill_acquired_flags. | | `0x56738` | entity_battle_routine_random_rolls | high | investigation | Per-runtime-entity battle-step random rolls. FIELD and BTL fill every active entity's twenty cells with random-modulo-100; BTRTN executes a step when the matching roll is below battle_routine_activation_percents. | | `0x56b20` | entity_patrol_waypoint_indices | high | investigation | RTN_M011 compares each step's one-based waypoint_ordinal minus one with the current entity's value. Reaching the selected destination advances this value modulo the largest RTN_M011 waypoint ordinal in the routine set. | +| `0x56b85` | enemy_encyclopedia_revealed_flags | high | investigation | Persistent enemy-information reveal state. BTL marks both participating unit definitions as revealed and synchronizes their cells; INFOEN masks unrevealed unit names and details while still listing EBINIT rows enabled by unit_enemy_info_listed. | | `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. | | `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. | @@ -191,6 +193,7 @@ | `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. | +| `0x15a2a9` | glossary_topic_unlock_seen_decision_ids | high | investigation | VIINIT's three unlock prerequisites per glossary topic. INFOVO subtracts one from each positive value and exposes the topic when any referenced scene_decision_seen_flags cell is set; debug modes expose the same populated topics unconditionally. Columns: 0=unlock_seen_decision_1, 1=unlock_seen_decision_2, 2=unlock_seen_decision_3. | | `0x5` | — | low | auto-shape | array | | `0xd2` | — | low | auto-shape | array | | `0xd7` | — | low | auto-shape | array | @@ -223,7 +226,6 @@ | `0x4315` | — | low | auto-shape | array | | `0x4379` | — | low | auto-shape | array | | `0x45d7` | — | low | auto-shape | array | -| `0x463b` | — | low | auto-shape | array | | `0x671c` | — | low | auto-shape | array | | `0x671f` | — | low | auto-shape | array | | `0x6727` | — | low | auto-shape | array | @@ -248,11 +250,9 @@ | `0x55e3b` | — | low | auto-shape | array | | `0x56223` | — | low | auto-shape | array | | `0x56b52` | — | low | auto-shape | array | -| `0x56b85` | — | low | auto-shape | array | | `0x56f6d` | — | low | auto-shape | array | | `0x577a3` | — | low | auto-shape | array | | `0x5f0bb` | — | low | auto-shape | array | -| `0x5f0ed` | — | low | auto-shape | array | | `0x617fe` | — | low | auto-shape | array | | `0x61be6` | — | low | auto-shape | array | | `0x61c4a` | — | low | auto-shape | array | @@ -304,16 +304,6 @@ | `0xe6ae0` | — | low | auto-shape | array | | `0xe6c2a` | — | low | auto-shape | array | | `0xe6c48` | — | low | auto-shape | array | -| `0xe6c5e` | — | low | auto-shape | array | -| `0xe6cc2` | — | low | auto-shape | array | -| `0xe6d26` | — | low | auto-shape | array | -| `0xe6d8a` | — | low | auto-shape | array | -| `0xe6e52` | — | low | auto-shape | array | -| `0xe6eb6` | — | low | auto-shape | array | -| `0xe6f1a` | — | low | auto-shape | array | -| `0xe6f7e` | — | low | auto-shape | array | -| `0xe710e` | — | low | auto-shape | array | -| `0xe7172` | — | low | auto-shape | array | | `0xe7305` | — | low | auto-shape | array | | `0xe730e` | — | low | auto-shape | array | | `0xe73bb` | — | low | auto-shape | array | @@ -415,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. | +| `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. | ## story-flag @@ -472,6 +464,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. | +| `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) | | `0x277` | — | med | auto-shape | string-table (written by SC0130) | | `0x278` | — | med | auto-shape | string-table (written by FIELD) | @@ -5046,7 +5039,17 @@ | `0xccc33` | — | med | auto-shape | record-table[stride 3] | | `0xccc93` | — | med | auto-shape | record-table[stride 53] | | `0xe6afe` | — | med | auto-shape | record-table[stride 10] | +| `0xe6c5e` | — | med | auto-shape | obinit-field | +| `0xe6cc2` | — | med | auto-shape | obinit-field | +| `0xe6d26` | — | med | auto-shape | obinit-field | +| `0xe6d8a` | — | med | auto-shape | obinit-field | +| `0xe6e52` | — | low | auto-shape | obinit-field? | +| `0xe6eb6` | — | med | auto-shape | obinit-field | +| `0xe6f1a` | — | low | auto-shape | obinit-field? | +| `0xe6f7e` | — | low | auto-shape | obinit-field? | | `0xe6fe2` | — | med | auto-shape | record-table[stride 3] | +| `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] | @@ -5078,7 +5081,6 @@ | `0x158154` | — | med | auto-shape | record-table[stride 4] | | `0x1590f4` | — | med | auto-shape | record-table[stride 4] | | `0x15a097` | — | med | auto-shape | record-table[stride 4] | -| `0x15a2a9` | — | med | auto-shape | record-table[stride 3] | | `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 9b706a1..6189fc1 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -221,13 +221,27 @@ after the hit check and selects the critical-result state on success. Column 8 i for both `item_stat_modifiers` and `skill_combat_stat_deltas`; the skill descriptions and matching item columns also confirm evasion, magic defense, and speed. -ITMES and SKMES are now joined back to their INIT records by a reusable id-dispatch extractor: all 287 item -ids and all 131 skill ids match exactly. `init_table_profile.py --message-query REGEX` puts the complete -player-facing description beside every populated field, which confirms the item/skill condition, resource, -range, combat-stat, and restriction mappings without relying on column position. The same CHMENU trace -identifies SKINIT `0xa70b2` as `skill_change_catalog_eligible`, distinguishes persistent -`skill_acquired_flags` from broader `skill_info_revealed_flags`, and the explicit ITMES “female-only” record -raises `item_sex_restriction_mask` to high confidence. +ITMES, SKMES, VIMES, and EIMES are now joined back to their INIT records by a reusable id-dispatch +extractor. It recognizes both the ITMES/SKMES fallthrough layout and the VIMES/EIMES layout whose compact +guard block branches forward to separately stored message bodies. All 287 item ids, 131 skill ids, and 65 +glossary-topic ids match their INIT definitions exactly; all 192 sparse EIMES ids resolve to EBINIT unit +definitions. `init_table_profile.py --message-query REGEX` puts the complete player-facing text beside +every populated field, which confirms item/skill condition, resource, range, combat-stat, and restriction +mappings without relying on column position. The same CHMENU trace identifies SKINIT `0xa70b2` as +`skill_change_catalog_eligible`, distinguishes persistent `skill_acquired_flags` from broader +`skill_info_revealed_flags`, and the explicit ITMES “female-only” record raises +`item_sex_restriction_mask` to high confidence. + +VIINIT is a sparse 200-row glossary schema with 65 populated topic ids. Its global-string array supplies +the list title, while a three-column row table supplies up to three one-based scene-decision prerequisites; +the shipped data populates the first column for every topic and the second for thirteen topics. INFOVO +subtracts one from each positive prerequisite and exposes a topic when any referenced +`scene_decision_seen_flags` cell is set. VIMES supplies the full title/description body for every one of +those 65 ids. EIMES instead supplies two untitled lines—`summary` and `strategy`—for 192 EBINIT unit ids. +INFOEN lists the 183 definitions whose `unit_enemy_info_listed` field is set, masks details until the +corresponding `enemy_encyclopedia_revealed_flags` cell is set, and uses the selected EBINIT id as EIMES's +dispatch key. Nine authored EIMES boss variants are valid definitions but are not enabled in that shipped +list. 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 @@ -631,11 +645,12 @@ The v1 map labels *shapes and tables*; the next increments add *meaning*, cheape 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, - resume the remaining `*MES` writers and other reader-proven 2D tables; never assign one universal - meaning to a parameter bank whose meaning varies by provider selector. -2. **Extend message-table joins beyond the completed ITMES/SKMES pair** (`VIMES`, other id dispatchers, …) - and fold in other `set-string`/`copy-to-global` writers not covered by the `*INIT` set. + shipped movement steps joined to provider behavior. With RTINIT's used-provider surface closed and + ITMES/SKMES/VIMES/EIMES joined, resume the remaining `*MES` writers and other reader-proven 2D tables; + never assign one universal meaning to a parameter bank whose meaning varies by provider selector. +2. **Extend message-table joins beyond the completed ITMES/SKMES/VIMES/EIMES set**, starting with + CIMES's matching branch-target shape, then classify the smaller non-ID-dispatch message scripts and + 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 `rec[sN]` table and infer purpose from context (e.g. RECOVER's 30-wide tables ↔ a status/recovery system). Static, medium effort. diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index 712a13f..b4bb170 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -728,13 +728,21 @@ Its linked-table shapes are 18 SKINIT and 82 EBINIT populated columns. The next state flags, per-action unlock requirements, SCJUMP event ids, normal/brainwashed unit variants, and SALLY's per-unit bonus item. Combat tracing also names the shared column-8 item/skill modifier as critical chance. -The message-table tranche adds a reusable extractor for global-id dispatch chains and joins player-facing -ITMES/SKMES text back to INIT records. All 287 item ids and all 131 skill ids match exactly in both -directions. Each joined message retains its rendered title, richer description, furigana annotations, and -dispatch offset independently of INIT's shorter effect label; generated profiles report complete coverage. -The dispatch keys also establish `current_item_id` and `current_skill_id` as high-confidence shared index -slots. This makes message/field correlation the next evidence source for the remaining sparse item and skill -columns. +The message-table tranche adds a reusable extractor for both shipped global-id control-flow layouts and +joins player-facing ITMES/SKMES/VIMES/EIMES text back to INIT records. All 287 item ids, 131 skill ids, +and 65 glossary topic ids match exactly in both directions; all 192 sparse EIMES ids resolve to EBINIT +unit definitions. Joined records retain dispatch and message-body offsets, rendered text, and furigana. +Title-bearing tables expose title/description, while EIMES preserves its untitled lines as enemy summary +and strategy. The dispatch keys establish `current_item_id`, `current_skill_id`, +`current_glossary_topic_id`, and `current_enemy_encyclopedia_unit_id` as high-confidence shared index +slots. + +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. +INFOEN likewise gates EBINIT's enabled enemy-information rows through persistent encounter/reveal flags. +The generated VIINIT profile reports 65/65 VIMES coverage, while EBINIT reports 192/277 EIMES coverage; +the remaining EBINIT rows are valid player/unused unit definitions rather than missing EIMES records. The follow-up correlation pass makes that evidence directly queryable with `init_table_profile.py --message-query REGEX` and moves confirmed item/skill row-column meanings into diff --git a/docs/script-inventory.md b/docs/script-inventory.md index c57b7cc..e3f2c2a 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -115,7 +115,13 @@ instantiated steps to join. ### Message/string tables (`*MES`) `ITMES` (64 KB — item text), `VIMES` (43 KB), `EIMES` (37 KB), `SKMES` (31 KB — skill text), `CIMES` (15 KB), `MAMES`, `INFOMES`, `MES` — where most translatable text -outside scenes lives. +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, and EIMES's 192 sparse EBINIT enemy-commentary +records. ITMES/SKMES use fallthrough message bodies; VIMES/EIMES place all guards first and branch to +later bodies. VIMES records retain title plus multiline description, while EIMES's untitled two-line +records expose `summary` and `strategy`. CIMES has the same branch-target dispatcher shape and is the +next unjoined message table; the smaller MAMES/INFOMES/MES scripts need separate structural +classification. ### Battle system `BTL` (61 KB — main battle loop), `BTRTN`, `ROUND`, `AIM`, `ATSEEK`, `MVSEEK`, @@ -141,7 +147,7 @@ outside scenes lives. ### Menus / UI / meta `MENU`, `CHMENU` (69 KB — character menu), `INFO`/`INFOAF`/`INFOCH`/`INFOEN`/ -`INFOIT`/`INFOVO` (info panels: characters, enemies, items, voices), `SAVE` (40 KB), +`INFOIT`/`INFOVO` (info panels: characters, enemies, items, glossary/help topics), `SAVE` (40 KB), `HISTORY`, `HIDEWIN`, `CLOSE`, `INPUTNAME` (28 KB), `CGMODE` (24 KB — gallery), `MMODE` (music mode), `HMODE` (22 KB — scene replay) diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 466b5c0..a67c8ec 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -54,12 +54,16 @@ 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 a repeated global-id dispatch chain such as ITMES/SKMES, reconstruct player-facing title/description lines (including furigana surface text and readings), and emit an ID-keyed message table with bytecode provenance. | `extract_message_table.py [OUTNAME]` | `.BIN` → `build/data/.json` | +| `extract_message_table.py` | Discover repeated global-id dispatch chains in either fallthrough-body (ITMES/SKMES) or branch-target-body (VIMES/EIMES) 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`. | `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. 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 and SKINIT join ITMES/SKMES messages. Top-level `field_semantics` maps raw keys to canonical global/column names, while each record's `semantic_fields` is the generated name-keyed join; complete footer copies map to a row and expose its values without the raw provenance wrapper. Raw keys and unresolved tagged payload cells remain provenance. OUTNAME accepts a stem or one `.json` suffix and rejects paths. Refreshes the generated data index. | `extract_init.py [OUTNAME] [--mode …]` | `
.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, `build/callscript-names.json` 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 also report message coverage; rule tables summarize covered units, titled and level-independent rules, threshold/slot distributions, and skill awards; dispatch tables summarize assignments, overwrites, script-name resolution, and SCJUMP chapter agreement; banked tables summarize populated/reserved banks, movement/battle steps and providers, selector-specific semantic coverage, and overwrite conflicts. `--message-query REGEX` shows every matching name/message beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py
[--build] [--limit N] [--message-query REGEX]` | `build/data/
.json` + corpus → stdout; with `--build`, `build/data/
-field-profile.{json,md}` | +| `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 both title/description and summary/strategy 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 a VIINIT name-mode specialization for its sparse 200-row glossary table, +whose up-to-three unlock writes precede each title rather than follow it. Matching message joins are +ITINIT→ITMES, SKINIT→SKMES, VIINIT→VIMES, and EBINIT→EIMES; the last is intentionally sparse. + ## VM | Tool | Purpose | Run | Reads → Writes | diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index 1e70a61..f179d45 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's entire used-provider surface now closed at 1,043/1,043 movement steps, fold in remaining `*MES` writers and label other 2D record tables by their reader scripts. Then Frida to name *which stat* each unresolved field is. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids, SCJUMP decision-to-scene dispatch, and the shipped RTINIT movement-provider join are resolved; see `docs/name-resolution.md` #1 and `docs/scjump-progression.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 joined to their definitions, decode CIMES's matching branch-target dispatcher, 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 four 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 1294413..74ad069 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -493,10 +493,17 @@ UNIT_STAT_COLUMNS = ( ) MESSAGE_TABLES = { + "EBINIT": "EIMES", "ITINIT": "ITMES", "SKINIT": "SKMES", + "VIINIT": "VIMES", } +VOCABULARY_NAME_ARRAY_BASE = 0x463B +VOCABULARY_RECORD_TABLE_BASE = 0x15A2A9 +VOCABULARY_RECORD_STRIDE = 3 +VOCABULARY_RECORD_SPAN = 200 + def resolve(name: str) -> Path: for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"): @@ -1103,6 +1110,70 @@ def extract_name(scr): "desc_array_bases": {k: f"0x{v:x}" for k, v in sorted(desc_bases.items())}} +def extract_vocabulary(scr): + """Extract VIINIT's sparse glossary names and pre-name row-table writes.""" + 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] - VOCABULARY_NAME_ARRAY_BASE + if not (1 <= record_id < VOCABULARY_RECORD_SPAN): + raise ValueError( + f"{scr.path.name}: glossary 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": {}, + "record_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 + relative = destination - VOCABULARY_RECORD_TABLE_BASE + if not (0 <= relative < VOCABULARY_RECORD_SPAN * VOCABULARY_RECORD_STRIDE): + raise ValueError( + f"{scr.path.name}: unexpected integer write 0x{destination:x}" + ) + record_id, column = divmod(relative, VOCABULARY_RECORD_STRIDE) + if record_id not in by_id: + raise ValueError( + f"{scr.path.name}: integer write for unnamed glossary id {record_id}" + ) + _store_unique( + by_id[record_id]["record_fields"], + ( + f"0x{VOCABULARY_RECORD_TABLE_BASE:x}/" + f"{VOCABULARY_RECORD_STRIDE}/{column}" + ), + value, + record_id, + ) + + record_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("/"))) + return records, { + "name_array_base": f"0x{VOCABULARY_NAME_ARRAY_BASE:x}", + "name_write_base": f"0x{VOCABULARY_NAME_ARRAY_BASE + 1:x}", + "first_record_id": 1, + "record_span": VOCABULARY_RECORD_SPAN, + "record_field_columns": record_columns, + } + + @cache def object_type_definitions() -> dict[int, dict]: """Load OBINIT's authoritative display and state-row metadata by object type id.""" @@ -1914,6 +1985,8 @@ def main() -> int: "dispatch": extract_dispatch, "banked": extract_banked, }[mode] + if mode == "name" and name == "VIINIT": + extractor = extract_vocabulary 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 c2cf0c2..f897059 100644 --- a/tools/extract_message_table.py +++ b/tools/extract_message_table.py @@ -1,21 +1,23 @@ #!/usr/bin/env python3 """Extract an ID-dispatched SYS4 message table. -ITMES and SKMES are long chains of: +The shipped tables use two equivalent control-flow shapes: eq , , - jcc ... - show-text ... - ... + jcc + jmp -This tool discovers the selector global from that repeated shape, reconstructs -the displayed lines (including the surface text of furigana spans), and emits a -reusable ID-to-message JSON table. +or a compact guard block whose successful branches target message bodies stored +later in the script. This tool discovers the selector global, follows either +layout, reconstructs the displayed lines (including the surface text of +furigana spans), and emits a reusable ID-to-message JSON table. Usage: py -3.11 -X utf8 tools/extract_message_table.py ITMES py -3.11 -X utf8 tools/extract_message_table.py SKMES [OUTNAME] + py -3.11 -X utf8 tools/extract_message_table.py VIMES + py -3.11 -X utf8 tools/extract_message_table.py EIMES """ from __future__ import annotations @@ -33,6 +35,11 @@ import sys4load T_IMM = 0 T_INLINE_STRING = 2 T_GLOBAL_INT = 3 +BRANCH_SENTINEL = 0xFFFFFFFF + +MESSAGE_LAYOUTS = { + "EIMES.BIN": "enemy-commentary", +} def resolve(name: str) -> Path: @@ -77,7 +84,9 @@ def _strings(ins, scr) -> list[str]: ] -def _message_body(scr, start: int, stop: int) -> dict | None: +def _message_body( + scr, start: int, stop: int, layout: str = "title-description" +) -> dict | None: fragments: list[str] = [] lines: list[str] = [] furigana: list[dict] = [] @@ -102,15 +111,34 @@ def _message_body(scr, start: int, stop: int) -> dict | None: lines.append("".join(fragments)) if not lines: return None - message = { - "title": lines[0], - "description": "\n".join(lines[1:]), - } + if layout == "enemy-commentary": + message = { + "summary": lines[0], + "strategy": "\n".join(lines[1:]), + } + else: + message = { + "title": lines[0], + "description": "\n".join(lines[1:]), + } if furigana: message["furigana"] = furigana return message +def _branch_target_index(scr, guard_index: int, offsets: dict[int, int]) -> int | None: + """Resolve the conditional branch immediately following a dispatch guard.""" + if guard_index + 1 >= len(scr.instructions): + return None + branch = scr.instructions[guard_index + 1] + if sys4load.display_label(branch.opcode) != "jcc": + return None + for arg_type, value in branch.args[1:]: + if arg_type == T_IMM and value != BRANCH_SENTINEL and value in offsets: + return offsets[value] + return None + + def extract_messages(scr, selector: int | None = None) -> tuple[list[dict], dict]: """Extract ordered message records and dispatch metadata from a script.""" discovered_selector, comparison_count = discover_selector(scr) @@ -122,24 +150,49 @@ def extract_messages(scr, selector: int | None = None) -> tuple[list[dict], dict ] records: list[dict] = [] seen: set[int] = set() + offsets = { + instruction.offset: index + for index, instruction in enumerate(scr.instructions) + } + message_layout = MESSAGE_LAYOUTS.get(scr.path.name.upper(), "title-description") + dispatch_layout_counts = collections.Counter() for guard_index, (instruction_index, record_id) in enumerate(guards): stop = guards[guard_index + 1][0] if guard_index + 1 < len(guards) else len(scr.instructions) - message = _message_body(scr, instruction_index + 1, stop) + message_start = instruction_index + 2 + message = _message_body(scr, message_start, stop, message_layout) + dispatch_layout = "fallthrough" + if message is None: + branch_target = _branch_target_index(scr, instruction_index, offsets) + if branch_target is not None: + message_start = branch_target + message = _message_body( + scr, message_start, len(scr.instructions), message_layout + ) + dispatch_layout = "branch-target" if message is None: continue if record_id in seen: raise ValueError(f"{scr.path.name}: duplicate message id {record_id}") seen.add(record_id) + dispatch_layout_counts[dispatch_layout] += 1 records.append({ "id": record_id, "dispatch_offset": f"0x{scr.instructions[instruction_index].offset:x}", + "message_offset": f"0x{scr.instructions[message_start].offset:x}", **message, }) + dispatch_layout = ( + next(iter(dispatch_layout_counts)) + if len(dispatch_layout_counts) == 1 + else "mixed" + ) return records, { "selector_global": f"0x{selector:x}", "dispatch_guard_count": len(guards), "message_count": len(records), "selector_discovery_count": comparison_count, + "dispatch_layout": dispatch_layout, + "message_layout": message_layout, } diff --git a/tools/init_table_profile.py b/tools/init_table_profile.py index 7e73f0d..71db2ad 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -32,6 +32,14 @@ import sys4load GLOBAL_OPERAND_TYPES = {3, 4, 5, 6, 8} +def message_heading_body(message: dict) -> tuple[str, str]: + """Return presentation-neutral heading/body text for supported MES layouts.""" + return ( + message.get("title", message.get("summary", "")), + message.get("description", message.get("strategy", "")), + ) + + def load_table(name: str) -> dict: path = paths.BUILD / "data" / f"{name}.json" if not path.exists(): @@ -76,7 +84,8 @@ def profile_columns(data: dict) -> list[dict]: **extra, } if message := record.get("message"): - example["message_description"] = message.get("description", "") + _, body = message_heading_body(message) + example["message_description"] = body examples[key].append(example) for record in records: @@ -264,24 +273,30 @@ def profile_messages(data: dict) -> dict: with_furigana = [ record for record in with_message if record["message"].get("furigana") ] + examples = [] + for record in with_message[:5]: + heading, body = message_heading_body(record["message"]) + examples.append({ + "id": record["id"], + "name": record.get("name", ""), + "title": heading, + "description": body, + "message_fields": { + key: record["message"][key] + for key in ("title", "description", "summary", "strategy") + if key in record["message"] + }, + }) return { "population": len(with_message), "coverage": len(with_message) / len(records) if records else 0.0, "furigana_records": len(with_furigana), - "examples": [ - { - "id": record["id"], - "name": record.get("name", ""), - "title": record["message"]["title"], - "description": record["message"]["description"], - } - for record in with_message[:5] - ], + "examples": examples, } def find_message_matches(data: dict, pattern: str) -> list[dict]: - """Return records whose name/title/description matches a regular expression.""" + """Return records whose name or supported message text matches a regex.""" regex = re.compile(pattern, re.IGNORECASE) return [ record @@ -290,6 +305,8 @@ def find_message_matches(data: dict, pattern: str) -> list[dict]: record.get("name", ""), record.get("message", {}).get("title", ""), record.get("message", {}).get("description", ""), + record.get("message", {}).get("summary", ""), + record.get("message", {}).get("strategy", ""), ])) ] @@ -314,7 +331,8 @@ def render_message_matches(data: dict, pattern: str) -> str: for key, value in sorted(fields.items()) ) name = record.get("name", "").replace("|", "\\|") - description = record.get("message", {}).get("description", "").replace("|", "\\|") + _, body = message_heading_body(record.get("message", {})) + description = body.replace("|", "\\|") lines.append( f"| {record['id']} | {name} | {description} | {rendered_fields} |" ) diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 640bf2c..9518605 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -88,6 +88,22 @@ def test_real_name_tables() -> None: check(object_by_id[28]["name"] == "カード取得", "OBINIT object ids provide authoritative STINIT type labels") + vocabulary, vocabulary_meta = extract_init.extract_vocabulary( + sys4load.load(scripts["VIINIT.BIN"]) + ) + vocabulary_by_id = {record["id"]: record for record in vocabulary} + check( + vocabulary_meta["record_span"] == 200 + and len(vocabulary) == 65 + and vocabulary[-1]["id"] == 120, + "VIINIT extracts 65 sparse glossary topics from its 200-row table", + ) + check( + vocabulary_by_id[1]["name"] == "【迷宮】占有率" + and vocabulary_by_id[1]["record_fields"]["0x15a2a9/3/0"] == 201, + "VIINIT associates pre-name unlock writes with the correct topic", + ) + def test_static_negative_write() -> None: class Instruction: @@ -561,10 +577,12 @@ def test_real_routine_banks() -> None: def test_real_message_tables() -> None: scripts = paths.scripts() expected = { - "ITMES.BIN": (0x8C877, 287), - "SKMES.BIN": (0xA6E59, 131), + "ITMES.BIN": (0x8C877, 287, "fallthrough"), + "SKMES.BIN": (0xA6E59, 131, "fallthrough"), + "VIMES.BIN": (0x15A2A8, 65, "branch-target"), + "EIMES.BIN": (0x15A759, 192, "branch-target"), } - for name, (selector, count) in expected.items(): + for name, (selector, count, dispatch_layout) in expected.items(): records, meta = extract_message_table.extract_messages( sys4load.load(scripts[name]) ) @@ -575,6 +593,8 @@ def test_real_message_tables() -> None: f"{name}: every dispatch guard yields a message") check(len({record['id'] for record in records}) == len(records), f"{name}: message ids are unique") + check(meta["dispatch_layout"] == dispatch_layout, + f"{name}: recognizes its {dispatch_layout} dispatch layout") item_messages, _ = extract_message_table.extract_messages( sys4load.load(scripts["ITMES.BIN"]) @@ -598,6 +618,27 @@ def test_real_message_tables() -> None: check(skills[1]["description"] == " 床のない地形を移動可能になる", "SKMES skill 1 keeps its player-facing behavior") + vocabulary_messages, vocabulary_meta = extract_message_table.extract_messages( + sys4load.load(scripts["VIMES.BIN"]) + ) + vocabulary = {record["id"]: record for record in vocabulary_messages} + check(vocabulary[1]["title"] == "『【迷宮】占有率』" + and "各勢力の占領度合" in vocabulary[1]["description"], + "VIMES follows branch targets and reconstructs glossary help text") + check(vocabulary_meta["message_layout"] == "title-description", + "VIMES retains the title/description message layout") + + enemy_messages, enemy_meta = extract_message_table.extract_messages( + sys4load.load(scripts["EIMES.BIN"]) + ) + enemies = {record["id"]: record for record in enemy_messages} + check(enemies[101]["summary"] == "高い能力を秘めた隣国の姫騎士" + and enemies[101]["strategy"] == "初遭遇時にはまず勝てない", + "EIMES exposes its two lines as enemy summary and strategy") + check(enemy_meta["message_layout"] == "enemy-commentary" + and "title" not in enemies[101], + "EIMES does not mislabel its first commentary line as a title") + def test_message_join() -> None: scripts = paths.scripts() @@ -621,6 +662,35 @@ def test_message_join() -> None: check(joined["IT"][1]["message"]["description"] == expected["IT"][1], "INIT/MES join uses the shared runtime id") + vocabulary, _ = extract_init.extract_vocabulary( + sys4load.load(scripts["VIINIT.BIN"]) + ) + vocabulary_meta = extract_init.join_messages( + vocabulary, sys4load.load(scripts["VIMES.BIN"]) + ) + check( + vocabulary_meta["joined_count"] == 65 + and not vocabulary_meta["init_ids_without_message"] + and not vocabulary_meta["message_ids_without_init"], + "VIINIT and VIMES form a complete 65-topic runtime-id join", + ) + + units, _ = extract_init.extract_name(sys4load.load(scripts["EBINIT.BIN"])) + enemy_meta = extract_init.join_messages( + units, sys4load.load(scripts["EIMES.BIN"]) + ) + unit_by_id = {record["id"]: record for record in units} + check( + enemy_meta["joined_count"] == 192 + and len(enemy_meta["init_ids_without_message"]) == 85 + and not enemy_meta["message_ids_without_init"], + "EIMES joins 192 sparse enemy-commentary rows to EBINIT", + ) + check( + unit_by_id[101]["message"]["strategy"] == "初遭遇時にはまず勝てない", + "EBINIT records expose EIMES strategy text by unit id", + ) + def test_field_semantics() -> None: scripts = paths.scripts() diff --git a/tools/test_globals.py b/tools/test_globals.py index 85b6ba2..7a20725 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -58,6 +58,17 @@ def test_load_and_lint(): "AI action-element eligibility state is curated") check(entries[0xcc9fe]["name"] == "healing_action_scope_masks", "AI healing-action eligibility state is curated") + check(entries[0x15a2a8]["name"] == "current_glossary_topic_id" + and entries[0x15a2a9]["name"] + == "glossary_topic_unlock_seen_decision_ids" + and entries[0x463b]["name"] == "glossary_topic_titles", + "VIINIT/VIMES glossary state is curated") + check(entries[0x15a759]["name"] == "current_enemy_encyclopedia_unit_id" + and entries[0x56b85]["name"] + == "enemy_encyclopedia_revealed_flags", + "EBINIT/EIMES enemy-encyclopedia 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", "faction-specific tile traversal masks are curated") check(entries[0xaba64]["name"] == "stage_object_runtime_flags", diff --git a/tools/test_init_table_profile.py b/tools/test_init_table_profile.py index bdd5b6e..b3acd18 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -178,6 +178,28 @@ def main() -> int: rendered = profile.render_message_matches(fixture, "First") assert "| 1 | one | First |" in rendered assert "`test_record.zero` (`0x30/3/0`)=9" in rendered + + enemy_fixture = { + "table": "ENEMY", + "records": [ + { + "id": 101, + "name": "boss", + "message": { + "summary": "Powerful knight", + "strategy": "Avoid the first encounter", + }, + } + ], + } + enemy_messages = profile.profile_messages(enemy_fixture) + assert enemy_messages["examples"][0]["title"] == "Powerful knight" + assert enemy_messages["examples"][0]["description"] == "Avoid the first encounter" + assert enemy_messages["examples"][0]["message_fields"] == { + "summary": "Powerful knight", + "strategy": "Avoid the first encounter", + } + assert profile.find_message_matches(enemy_fixture, "first encounter") print("all init_table_profile checks passed") return 0 diff --git a/vm-map/globals.toml b/vm-map/globals.toml index a817725..91c182d 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -242,6 +242,73 @@ usage = "Shared skill-id argument/selection slot. Skill menus and combat scripts source = "investigation" confidence = "high" +[[global]] +address = "0x463b" +name = "glossary_topic_titles" +category = "string-table" +type = "string[200]" +value_domain = "VIINIT player-facing glossary title indexed by topic id; 65 populated shipped ids" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x15a2a8", "0x15a2a9"] + +[[global]] +address = "0x15a2a8" +name = "current_glossary_topic_id" +category = "index-pointer" +type = "int" +value_domain = "VIINIT glossary topic id in 1..199; shipped populated ids end at 120" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x463b", "0x15a2a9"] + +[[global]] +address = "0x15a2a9" +name = "glossary_topic_unlock_seen_decision_ids" +category = "data-table" +type = "int[200][3]" +columns = { "0" = "unlock_seen_decision_1", "1" = "unlock_seen_decision_2", "2" = "unlock_seen_decision_3" } +value_domain = "one-based scene decision id; zero means no prerequisite in that slot" +usage = "VIINIT's three unlock prerequisites per glossary topic. INFOVO subtracts one from each positive value and exposes the topic when any referenced scene_decision_seen_flags cell is set; debug modes expose the same populated topics unconditionally." +source = "investigation" +confidence = "high" +depends_on = ["0x5f0ed", "0x15a2a8"] + +[[global]] +address = "0x5f0ed" +name = "scene_decision_seen_flags" +category = "data-table" +type = "int[10000]" +value_domain = "mostly {0,1}, indexed by SCJUMP decision id" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x62ccf", "0x15a2a9"] + +[[global]] +address = "0x56b85" +name = "enemy_encyclopedia_revealed_flags" +category = "data-table" +type = "int[1000]" +value_domain = "0 hidden; nonzero encountered/revealed, indexed by EBINIT unit id" +usage = "Persistent enemy-information reveal state. BTL marks both participating unit definitions as revealed and synchronizes their cells; INFOEN masks unrevealed unit names and details while still listing EBINIT rows enabled by unit_enemy_info_listed." +source = "investigation" +confidence = "high" +depends_on = ["0x712f6", "0x15a759"] + +[[global]] +address = "0x15a759" +name = "current_enemy_encyclopedia_unit_id" +category = "index-pointer" +type = "int" +value_domain = "EBINIT unit definition id" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x56b85", "0x712f6"] + [[global]] address = "0xa6e5a" name = "skill_sort_key"