Expose semantic INIT column names

This commit is contained in:
gamer147
2026-07-22 23:15:33 -04:00
parent 511895fc05
commit 00640696dc
11 changed files with 251 additions and 37 deletions

View File

@@ -1,7 +1,7 @@
<!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build --> <!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build -->
# Global Variable Reference (generated) # Global Variable Reference (generated)
5023 globals (145 curated, 4878 auto shape-inferred). Source of truth: `vm-map/globals.toml`. 5023 globals (148 curated, 4875 auto shape-inferred). Source of truth: `vm-map/globals.toml`.
## choice-output ## choice-output
@@ -27,8 +27,10 @@
| address | name | conf | source | usage | | address | name | conf | source | usage |
|---|---|---|---|---| |---|---|---|---|---|
| `0x2e49` | character_voice_suppressed | high | investigation | Base of the per-character voice enable/suppress settings. INITCONFIG zero-fills all 13 cells and registers each with the shared profile service; LOADCONFIG restores them. CONFIG indexes the table to preview a character voice and write 0/1. ROOM reads cell 0 before assigning its selected greeter's greeting/farewell voice ids, so the port's former scalar interpretation of zero-int-range (writing 13 into the base cell) suppressed those voices on every natural boot. This names the script-visible setting array without choosing a persistence backend for op 0x1a2/0x1a3. | | `0x2e49` | character_voice_suppressed | high | investigation | Base of the per-character voice enable/suppress settings. INITCONFIG zero-fills all 13 cells and registers each with the shared profile service; LOADCONFIG restores them. CONFIG indexes the table to preview a character voice and write 0/1. ROOM reads cell 0 before assigning its selected greeter's greeting/farewell voice ids, so the port's former scalar interpretation of zero-int-range (writing 13 into the base cell) suppressed those voices on every natural boot. This names the script-visible setting array without choosing a persistence backend for op 0x1a2/0x1a3. |
| `0x65ce` | skill_acquired_flags | high | investigation | Persistent acquired-skill flags. ADDSKILL sets the selected skill after resolving the unit's equipped-skill slots; FORT checks the flag before granting a skill; CHMENU combines it with skill_change_catalog_eligible to build the available skill-change catalog. |
| `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. | | `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. |
| `0x67a0` | party_slot_character_id | high | investigation | Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster. | | `0x67a0` | party_slot_character_id | high | investigation | Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster. |
| `0x5660b` | skill_info_revealed_flags | high | investigation | Persistent skill-information visibility flags. ADDSKILL sets the selected skill, BTL marks every equipped skill when it is observed in combat, and INFOIT suppresses a skill's icon/handler-driven details until this flag is nonzero. This is broader than skill_acquired_flags. |
| `0x66716` | unit_voice_asset_ids | high | investigation | EBINIT per-unit voice bank for 116 voiced characters and variants. Resolving the values through SYS4INI yields character OGG clips (for example Lily's row is LILA1381..1406); BTL, FIELD, and SHOWGROW select columns for battle, map, and level-up reactions. | | `0x66716` | unit_voice_asset_ids | high | investigation | EBINIT per-unit voice bank for 116 voiced characters and variants. Resolving the values through SYS4INI yields character OGG clips (for example Lily's row is LILA1381..1406); BTL, FIELD, and SHOWGROW select columns for battle, map, and level-up reactions. |
| `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 the requested variant, loads it into the status-panel texture slot, and draws it at the lower right. | | `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 the requested variant, loads it into the status-panel texture slot, and draws it at the lower right. |
| `0x6e7fe` | unit_map_sprite_asset_ids | high | investigation | Five-context unit sprite-sheet table for 251 units. The ids resolve to CP sprite atlases (compact AA and full directional AB sheets); FIELD and DRAWCH use them on the map, while INFOCH/INFOEN and DRAWENP reuse the same animated unit representation. | | `0x6e7fe` | unit_map_sprite_asset_ids | high | investigation | Five-context unit sprite-sheet table for 251 units. The ids resolve to CP sprite atlases (compact AA and full directional AB sheets); FIELD and DRAWCH use them on the map, while INFOCH/INFOEN and DRAWENP reuse the same animated unit representation. |
@@ -75,26 +77,27 @@
| `0x8dc01` | item_icon_id | high | investigation | ITINIT field for all 287 items. READICON sorts and indexes this array to select icon atlas entries; ALCHEMY, CHMENU, IMPROVE, and INFOIT use the same value for item-row presentation. | | `0x8dc01` | item_icon_id | high | investigation | ITINIT field for all 287 items. READICON sorts and indexes this array to select icon atlas entries; ALCHEMY, CHMENU, IMPROVE, and INFOIT use the same value for item-row presentation. |
| `0x8dfe9` | item_attack_element | high | investigation | Populated for 107 weapons/attacks. Values exactly match the AFINIT attack-attribute string table at GStr[0x2690 + value] and the Japanese item descriptions; combat scope/parameter code consumes the same array. | | `0x8dfe9` | item_attack_element | high | investigation | Populated for 107 weapons/attacks. Values exactly match the AFINIT attack-attribute string table at GStr[0x2690 + value] and the Japanese item descriptions; combat scope/parameter code consumes the same array. |
| `0x8e3d1` | item_defense_element | high | investigation | Populated for the 13 shields. Values match descriptions such as physical, universal, holy, dark, spirit, and divinity defense; DRAWTIP renders them through AFINIT's defense string table at GStr[0x26a4 + value]. | | `0x8e3d1` | item_defense_element | high | investigation | Populated for the 13 shields. Values match descriptions such as physical, universal, holy, dark, spirit, and divinity defense; DRAWTIP renders them through AFINIT's defense string table at GStr[0x26a4 + value]. |
| `0x8e7b9` | item_character_whitelist | high | investigation | Sparse ITINIT row-major table with stride 5. CHMENU rejects an item when column 0 is populated and the selected party slot's character id is absent from the row. Unique accessories 471/472 allow one character each, while crossover accessories 480..485 allow character ids 2, 3, and 4. Unused trailing columns remain zero. | | `0x8e7b9` | item_character_whitelist | high | investigation | Sparse ITINIT row-major table with stride 5. CHMENU rejects an item when column 0 is populated and the selected party slot's character id is absent from the row. Unique accessories 471/472 allow one character each, while crossover accessories 480..485 allow character ids 2, 3, and 4. Unused trailing columns remain zero. Columns: 0=allowed_character_id_1, 1=allowed_character_id_2, 2=allowed_character_id_3, 3=allowed_character_id_4, 4=allowed_character_id_5. |
| `0x8ff29` | item_sex_restriction_mask | med | investigation | Sparse ITINIT equipment restriction. CHMENU tests this mask against EBINIT field 0x71eae, whose values partition male, female, and sexless units; both populated items carry bit 2 and are therefore female-only. | | `0x8ff29` | item_sex_restriction_mask | high | investigation | Sparse ITINIT equipment restriction. CHMENU uses unit_sex_category as a bit index and rejects an item when that bit is absent from this mask. Both populated items carry only bit 2; EBINIT value 2 is female, and ITMES explicitly describes item 419 as female-only. |
| `0x906f9` | item_status_delta_levels | high | investigation | Sparse ITINIT row-major table consumed by USEITEM and CALCILL when applying an item's effects. Positive values inflict or drain; -5 removes a condition (for example paralysis-removal item 108 stores -5 in column 8). Confirmed columns are 2 HP drain, 3 SP drain, 4 FS drain, 5 curse, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear. | | `0x906f9` | item_status_delta_levels | high | investigation | Sparse ITINIT row-major table consumed by USEITEM and CALCILL when applying an item's effects. Positive values inflict or drain; -5 removes a condition (for example paralysis-removal item 108 stores -5 in column 8). Confirmed columns are 2 HP drain, 3 SP drain, 4 FS drain, 5 curse, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear. Columns: 2=hp_drain, 3=sp_drain, 4=fs_drain, 5=curse, 6=charm, 7=confusion, 8=paralysis, 9=poison, 10=water_flow, 11=fear. |
| `0x97c29` | item_equipped_status_levels | high | investigation | Sparse ITINIT row-major table added to a unit's 30-column condition state by CALCREVISE. Item descriptions identify populated columns 9 poison, 11 fear, 13 regeneration, and 14 exaltation; these are passive equipped effects, distinct from item_status_delta_levels. | | `0x97c29` | item_equipped_status_levels | high | investigation | Sparse ITINIT row-major table added to a unit's 30-column condition state by CALCREVISE. Item descriptions identify populated columns 9 poison, 11 fear, 13 regeneration, and 14 exaltation; these are passive equipped effects, distinct from item_status_delta_levels. Columns: 9=poison, 11=fear, 13=regeneration, 14=exaltation. |
| `0x9f159` | item_granted_skill_id | high | investigation | Populated for 85 equipment items. Values cross-resolve to SKINIT (for example flying bracelet=1 Flying, transfer bracelet=21 Transfer, thief key=22 Lockpick, ropes=156 Capture Attack); CALCREVISE applies the linked skill and UI scripts display it. | | `0x9f159` | item_granted_skill_id | high | investigation | Populated for 85 equipment items. Values cross-resolve to SKINIT (for example flying bracelet=1 Flying, transfer bracelet=21 Transfer, thief key=22 Lockpick, ropes=156 Capture Attack); CALCREVISE applies the linked skill and UI scripts display it. |
| `0x9f541` | item_stat_modifiers | high | investigation | ITINIT row-major equipment modifiers added directly to the unit's 14-column stat record by CALCREVISE. Descriptions and consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 8 critical chance, 9 capture power, 10 movement, 11 max HP, 12 max SP, and 13 max FS. CALCBTPARAM adds columns 7 and 8 into the action's critical percentage, and CALCDMG compares the clamped result with a random-modulo-100 roll. | | `0x9f541` | item_stat_modifiers | high | investigation | ITINIT row-major equipment modifiers added directly to the unit's 14-column stat record by CALCREVISE. Descriptions and consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 8 critical chance, 9 capture power, 10 movement, 11 max HP, 12 max SP, and 13 max FS. CALCBTPARAM adds columns 7 and 8 into the action's critical percentage, and CALCDMG compares the clamped result with a random-modulo-100 roll. Columns: 0=accuracy, 1=evasion, 2=physical_attack, 3=physical_defense, 4=magic_attack, 5=magic_defense, 6=speed, 7=luck, 8=critical_chance, 9=capture_power, 10=movement, 11=max_hp, 12=max_sp, 13=max_fs. |
| `0xa2bf1` | item_tuning_curve_ids | high | investigation | ITINIT row-major table selecting an equipment-growth curve for each of the ten tunable fields. TUNE, IMPROVE, DRAWTIP, and CALCREVISE combine each nonzero curve id with the item's corresponding tuning level and index the shared curve-value table at 0xab6fa. Columns align with item_stat_modifiers columns 0..9. | | `0xa2bf1` | item_tuning_curve_ids | high | investigation | ITINIT row-major table selecting an equipment-growth curve for each of the ten tunable fields. TUNE, IMPROVE, DRAWTIP, and CALCREVISE combine each nonzero curve id with the item's corresponding tuning level and index the shared curve-value table at 0xab6fa. Columns align with item_stat_modifiers columns 0..9. Columns: 0=accuracy, 1=evasion, 2=physical_attack, 3=physical_defense, 4=magic_attack, 5=magic_defense, 6=speed, 7=luck, 8=critical_chance, 9=capture_power. |
| `0xa5301` | item_resource_recovery_amounts | high | investigation | Sparse ITINIT row-major consumable table. USEITEM applies columns 0, 1, and 2 to the matching three-column unit resource record; descriptions prove these are HP, SP, and FS respectively (for example item 101 stores HP 30, and item 107 stores 999/99/99 for full recovery). | | `0xa5301` | item_resource_recovery_amounts | high | investigation | Sparse ITINIT row-major consumable table. USEITEM applies columns 0, 1, and 2 to the matching three-column unit resource record; descriptions prove these are HP, SP, and FS respectively (for example item 101 stores HP 30, and item 107 stores 999/99/99 for full recovery). Columns: 0=hp, 1=sp, 2=fs. |
| `0xa62a1` | item_essence_recovery_amount | high | investigation | Sparse ITINIT consumable field. Item 106, Blood Price Healing Hand, describes essence recovery 50 and stores 50 here; USEITEM follows the dedicated essence-recovery path and scales the value before updating the selected unit. | | `0xa62a1` | item_essence_recovery_amount | high | investigation | Sparse ITINIT consumable field. Item 106, Blood Price Healing Hand, describes essence recovery 50 and stores 50 here; USEITEM follows the dedicated essence-recovery path and scales the value before updating the selected unit. |
| `0xa6689` | item_weapon_class | high | investigation | Populated for 104 weapons/innate attacks. Stable values identify weapon families (1 unarmed, 2 staff, 3 claw, 4 dagger, 5 sword, 6 chain blade, 7 spear, 8 axe, 9 bow, 11 blade boots; later values are monster/natural attack classes). CALCDMG consumes it. | | `0xa6689` | item_weapon_class | high | investigation | Populated for 104 weapons/innate attacks. Stable values identify weapon families (1 unarmed, 2 staff, 3 claw, 4 dagger, 5 sword, 6 chain blade, 7 spear, 8 axe, 9 bow, 11 blade boots; later values are monster/natural attack classes). CALCDMG consumes it. |
| `0xa6a71` | item_handler_script_id | high | investigation | ITINIT field for all 287 items. Item menus look up this value and feed it directly to call-script; the packed id 0x319b resolves to ITMES.BIN, the shared per-item behavior/description dispatcher. | | `0xa6a71` | item_handler_script_id | high | investigation | ITINIT field for all 287 items. Item menus look up this value and feed it directly to call-script; the packed id 0x319b resolves to ITMES.BIN, the shared per-item behavior/description dispatcher. |
| `0xa6e5a` | skill_sort_key | high | investigation | SKINIT field populated for 129 of 131 skills. ADDEXP, ADDSKILL, CHMENU, INFOIT, and SETCH combine it with skill_category to construct deterministic skill-list ordering keys; higher-level sort operations consume the resulting indices. | | `0xa6e5a` | skill_sort_key | high | investigation | SKINIT field populated for 129 of 131 skills. ADDEXP, ADDSKILL, CHMENU, INFOIT, and SETCH combine it with skill_category to construct deterministic skill-list ordering keys; higher-level sort operations consume the resulting indices. |
| `0xa6f86` | skill_category | high | investigation | SKINIT category for all 131 skills. The records establish 1 movement/exploration, 2 defense/special attack, 3 utility/passive, 4 combat passive, 5 physical special, 6 offensive magic, and 7 healing magic. Combat and menu scripts dispatch directly on these values. | | `0xa6f86` | skill_category | high | investigation | SKINIT category for all 131 skills. The records establish 1 movement/exploration, 2 defense/special attack, 3 utility/passive, 4 combat passive, 5 physical special, 6 offensive magic, and 7 healing magic. Combat and menu scripts dispatch directly on these values. |
| `0xa70b2` | skill_change_catalog_eligible | high | investigation | SKINIT inclusion flag for CHMENU's fourth (skill-change) catalog. CHMENU scans all 300 ids, keeps only nonzero rows, sorts acquired rows ahead of unavailable rows using skill_category and skill_sort_key, and copies the acquired prefix into catalog row 3. Eligible ids are 1, 2, 112, 130..133, 210, 228, 230, and 231. |
| `0xa71de` | skill_min_range_encoded | high | investigation | Populated for the 94 active skills. CALCSCOPE and CALCREVISE subtract one before using it as the lower range bound; SELACT and DRAWTIP use the same encoded bound. | | `0xa71de` | skill_min_range_encoded | high | investigation | Populated for the 94 active skills. CALCSCOPE and CALCREVISE subtract one before using it as the lower range bound; SELACT and DRAWTIP use the same encoded bound. |
| `0xa730a` | skill_max_range_encoded | high | investigation | Populated for the same 94 active skills as skill_min_range_encoded. CALCSCOPE subtracts one to obtain the upper bound, exactly matching descriptions such as range 3 -> stored 4 and range 9 -> stored 10. | | `0xa730a` | skill_max_range_encoded | high | investigation | Populated for the same 94 active skills as skill_min_range_encoded. CALCSCOPE subtracts one to obtain the upper bound, exactly matching descriptions such as range 3 -> stored 4 and range 9 -> stored 10. |
| `0xa7436` | skill_attack_element | high | investigation | Populated for 60 elemental attacks and spells. Values match the Japanese descriptions and the same eight-value attack-element enum used by item_attack_element; CALCSCOPE, CALCBTPARAM, and SELACT consume it. | | `0xa7436` | skill_attack_element | high | investigation | Populated for 60 elemental attacks and spells. Values match the Japanese descriptions and the same eight-value attack-element enum used by item_attack_element; CALCSCOPE, CALCBTPARAM, and SELACT consume it. |
| `0xa7562` | skill_status_levels | high | investigation | Sparse SKINIT row-major condition table consumed by CALCILL when applying a skill's effects to the target unit. Skill names and descriptions identify columns 1 instant death, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear; the column layout matches item_status_delta_levels. | | `0xa7562` | skill_status_levels | high | investigation | Sparse SKINIT row-major condition table consumed by CALCILL when applying a skill's effects to the target unit. Skill names and descriptions identify columns 1 instant death, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear; the column layout matches item_status_delta_levels. Columns: 1=instant_death, 6=charm, 7=confusion, 8=paralysis, 9=poison, 10=water_flow, 11=fear. |
| `0xa988a` | skill_icon_id | high | investigation | SKINIT icon id for all 131 skills. CHMENU and INFOIT translate it into the shared icon atlas; the six ids group movement, defense, utility, passive, physical-special, and magic icons. | | `0xa988a` | skill_icon_id | high | investigation | SKINIT icon id for all 131 skills. CHMENU and INFOIT translate it into the shared icon atlas; the six ids group movement, defense, utility, passive, physical-special, and magic icons. |
| `0xa99b6` | skill_combat_stat_deltas | high | investigation | Sparse SKINIT row-major table applied by CALCBTPARAM for the selected action. Descriptions and combat consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 8 critical chance, and 9 capture power. Column 7 is unpopulated in shipped SKINIT; CALCBTPARAM adds any luck and critical modifiers into the critical percentage that CALCDMG rolls after the hit check. | | `0xa99b6` | skill_combat_stat_deltas | high | investigation | Sparse SKINIT row-major table applied by CALCBTPARAM for the selected action. Descriptions and combat consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 8 critical chance, and 9 capture power. Column 7 is unpopulated in shipped SKINIT; CALCBTPARAM adds any luck and critical modifiers into the critical percentage that CALCDMG rolls after the hit check. Columns: 0=accuracy, 1=evasion, 2=physical_attack, 3=physical_defense, 4=magic_attack, 5=magic_defense, 6=speed, 7=luck, 8=critical_chance, 9=capture_power. |
| `0xaa56e` | skill_resource_deltas | high | investigation | Sparse SKINIT resource table used throughout skill selection and resolution. Column 0 is HP recovery for the three healing spells; column 1 is the negative SP cost for all 95 active skills, exactly matching each description. Column 2 is unpopulated in the shipped table. | | `0xaa56e` | skill_resource_deltas | high | investigation | Sparse SKINIT resource table used throughout skill selection and resolution. Column 0 is HP recovery for the three healing spells; column 1 is the negative SP cost for all 95 active skills, exactly matching each description. Column 2 is unpopulated in the shipped table. Columns: 0=hp_recovery, 1=sp_cost. |
| `0xaa8f2` | skill_proc_chance_percent | high | investigation | Probability for 14 passive skills. CALCDMG compares random-modulo 100 against this value; examples include Re-action 20, Double Action 100, Counter 10, and Resurrection 50. | | `0xaa8f2` | skill_proc_chance_percent | high | investigation | Probability for 14 passive skills. CALCDMG compares random-modulo 100 against this value; examples include Re-action 20, Double Action 100, Counter 10, and Resurrection 50. |
| `0xaaa1e` | skill_battle_animation_id | high | investigation | Populated for 101 combat skills. BTL and CALCDMG place this value in the battle-animation selector before calling BTANINIT; most skills reuse their own id, while related skills deliberately share an animation and passive reactions use ids 801..808. | | `0xaaa1e` | skill_battle_animation_id | high | investigation | Populated for 101 combat skills. BTL and CALCDMG place this value in the battle-animation selector before calling BTANINIT; most skills reuse their own id, while related skills deliberately share an animation and passive reactions use ids 801..808. |
| `0xaab4a` | skill_handler_script_id | high | investigation | SKINIT field for all 131 skills. CHMENU and INFOIT look it up and pass it directly to call-script; packed id 0x31ca resolves to SKMES.BIN, the shared per-skill text/behavior dispatcher. | | `0xaab4a` | skill_handler_script_id | high | investigation | SKINIT field for all 131 skills. CHMENU and INFOIT look it up and pass it directly to call-script; packed id 0x31ca resolves to SKMES.BIN, the shared per-skill text/behavior dispatcher. |
@@ -135,7 +138,6 @@
| `0x4379` | — | low | auto-shape | array | | `0x4379` | — | low | auto-shape | array |
| `0x45d7` | — | low | auto-shape | array | | `0x45d7` | — | low | auto-shape | array |
| `0x463b` | — | low | auto-shape | array | | `0x463b` | — | low | auto-shape | array |
| `0x65ce` | — | low | auto-shape | array |
| `0x671c` | — | low | auto-shape | array | | `0x671c` | — | low | auto-shape | array |
| `0x671f` | — | low | auto-shape | array | | `0x671f` | — | low | auto-shape | array |
| `0x6727` | — | low | auto-shape | array | | `0x6727` | — | low | auto-shape | array |
@@ -166,7 +168,6 @@
| `0x53ede` | — | low | auto-shape | array | | `0x53ede` | — | low | auto-shape | array |
| `0x55e3b` | — | low | auto-shape | array | | `0x55e3b` | — | low | auto-shape | array |
| `0x56223` | — | low | auto-shape | array | | `0x56223` | — | low | auto-shape | array |
| `0x5660b` | — | low | auto-shape | array |
| `0x56b20` | — | low | auto-shape | array | | `0x56b20` | — | low | auto-shape | array |
| `0x56b52` | — | low | auto-shape | array | | `0x56b52` | — | low | auto-shape | array |
| `0x56b85` | — | low | auto-shape | array | | `0x56b85` | — | low | auto-shape | array |
@@ -4987,7 +4988,6 @@
| `0x7843e` | — | med | auto-shape | unit-field | | `0x7843e` | — | med | auto-shape | unit-field |
| `0x81c96` | — | med | auto-shape | record-table[stride 3] | | `0x81c96` | — | med | auto-shape | record-table[stride 3] |
| `0x8284e` | — | med | auto-shape | record-table[stride 3] | | `0x8284e` | — | med | auto-shape | record-table[stride 3] |
| `0xa70b2` | — | low | auto-shape | skill-field? |
| `0xaac76` | — | low | auto-shape | index/counter? | | `0xaac76` | — | low | auto-shape | index/counter? |
| `0xaacf0` | — | med | auto-shape | record-table[stride 5] | | `0xaacf0` | — | med | auto-shape | record-table[stride 5] |
| `0xaad86` | — | med | auto-shape | record-table[stride 55] | | `0xaad86` | — | med | auto-shape | record-table[stride 55] |

View File

@@ -215,6 +215,20 @@ 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 for both `item_stat_modifiers` and `skill_combat_stat_deltas`; the skill descriptions and matching item
columns also confirm evasion, magic defense, and speed. 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.
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
as provenance. Generated profiles therefore render names such as
`item_stat_modifiers.critical_chance` and `skill_status_levels.paralysis` directly.
### The curated registry — `vm-map/globals.toml` (2026-07-07) ### The curated registry — `vm-map/globals.toml` (2026-07-07)
The v1 auto map (`build/global-var-map.json`) infers *shapes* but cannot recover branch-flag The v1 auto map (`build/global-var-map.json`) infers *shapes* but cannot recover branch-flag
@@ -223,7 +237,7 @@ The v1 auto map (`build/global-var-map.json`) infers *shapes* but cannot recover
- **`vm-map/globals.toml`** — the only hand-edited source. One `[[global]]` per known address: - **`vm-map/globals.toml`** — the only hand-edited source. One `[[global]]` per known address:
`name`, `category` (`story-flag`/`index-pointer`/`data-table`/`string-table`/`ui-toggle`/ `name`, `category` (`story-flag`/`index-pointer`/`data-table`/`string-table`/`ui-toggle`/
`choice-output`/`counter`/`unknown`), `type`, `value_domain`, `usage`, and provenance `choice-output`/`counter`/`unknown`), `type`, optional row-table `columns`, `value_domain`, `usage`, and provenance
(`source`/`confidence`/`depends_on`). (`source`/`confidence`/`depends_on`).
- **`tools/globals_build.py --build`** merges curated entries *over* the auto map → - **`tools/globals_build.py --build`** merges curated entries *over* the auto map →
`build/globals.json` (machine) + `docs/global-reference.md` (generated human view). `--lint` `build/globals.json` (machine) + `docs/global-reference.md` (generated human view). `--lint`
@@ -259,14 +273,13 @@ 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: The v1 map labels *shapes and tables*; the next increments add *meaning*, cheapest first:
1. **Continue INIT semantics by evidence density.** Resolve ITINIT/SKINIT's remaining stat and condition 1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT's populated row columns now have
columns, then work through EBINIT's AI, route/evolution, and remaining sparse-flag tables by consumer machine-readable meanings; extend the same structured `columns` metadata through the confirmed EBINIT
strength. Preserve explicit item → skill and unit → attack/skill/equipment/drop joins. Do not infer tables, then investigate its unread enum and boss-class sign only when consumer evidence appears.
meaning from column position alone. Preserve explicit item → skill and unit → attack/skill/equipment/drop joins. Do not infer meaning from
2. **Fold in the `*MES` message-table writers** (`ITMES`, `SKMES`, `VIMES`, …) and any other column position alone.
`set-string`/`copy-to-global` writers not covered by the `*INIT` set — pure static win, 2. **Extend message-table joins beyond the completed ITMES/SKMES pair** (`VIMES`, other id dispatchers, …)
extends the string/data labels. (Also: most name-table bases are *read* rarely — reads and fold in other `set-string`/`copy-to-global` writers not covered by the `*INIT` set.
likely go through `*MES`/an indirection; tracing that would connect names to their readers.)
3. **Label 2D record tables by their readers** — cross-reference which scripts read each 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 `rec[sN]` table and infer purpose from context (e.g. RECOVER's 30-wide tables ↔ a
status/recovery system). Static, medium effort. status/recovery system). Static, medium effort.

View File

@@ -736,6 +736,15 @@ The dispatch keys also establish `current_item_id` and `current_skill_id` as hig
slots. This makes message/field correlation the next evidence source for the remaining sparse item and skill slots. This makes message/field correlation the next evidence source for the remaining sparse item and skill
columns. columns.
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
structured `globals.toml` metadata. Extracted JSON and profiles now resolve raw keys to names such as
`item_stat_modifiers.critical_chance` while preserving the original address key. The message audit confirms
all populated condition columns and the female-only item mask. CHMENU also resolves the last anonymous
SKINIT parallel field as the skill-change catalog inclusion flag and separates acquired-skill state from
skill-information visibility. The remaining item/skill work is refinement rather than an unnamed populated
schema.
The remaining EBINIT unknowns are now the unread `0x7843e` enum and the signed meaning within boss classes; The remaining EBINIT unknowns are now the unread `0x7843e` enum and the signed meaning within boss classes;
enemy AI appears to live outside the static EBINIT schema. STINIT's bespoke parser remains a separate enemy AI appears to live outside the static EBINIT schema. STINIT's bespoke parser remains a separate
extraction task. extraction task.

View File

@@ -43,7 +43,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited *
| `validate_opcode_table.py` | Definitive decode-coverage validator (replicates Kelebek's `data_array_end` code/data split). | `validate_opcode_table.py` | corpus → stdout | | `validate_opcode_table.py` | Definitive decode-coverage validator (replicates Kelebek's `data_array_end` code/data split). | `validate_opcode_table.py` | corpus → stdout |
| `validate_opcode_table_naive.py` | Naïve variant of the above (baseline comparison). | `validate_opcode_table_naive.py` | corpus → stdout | | `validate_opcode_table_naive.py` | Naïve variant of the above (baseline comparison). | `validate_opcode_table_naive.py` | corpus → stdout |
| `age_opcodes_himegari.py` | ⚙ Inferred Himegari opcode semantics — **generated; do not hand-edit.** | *Imported by `sys4load.py`.* | — | | `age_opcodes_himegari.py` | ⚙ Inferred Himegari opcode semantics — **generated; do not hand-edit.** | *Imported by `sys4load.py`.* | — |
| `globals_build.py` | Merge curated `globals.toml` over the auto shape map; generate the global registry + linter. | `--build` · `--lint` | `vm-map/globals.toml`, `build/global-var-map.json` → ⚙ `build/globals.json`, ⚙ `docs/global-reference.md` | | `globals_build.py` | Merge curated `globals.toml` over the auto shape map, preserve optional machine-readable row-table `columns`, and generate the global registry + linter. | `--build` · `--lint` | `vm-map/globals.toml`, `build/global-var-map.json` → ⚙ `build/globals.json`, ⚙ `docs/global-reference.md` |
| `story_flags.py` | Static story-flag miner (branch-condition mining) + `--bootstrap` skeleton seeding. | `story_flags.py` · `--bootstrap` | corpus, `build/global-var-map.json` → ⚙ `build/story-flags-candidates.json`, appends `vm-map/globals.toml` | | `story_flags.py` | Static story-flag miner (branch-condition mining) + `--bootstrap` skeleton seeding. | `story_flags.py` · `--bootstrap` | corpus, `build/global-var-map.json` → ⚙ `build/story-flags-candidates.json`, appends `vm-map/globals.toml` |
| `test_globals.py` | Unit tests for the globals registry + story-flag miner. | `test_globals.py` | — | | `test_globals.py` | Unit tests for the globals registry + story-flag miner. | `test_globals.py` | — |
| `scjump_decode.py` | Decode SCJUMP's progression logic → decision table; `--verify` VM cross-check. | `scjump_decode.py` · `--verify` | SCJUMP.BIN, `build/globals.json` → ⚙ `build/scjump-decisions.{json,md}` | | `scjump_decode.py` | Decode SCJUMP's progression logic → decision table; `--verify` VM cross-check. | `scjump_decode.py` · `--verify` | SCJUMP.BIN, `build/globals.json` → ⚙ `build/scjump-decisions.{json,md}` |
@@ -55,8 +55,8 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited *
|---|---|---|---| |---|---|---|---|
| `extract_phase2.py` | Batch: disassembly + text corpora for every script. | `extract_phase2.py` | corpus → `build/disasm/*.asm`, `build/text/{dialogue.jsonl,strings.jsonl,*.strings.txt}`, `build/manifest.json` | | `extract_phase2.py` | Batch: disassembly + text corpora for every script. | `extract_phase2.py` | corpus → `build/disasm/*.asm`, `build/text/{dialogue.jsonl,strings.jsonl,*.strings.txt}`, `build/manifest.json` |
| `extract_message_table.py` | Discover a repeated global-id dispatch chain such as ITMES/SKMES, reconstruct player-facing title/description lines (including furigana surface text and readings), and emit an ID-keyed message table with bytecode provenance. | `extract_message_table.py <MES> [OUTNAME]` | `<MES>.BIN``build/data/<OUTNAME>.json` | | `extract_message_table.py` | Discover a repeated global-id dispatch chain such as ITMES/SKMES, reconstruct player-facing title/description lines (including furigana surface text and readings), and emit an ID-keyed message table with bytecode provenance. | `extract_message_table.py <MES> [OUTNAME]` | `<MES>.BIN``build/data/<OUTNAME>.json` |
| `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer 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`. ITINIT and SKINIT also join their matching ITMES/SKMES player-facing messages by runtime id. Refreshes the generated data index. | `extract_init.py <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` plus matching `<MES>.BIN` when supported → `build/data/<OUTNAME>.json`, `build/data/README.md` | | `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer 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`. ITINIT and SKINIT join ITMES/SKMES messages; top-level `field_semantics` maps raw keys to canonical global/column names. Refreshes the generated data index. | `extract_init.py <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` plus matching `<MES>.BIN` when supported + `build/globals.json``build/data/<OUTNAME>.json`, `build/data/README.md` |
| `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric table: message coverage, per-array and per-record-column population/value distributions, representative records, and direct opcode/script consumers. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py <TABLE> [--build] [--limit N]` | `build/data/<TABLE>.json` + corpus → stdout; with `--build`, `build/data/<TABLE>-field-profile.{json,md}` | | `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric table: message coverage, per-array and per-record-column population/value distributions, representative records with their player-facing descriptions, and direct opcode/script consumers. `--message-query REGEX` shows every matching name/message beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py <TABLE> [--build] [--limit N] [--message-query REGEX]` | `build/data/<TABLE>.json` + corpus → stdout; with `--build`, `build/data/<TABLE>-field-profile.{json,md}` |
| `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based INIT extraction, MES dispatch reconstruction/joins, and field/message profiling. | run each directly | — | | `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based INIT extraction, MES dispatch reconstruction/joins, and field/message profiling. | run each directly | — |
| `global_map.py` | Build the partial global-variable name map from static evidence. | `global_map.py` | corpus + `build/data/``build/global-var-map.{json,md}` | | `global_map.py` | Build the partial global-variable name map from static evidence. | `global_map.py` | corpus + `build/data/``build/global-var-map.{json,md}` |

View File

@@ -280,6 +280,40 @@ def join_messages(records: list[dict], message_scr) -> dict:
} }
@cache
def _global_registry() -> dict:
path = paths.BUILD / "globals.json"
try:
return json.loads(path.read_text(encoding="utf8")).get("globals", {})
except (OSError, json.JSONDecodeError):
return {}
def field_semantics(records: list[dict]) -> dict[str, str]:
"""Map raw extracted field keys to canonical semantic names when available."""
keys = {
key
for record in records
for key in (*record.get("fields", {}), *record.get("record_fields", {}))
}
registry = _global_registry()
semantics = {}
for key in sorted(keys, key=lambda value: tuple(
int(part, 0) for part in value.split("/")
)):
parts = key.split("/")
entry = registry.get(f"0x{int(parts[0], 16):x}", {})
name = entry.get("name")
if not name:
continue
if len(parts) == 3:
column = parts[2]
column_name = entry.get("columns", {}).get(column, f"column_{column}")
name = f"{name}.{column_name}"
semantics[key] = name
return semantics
def write_data_index(data_dir: Path) -> None: def write_data_index(data_dir: Path) -> None:
"""Regenerate the disposable build/data index from current table JSONs.""" """Regenerate the disposable build/data index from current table JSONs."""
tables = [] tables = []
@@ -323,6 +357,8 @@ def write_data_index(data_dir: Path) -> None:
"Where a matching `*MES` dispatcher exists, `message` preserves its player-facing", "Where a matching `*MES` dispatcher exists, `message` preserves its player-facing",
"title, description, furigana, and bytecode dispatch offset separately from the", "title, description, furigana, and bytecode dispatch offset separately from the",
"short description stored by the INIT script.", "short description stored by the INIT script.",
"Top-level `field_semantics` maps raw array/row-column keys to canonical machine-readable",
"names from `vm-map/globals.toml`; raw keys remain intact as bytecode provenance.",
"", "",
"Use `tools/init_table_profile.py <TABLE> --build` to generate value/population and", "Use `tools/init_table_profile.py <TABLE> --build` to generate value/population and",
"direct-consumer evidence. `STINIT` still requires a bespoke mixed numeric/string parser.", "direct-consumer evidence. `STINIT` still requires a bespoke mixed numeric/string parser.",
@@ -350,9 +386,11 @@ def main() -> int:
) )
cols = sorted({c for r in recs for c in r.get("fields", {})}, key=lambda h: int(h, 16)) cols = sorted({c for r in recs for c in r.get("fields", {})}, key=lambda h: int(h, 16))
semantics = field_semantics(recs)
out = {"table": name, "source": scr.path.name, "magic": scr.magic, "mode": mode, out = {"table": name, "source": scr.path.name, "magic": scr.magic, "mode": mode,
"record_count": len(recs), **meta, "record_count": len(recs), **meta,
"field_columns": cols if mode != "footer" else None, "records": recs} "field_columns": cols if mode != "footer" else None,
"field_semantics": semantics, "records": recs}
outpath = paths.BUILD / "data" / f"{outname}.json" outpath = paths.BUILD / "data" / f"{outname}.json"
outpath.parent.mkdir(parents=True, exist_ok=True) outpath.parent.mkdir(parents=True, exist_ok=True)
outpath.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8") outpath.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8")

View File

@@ -49,6 +49,20 @@ def lint(entries: dict[int, dict], all_addrs: set[int]) -> tuple[list[str], list
errors.append(f"{tag}: bad confidence {conf!r}") errors.append(f"{tag}: bad confidence {conf!r}")
if src == "auto-shape" and conf == "high": if src == "auto-shape" and conf == "high":
errors.append(f"{tag}: auto-shape source may not claim high confidence") errors.append(f"{tag}: auto-shape source may not claim high confidence")
columns = e.get("columns", {})
if not isinstance(columns, dict):
errors.append(f"{tag}: columns must be a table")
else:
for column, name in columns.items():
try:
column_index = int(column)
except (TypeError, ValueError):
errors.append(f"{tag}: bad column index {column!r}")
continue
if column_index < 0:
errors.append(f"{tag}: negative column index {column_index}")
if not isinstance(name, str) or not name:
errors.append(f"{tag}: column {column_index} has no semantic name")
for dep in e.get("depends_on", []): for dep in e.get("depends_on", []):
if _parse_addr(dep) not in all_addrs: if _parse_addr(dep) not in all_addrs:
errors.append(f"{tag}: depends_on missing address {dep}") errors.append(f"{tag}: depends_on missing address {dep}")
@@ -96,6 +110,10 @@ def merge(curated: dict[int, dict], auto: dict) -> dict[int, dict]:
"source": e.get("source", "inference"), "confidence": e.get("confidence", "low"), "source": e.get("source", "inference"), "confidence": e.get("confidence", "low"),
"depends_on": [f"0x{_parse_addr(d):x}" for d in e.get("depends_on", [])], "depends_on": [f"0x{_parse_addr(d):x}" for d in e.get("depends_on", [])],
"provenance": "curated"} "provenance": "curated"}
if e.get("columns"):
out[addr]["columns"] = {
str(column): name for column, name in e["columns"].items()
}
return out return out
@@ -121,6 +139,14 @@ def emit_reference_md(merged: dict[int, dict]) -> str:
e = merged[addr] e = merged[addr]
name = e["name"] or "" name = e["name"] or ""
usage = (e["usage"] or "").replace("|", "\\|").replace("\n", " ") usage = (e["usage"] or "").replace("|", "\\|").replace("\n", " ")
if columns := e.get("columns"):
mapping = ", ".join(
f"{column}={column_name}"
for column, column_name in sorted(
columns.items(), key=lambda item: int(item[0])
)
)
usage += f" Columns: {mapping}."
L.append(f"| `{e['address']}` | {name} | {e['confidence']} | {e['source']} | {usage} |") L.append(f"| `{e['address']}` | {name} | {e['confidence']} | {e['source']} | {usage} |")
L.append("") L.append("")
return "\n".join(L) + "\n" return "\n".join(L) + "\n"

View File

@@ -19,6 +19,7 @@ from __future__ import annotations
import argparse import argparse
import collections import collections
import json import json
import re
import sys import sys
from pathlib import Path from pathlib import Path
@@ -49,6 +50,7 @@ def value_key(value) -> str:
def profile_columns(data: dict) -> list[dict]: def profile_columns(data: dict) -> list[dict]:
records = data["records"] records = data["records"]
field_semantics = data.get("field_semantics", {})
values: dict[str, list] = collections.defaultdict(list) values: dict[str, list] = collections.defaultdict(list)
examples: dict[str, list[dict]] = collections.defaultdict(list) examples: dict[str, list[dict]] = collections.defaultdict(list)
identities: dict[str, dict] = {} identities: dict[str, dict] = {}
@@ -59,14 +61,18 @@ def profile_columns(data: dict) -> list[dict]:
identities[key] = { identities[key] = {
"key": key, "kind": "parallel-array", "base": key, "key": key, "kind": "parallel-array", "base": key,
"stride": None, "column": None, "stride": None, "column": None,
"semantic_name": field_semantics.get(key),
} }
values[key].append(value) values[key].append(value)
if len(examples[key]) < 5: if len(examples[key]) < 5:
examples[key].append({ example = {
"id": record["id"], "id": record["id"],
"name": record.get("name", ""), "name": record.get("name", ""),
"value": value, "value": value,
}) }
if message := record.get("message"):
example["message_description"] = message.get("description", "")
examples[key].append(example)
for key, value in record.get("record_fields", {}).items(): for key, value in record.get("record_fields", {}).items():
base_text, stride_text, column_text = key.split("/") base_text, stride_text, column_text = key.split("/")
base = int(base_text, 16) base = int(base_text, 16)
@@ -79,14 +85,18 @@ def profile_columns(data: dict) -> list[dict]:
"base": f"0x{base:x}", "base": f"0x{base:x}",
"stride": stride, "stride": stride,
"column": column, "column": column,
"semantic_name": field_semantics.get(normalized_key),
} }
values[normalized_key].append(value) values[normalized_key].append(value)
if len(examples[normalized_key]) < 5: if len(examples[normalized_key]) < 5:
examples[normalized_key].append({ example = {
"id": record["id"], "id": record["id"],
"name": record.get("name", ""), "name": record.get("name", ""),
"value": value, "value": value,
}) }
if message := record.get("message"):
example["message_description"] = message.get("description", "")
examples[normalized_key].append(example)
rows = [] rows = []
for key, vals in values.items(): for key, vals in values.items():
@@ -131,6 +141,48 @@ def profile_messages(data: dict) -> dict:
} }
def find_message_matches(data: dict, pattern: str) -> list[dict]:
"""Return records whose name/title/description matches a regular expression."""
regex = re.compile(pattern, re.IGNORECASE)
return [
record
for record in data["records"]
if regex.search("\n".join([
record.get("name", ""),
record.get("message", {}).get("title", ""),
record.get("message", {}).get("description", ""),
]))
]
def render_message_matches(data: dict, pattern: str) -> str:
"""Render message hits beside every populated INIT field for correlation."""
matches = find_message_matches(data, pattern)
escaped_pattern = pattern.replace("`", "\\`")
lines = [
f"# {data['table']} message matches",
"",
f"- query: `{escaped_pattern}`",
f"- matches: {len(matches)}",
"",
"| id | name | player-facing description | populated fields |",
"|---:|---|---|---|",
]
for record in matches:
fields = {**record.get("fields", {}), **record.get("record_fields", {})}
rendered_fields = ", ".join(
f"`{data.get('field_semantics', {}).get(key, key)}` (`{key}`)={value}"
for key, value in sorted(fields.items())
)
name = record.get("name", "").replace("|", "\\|")
description = record.get("message", {}).get("description", "").replace("|", "\\|")
lines.append(
f"| {record['id']} | {name} | {description} | {rendered_fields} |"
)
lines.append("")
return "\n".join(lines)
def add_direct_references(rows: list[dict], source_name: str) -> None: def add_direct_references(rows: list[dict], source_name: str) -> None:
by_base: dict[int, list[dict]] = collections.defaultdict(list) by_base: dict[int, list[dict]] = collections.defaultdict(list)
for row in rows: for row in rows:
@@ -185,8 +237,8 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str:
f"- messages with furigana spans: {message_profile['furigana_records']}", f"- messages with furigana spans: {message_profile['furigana_records']}",
f"- rows shown: {len(shown)} (ranked by record coverage, then consumer references)", f"- rows shown: {len(shown)} (ranked by record coverage, then consumer references)",
"", "",
"| field | populated | distinct | range | direct refs | readers | common values | examples |", "| field | meaning | populated | distinct | range | direct refs | readers | common values | examples |",
"|---|---:|---:|---|---:|---|---|---|", "|---|---|---:|---:|---|---:|---|---|---|",
] ]
for row in shown: for row in shown:
value_range = "" if row["min"] is None else f"{row['min']}..{row['max']}" value_range = "" if row["min"] is None else f"{row['min']}..{row['max']}"
@@ -198,7 +250,8 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str:
for entry in row["examples"][:3] for entry in row["examples"][:3]
).replace("|", "\\|") ).replace("|", "\\|")
lines.append( lines.append(
f"| `{row['key']}` | {row['population']}/{data['record_count']} " f"| `{row['key']}` | {row.get('semantic_name') or ''} | "
f"{row['population']}/{data['record_count']} "
f"({row['coverage']:.0%}) | {row['distinct_values']} | {value_range} | " f"({row['coverage']:.0%}) | {row['distinct_values']} | {value_range} | "
f"{row['references']} | {readers} | {common} | {examples} |" f"{row['references']} | {readers} | {common} | {examples} |"
) )
@@ -211,6 +264,11 @@ def main() -> int:
parser.add_argument("table", help="extracted table name, e.g. ITINIT") parser.add_argument("table", help="extracted table name, e.g. ITINIT")
parser.add_argument("--build", action="store_true", help="write JSON and Markdown profiles") parser.add_argument("--build", action="store_true", help="write JSON and Markdown profiles")
parser.add_argument("--limit", type=int, default=40, help="Markdown/console row limit") parser.add_argument("--limit", type=int, default=40, help="Markdown/console row limit")
parser.add_argument(
"--message-query",
metavar="REGEX",
help="show matching names/player-facing messages beside all populated fields",
)
args = parser.parse_args() args = parser.parse_args()
name = args.table.upper().removesuffix(".JSON").removesuffix(".BIN") name = args.table.upper().removesuffix(".JSON").removesuffix(".BIN")
@@ -231,6 +289,8 @@ def main() -> int:
)), )),
} }
markdown = render_markdown(data, rows, args.limit) markdown = render_markdown(data, rows, args.limit)
if args.message_query:
print(render_message_matches(data, args.message_query))
print(markdown) print(markdown)
if args.build: if args.build:
stem = paths.BUILD / "data" / f"{name}-field-profile" stem = paths.BUILD / "data" / f"{name}-field-profile"

View File

@@ -130,11 +130,22 @@ def test_message_join() -> None:
"INIT/MES join uses the shared runtime id") "INIT/MES join uses the shared runtime id")
def test_field_semantics() -> None:
scripts = paths.scripts()
items, _ = extract_init.extract_name(sys4load.load(scripts["ITINIT.BIN"]))
semantics = extract_init.field_semantics(items)
check(semantics["0x8c879"] == "item_sort_key",
"parallel INIT fields expose canonical semantic names")
check(semantics["0x9f541/14/8"] == "item_stat_modifiers.critical_chance",
"row-table columns expose canonical semantic names")
if __name__ == "__main__": if __name__ == "__main__":
test_real_name_tables() test_real_name_tables()
test_static_negative_write() test_static_negative_write()
test_real_message_tables() test_real_message_tables()
test_message_join() test_message_join()
test_field_semantics()
if FAILS: if FAILS:
raise SystemExit(f"{len(FAILS)} failed checks") raise SystemExit(f"{len(FAILS)} failed checks")
print("all extract_init checks passed") print("all extract_init checks passed")

View File

@@ -22,10 +22,13 @@ def test_load_and_lint():
def test_lint_catches_bad_vocab(): def test_lint_catches_bad_vocab():
bad = {0x1: {"_addr": 0x1, "name": "x", "category": "bogus", bad = {0x1: {"_addr": 0x1, "name": "x", "category": "bogus",
"source": "auto-shape", "confidence": "high"}} "source": "auto-shape", "confidence": "high",
"columns": {"not-an-index": "x", "-1": ""}}}
errors, _ = G.lint(bad, {0x1}) errors, _ = G.lint(bad, {0x1})
check(any("category" in e for e in errors), "lint flags bad category") check(any("category" in e for e in errors), "lint flags bad category")
check(any("confidence" in e for e in errors), "lint flags auto-shape claiming high confidence") check(any("confidence" in e for e in errors), "lint flags auto-shape claiming high confidence")
check(any("column index" in e for e in errors), "lint flags nonnumeric column indices")
check(any("negative column" in e for e in errors), "lint flags negative column indices")
def test_merge_precedence(): def test_merge_precedence():
curated, _ = G.load_toml(paths.VM_MAP / "globals.toml") curated, _ = G.load_toml(paths.VM_MAP / "globals.toml")
@@ -34,6 +37,8 @@ def test_merge_precedence():
check(merged[0xa57]["name"] == "lily_form_a", "curated 0xa57 name wins over auto label") check(merged[0xa57]["name"] == "lily_form_a", "curated 0xa57 name wins over auto label")
check(merged[0xa57]["category"] == "story-flag", "curated 0xa57 category overrides auto string-table") check(merged[0xa57]["category"] == "story-flag", "curated 0xa57 category overrides auto string-table")
check(merged[0xa57]["provenance"] == "curated", "0xa57 marked curated") check(merged[0xa57]["provenance"] == "curated", "0xa57 marked curated")
check(merged[0x9f541]["columns"]["8"] == "critical_chance",
"curated row-table column semantics survive the merge")
# an address only in the auto map falls through as provenance=auto # an address only in the auto map falls through as provenance=auto
auto_only = next((a for a in auto.get("globals", {}) auto_only = next((a for a in auto.get("globals", {})
if int(a, 16) not in curated and auto["globals"][a].get("label")), None) if int(a, 16) not in curated and auto["globals"][a].get("label")), None)

View File

@@ -11,6 +11,11 @@ import init_table_profile as profile
def main() -> int: def main() -> int:
fixture = { fixture = {
"table": "TEST",
"field_semantics": {
"0x10": "test_parallel",
"0x30/3/0": "test_record.zero",
},
"records": [ "records": [
{"id": 1, "name": "one", "fields": {"0x10": 2, "0x20": 0}, {"id": 1, "name": "one", "fields": {"0x10": 2, "0x20": 0},
"record_fields": {"0x30/3/0": 9}, "record_fields": {"0x30/3/0": 9},
@@ -27,17 +32,25 @@ def main() -> int:
assert rows["0x10"]["distinct_values"] == 2 assert rows["0x10"]["distinct_values"] == 2
assert rows["0x10"]["min"] == 2 and rows["0x10"]["max"] == 5 assert rows["0x10"]["min"] == 2 and rows["0x10"]["max"] == 5
assert rows["0x10"]["common"][0] == {"value": "2", "count": 2} assert rows["0x10"]["common"][0] == {"value": "2", "count": 2}
assert rows["0x10"]["semantic_name"] == "test_parallel"
assert rows["0x10"]["examples"][0]["message_description"] == "First"
assert rows["0x20"]["population"] == 1 assert rows["0x20"]["population"] == 1
assert rows["0x20"]["examples"][0]["name"] == "one" assert rows["0x20"]["examples"][0]["name"] == "one"
assert rows["0x30/3/0"]["kind"] == "record-column" assert rows["0x30/3/0"]["kind"] == "record-column"
assert rows["0x30/3/0"]["base"] == "0x30" assert rows["0x30/3/0"]["base"] == "0x30"
assert rows["0x30/3/0"]["stride"] == 3 assert rows["0x30/3/0"]["stride"] == 3
assert rows["0x30/3/0"]["semantic_name"] == "test_record.zero"
assert rows["0x30/3/2"]["column"] == 2 assert rows["0x30/3/2"]["column"] == 2
messages = profile.profile_messages(fixture) messages = profile.profile_messages(fixture)
assert messages["population"] == 1 assert messages["population"] == 1
assert messages["coverage"] == 1 / 3 assert messages["coverage"] == 1 / 3
assert messages["furigana_records"] == 1 assert messages["furigana_records"] == 1
assert messages["examples"][0]["description"] == "First" assert messages["examples"][0]["description"] == "First"
matches = profile.find_message_matches(fixture, "first|three")
assert [record["id"] for record in matches] == [1, 3]
rendered = profile.render_message_matches(fixture, "First")
assert "| 1 | one | First |" in rendered
assert "`test_record.zero` (`0x30/3/0`)=9" in rendered
print("all init_table_profile checks passed") print("all init_table_profile checks passed")
return 0 return 0

View File

@@ -131,6 +131,7 @@ address = "0x8e7b9"
name = "item_character_whitelist" name = "item_character_whitelist"
category = "data-table" category = "data-table"
type = "int[1000][5]" type = "int[1000][5]"
columns = { "0" = "allowed_character_id_1", "1" = "allowed_character_id_2", "2" = "allowed_character_id_3", "3" = "allowed_character_id_4", "4" = "allowed_character_id_5" }
value_domain = "EBINIT character ids; up to five per item" value_domain = "EBINIT character ids; up to five per item"
usage = "Sparse ITINIT row-major table with stride 5. CHMENU rejects an item when column 0 is populated and the selected party slot's character id is absent from the row. Unique accessories 471/472 allow one character each, while crossover accessories 480..485 allow character ids 2, 3, and 4. Unused trailing columns remain zero." usage = "Sparse ITINIT row-major table with stride 5. CHMENU rejects an item when column 0 is populated and the selected party slot's character id is absent from the row. Unique accessories 471/472 allow one character each, while crossover accessories 480..485 allow character ids 2, 3, and 4. Unused trailing columns remain zero."
source = "investigation" source = "investigation"
@@ -142,15 +143,16 @@ name = "item_sex_restriction_mask"
category = "data-table" category = "data-table"
type = "int[1000]" type = "int[1000]"
value_domain = "bit mask; shipped ITINIT populates value 4 on two items" value_domain = "bit mask; shipped ITINIT populates value 4 on two items"
usage = "Sparse ITINIT equipment restriction. CHMENU tests this mask against EBINIT field 0x71eae, whose values partition male, female, and sexless units; both populated items carry bit 2 and are therefore female-only." usage = "Sparse ITINIT equipment restriction. CHMENU uses unit_sex_category as a bit index and rejects an item when that bit is absent from this mask. Both populated items carry only bit 2; EBINIT value 2 is female, and ITMES explicitly describes item 419 as female-only."
source = "investigation" source = "investigation"
confidence = "med" confidence = "high"
[[global]] [[global]]
address = "0x906f9" address = "0x906f9"
name = "item_status_delta_levels" name = "item_status_delta_levels"
category = "data-table" category = "data-table"
type = "int[1000][30]" type = "int[1000][30]"
columns = { "2" = "hp_drain", "3" = "sp_drain", "4" = "fs_drain", "5" = "curse", "6" = "charm", "7" = "confusion", "8" = "paralysis", "9" = "poison", "10" = "water_flow", "11" = "fear" }
value_domain = "condition delta -5..5; zero means absent" value_domain = "condition delta -5..5; zero means absent"
usage = "Sparse ITINIT row-major table consumed by USEITEM and CALCILL when applying an item's effects. Positive values inflict or drain; -5 removes a condition (for example paralysis-removal item 108 stores -5 in column 8). Confirmed columns are 2 HP drain, 3 SP drain, 4 FS drain, 5 curse, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear." usage = "Sparse ITINIT row-major table consumed by USEITEM and CALCILL when applying an item's effects. Positive values inflict or drain; -5 removes a condition (for example paralysis-removal item 108 stores -5 in column 8). Confirmed columns are 2 HP drain, 3 SP drain, 4 FS drain, 5 curse, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear."
source = "investigation" source = "investigation"
@@ -161,6 +163,7 @@ address = "0x97c29"
name = "item_equipped_status_levels" name = "item_equipped_status_levels"
category = "data-table" category = "data-table"
type = "int[1000][30]" type = "int[1000][30]"
columns = { "9" = "poison", "11" = "fear", "13" = "regeneration", "14" = "exaltation" }
value_domain = "condition strength 1..5; zero means absent" value_domain = "condition strength 1..5; zero means absent"
usage = "Sparse ITINIT row-major table added to a unit's 30-column condition state by CALCREVISE. Item descriptions identify populated columns 9 poison, 11 fear, 13 regeneration, and 14 exaltation; these are passive equipped effects, distinct from item_status_delta_levels." usage = "Sparse ITINIT row-major table added to a unit's 30-column condition state by CALCREVISE. Item descriptions identify populated columns 9 poison, 11 fear, 13 regeneration, and 14 exaltation; these are passive equipped effects, distinct from item_status_delta_levels."
source = "investigation" source = "investigation"
@@ -171,6 +174,7 @@ address = "0x9f541"
name = "item_stat_modifiers" name = "item_stat_modifiers"
category = "data-table" category = "data-table"
type = "int[1000][14]" type = "int[1000][14]"
columns = { "0" = "accuracy", "1" = "evasion", "2" = "physical_attack", "3" = "physical_defense", "4" = "magic_attack", "5" = "magic_defense", "6" = "speed", "7" = "luck", "8" = "critical_chance", "9" = "capture_power", "10" = "movement", "11" = "max_hp", "12" = "max_sp", "13" = "max_fs" }
value_domain = "signed additive stat values; observed -15..200" value_domain = "signed additive stat values; observed -15..200"
usage = "ITINIT row-major equipment modifiers added directly to the unit's 14-column stat record by CALCREVISE. Descriptions and consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 8 critical chance, 9 capture power, 10 movement, 11 max HP, 12 max SP, and 13 max FS. CALCBTPARAM adds columns 7 and 8 into the action's critical percentage, and CALCDMG compares the clamped result with a random-modulo-100 roll." usage = "ITINIT row-major equipment modifiers added directly to the unit's 14-column stat record by CALCREVISE. Descriptions and consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 8 critical chance, 9 capture power, 10 movement, 11 max HP, 12 max SP, and 13 max FS. CALCBTPARAM adds columns 7 and 8 into the action's critical percentage, and CALCDMG compares the clamped result with a random-modulo-100 roll."
source = "investigation" source = "investigation"
@@ -181,6 +185,7 @@ address = "0xa2bf1"
name = "item_tuning_curve_ids" name = "item_tuning_curve_ids"
category = "data-table" category = "data-table"
type = "int[1000][10]" type = "int[1000][10]"
columns = { "0" = "accuracy", "1" = "evasion", "2" = "physical_attack", "3" = "physical_defense", "4" = "magic_attack", "5" = "magic_defense", "6" = "speed", "7" = "luck", "8" = "critical_chance", "9" = "capture_power" }
value_domain = "curve ids 1..18; zero means the field cannot be tuned" value_domain = "curve ids 1..18; zero means the field cannot be tuned"
usage = "ITINIT row-major table selecting an equipment-growth curve for each of the ten tunable fields. TUNE, IMPROVE, DRAWTIP, and CALCREVISE combine each nonzero curve id with the item's corresponding tuning level and index the shared curve-value table at 0xab6fa. Columns align with item_stat_modifiers columns 0..9." usage = "ITINIT row-major table selecting an equipment-growth curve for each of the ten tunable fields. TUNE, IMPROVE, DRAWTIP, and CALCREVISE combine each nonzero curve id with the item's corresponding tuning level and index the shared curve-value table at 0xab6fa. Columns align with item_stat_modifiers columns 0..9."
source = "investigation" source = "investigation"
@@ -191,6 +196,7 @@ address = "0xa5301"
name = "item_resource_recovery_amounts" name = "item_resource_recovery_amounts"
category = "data-table" category = "data-table"
type = "int[1000][3]" type = "int[1000][3]"
columns = { "0" = "hp", "1" = "sp", "2" = "fs" }
value_domain = "recovery amount; zero means absent" value_domain = "recovery amount; zero means absent"
usage = "Sparse ITINIT row-major consumable table. USEITEM applies columns 0, 1, and 2 to the matching three-column unit resource record; descriptions prove these are HP, SP, and FS respectively (for example item 101 stores HP 30, and item 107 stores 999/99/99 for full recovery)." usage = "Sparse ITINIT row-major consumable table. USEITEM applies columns 0, 1, and 2 to the matching three-column unit resource record; descriptions prove these are HP, SP, and FS respectively (for example item 101 stores HP 30, and item 107 stores 999/99/99 for full recovery)."
source = "investigation" source = "investigation"
@@ -206,6 +212,26 @@ usage = "Sparse ITINIT consumable field. Item 106, Blood Price Healing Hand, des
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
[[global]]
address = "0x65ce"
name = "skill_acquired_flags"
category = "data-table"
type = "int[300]"
value_domain = "0/1 by SKINIT skill id"
usage = "Persistent acquired-skill flags. ADDSKILL sets the selected skill after resolving the unit's equipped-skill slots; FORT checks the flag before granting a skill; CHMENU combines it with skill_change_catalog_eligible to build the available skill-change catalog."
source = "investigation"
confidence = "high"
[[global]]
address = "0x5660b"
name = "skill_info_revealed_flags"
category = "data-table"
type = "int[300]"
value_domain = "0/1 by SKINIT skill id"
usage = "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."
source = "investigation"
confidence = "high"
[[global]] [[global]]
address = "0xa6e59" address = "0xa6e59"
name = "current_skill_id" name = "current_skill_id"
@@ -236,6 +262,16 @@ usage = "SKINIT category for all 131 skills. The records establish 1 movement/ex
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
[[global]]
address = "0xa70b2"
name = "skill_change_catalog_eligible"
category = "data-table"
type = "int[300]"
value_domain = "0/1; 11 eligible skills in shipped SKINIT"
usage = "SKINIT inclusion flag for CHMENU's fourth (skill-change) catalog. CHMENU scans all 300 ids, keeps only nonzero rows, sorts acquired rows ahead of unavailable rows using skill_category and skill_sort_key, and copies the acquired prefix into catalog row 3. Eligible ids are 1, 2, 112, 130..133, 210, 228, 230, and 231."
source = "investigation"
confidence = "high"
[[global]] [[global]]
address = "0xa71de" address = "0xa71de"
name = "skill_min_range_encoded" name = "skill_min_range_encoded"
@@ -271,6 +307,7 @@ address = "0xa7562"
name = "skill_status_levels" name = "skill_status_levels"
category = "data-table" category = "data-table"
type = "int[300][30]" type = "int[300][30]"
columns = { "1" = "instant_death", "6" = "charm", "7" = "confusion", "8" = "paralysis", "9" = "poison", "10" = "water_flow", "11" = "fear" }
value_domain = "condition strength 1..4; zero means absent" value_domain = "condition strength 1..4; zero means absent"
usage = "Sparse SKINIT row-major condition table consumed by CALCILL when applying a skill's effects to the target unit. Skill names and descriptions identify columns 1 instant death, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear; the column layout matches item_status_delta_levels." usage = "Sparse SKINIT row-major condition table consumed by CALCILL when applying a skill's effects to the target unit. Skill names and descriptions identify columns 1 instant death, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear; the column layout matches item_status_delta_levels."
source = "investigation" source = "investigation"
@@ -291,6 +328,7 @@ address = "0xa99b6"
name = "skill_combat_stat_deltas" name = "skill_combat_stat_deltas"
category = "data-table" category = "data-table"
type = "int[300][10]" type = "int[300][10]"
columns = { "0" = "accuracy", "1" = "evasion", "2" = "physical_attack", "3" = "physical_defense", "4" = "magic_attack", "5" = "magic_defense", "6" = "speed", "7" = "luck", "8" = "critical_chance", "9" = "capture_power" }
value_domain = "signed combat parameter deltas; observed -99..999" value_domain = "signed combat parameter deltas; observed -99..999"
usage = "Sparse SKINIT row-major table applied by CALCBTPARAM for the selected action. Descriptions and combat consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 8 critical chance, and 9 capture power. Column 7 is unpopulated in shipped SKINIT; CALCBTPARAM adds any luck and critical modifiers into the critical percentage that CALCDMG rolls after the hit check." usage = "Sparse SKINIT row-major table applied by CALCBTPARAM for the selected action. Descriptions and combat consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 8 critical chance, and 9 capture power. Column 7 is unpopulated in shipped SKINIT; CALCBTPARAM adds any luck and critical modifiers into the critical percentage that CALCDMG rolls after the hit check."
source = "investigation" source = "investigation"
@@ -301,6 +339,7 @@ address = "0xaa56e"
name = "skill_resource_deltas" name = "skill_resource_deltas"
category = "data-table" category = "data-table"
type = "int[300][3]" type = "int[300][3]"
columns = { "0" = "hp_recovery", "1" = "sp_cost" }
value_domain = "column 0 recovery 20..50; column 1 cost -20..-2" value_domain = "column 0 recovery 20..50; column 1 cost -20..-2"
usage = "Sparse SKINIT resource table used throughout skill selection and resolution. Column 0 is HP recovery for the three healing spells; column 1 is the negative SP cost for all 95 active skills, exactly matching each description. Column 2 is unpopulated in the shipped table." usage = "Sparse SKINIT resource table used throughout skill selection and resolution. Column 0 is HP recovery for the three healing spells; column 1 is the negative SP cost for all 95 active skills, exactly matching each description. Column 2 is unpopulated in the shipped table."
source = "investigation" source = "investigation"