From 9efa56c2107ad4b30cf6c78e8113d2da34888d09 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 22 Jul 2026 23:15:33 -0400 Subject: [PATCH] Expose semantic INIT column names --- docs/global-reference.md | 28 ++++++------ docs/name-resolution.md | 31 +++++++++---- docs/phase-b-framework.md | 9 ++++ docs/tools-reference.md | 6 +-- tools/extract_init.py | 40 ++++++++++++++++- tools/globals_build.py | 26 +++++++++++ tools/init_table_profile.py | 74 +++++++++++++++++++++++++++++--- tools/test_extract_init.py | 11 +++++ tools/test_globals.py | 7 ++- tools/test_init_table_profile.py | 13 ++++++ vm-map/globals.toml | 43 ++++++++++++++++++- 11 files changed, 251 insertions(+), 37 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 8c2add3..4c1c3db 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # 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 @@ -27,8 +27,10 @@ | 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. | +| `0x65ce` | skill_acquired_flags | high | investigation | Persistent acquired-skill flags. ADDSKILL sets the selected skill after resolving the unit's equipped-skill slots; FORT checks the flag before granting a skill; CHMENU combines it with skill_change_catalog_eligible to build the available skill-change catalog. | | `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. | | `0x67a0` | party_slot_character_id | high | investigation | Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster. | +| `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. | | `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. | @@ -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. | | `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]. | -| `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. | -| `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. | -| `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. | -| `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. | +| `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 | 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. 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. 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. | -| `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. | -| `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. | -| `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). | +| `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. 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). 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. | | `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. | | `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. | +| `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. | | `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. | -| `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. | -| `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. | -| `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. | +| `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. 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. | | `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. | @@ -135,7 +138,6 @@ | `0x4379` | — | low | auto-shape | array | | `0x45d7` | — | low | auto-shape | array | | `0x463b` | — | low | auto-shape | array | -| `0x65ce` | — | low | auto-shape | array | | `0x671c` | — | low | auto-shape | array | | `0x671f` | — | low | auto-shape | array | | `0x6727` | — | low | auto-shape | array | @@ -166,7 +168,6 @@ | `0x53ede` | — | low | auto-shape | array | | `0x55e3b` | — | low | auto-shape | array | | `0x56223` | — | low | auto-shape | array | -| `0x5660b` | — | low | auto-shape | array | | `0x56b20` | — | low | auto-shape | array | | `0x56b52` | — | low | auto-shape | array | | `0x56b85` | — | low | auto-shape | array | @@ -4987,7 +4988,6 @@ | `0x7843e` | — | med | auto-shape | unit-field | | `0x81c96` | — | 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? | | `0xaacf0` | — | med | auto-shape | record-table[stride 5] | | `0xaad86` | — | med | auto-shape | record-table[stride 55] | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index f778695..7107c79 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -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 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 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: `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`). - **`tools/globals_build.py --build`** merges curated entries *over* the auto map → `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: -1. **Continue INIT semantics by evidence density.** Resolve ITINIT/SKINIT's remaining stat and condition - columns, then work through EBINIT's AI, route/evolution, and remaining sparse-flag tables by consumer - strength. Preserve explicit item → skill and unit → attack/skill/equipment/drop joins. Do not infer - meaning from column position alone. -2. **Fold in the `*MES` message-table writers** (`ITMES`, `SKMES`, `VIMES`, …) and any other - `set-string`/`copy-to-global` writers not covered by the `*INIT` set — pure static win, - extends the string/data labels. (Also: most name-table bases are *read* rarely — reads - likely go through `*MES`/an indirection; tracing that would connect names to their readers.) +1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT's populated row columns now have + machine-readable meanings; extend the same structured `columns` metadata through the confirmed EBINIT + tables, then investigate its unread enum and boss-class sign only when consumer evidence appears. + Preserve explicit item → skill and unit → attack/skill/equipment/drop joins. Do not infer meaning from + column position alone. +2. **Extend message-table joins beyond the completed ITMES/SKMES pair** (`VIMES`, other id dispatchers, …) + and fold in other `set-string`/`copy-to-global` writers not covered by the `*INIT` set. 3. **Label 2D record tables by their readers** — cross-reference which scripts read each `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 71893d5..93d492f 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -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 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; enemy AI appears to live outside the static EBINIT schema. STINIT's bespoke parser remains a separate extraction task. diff --git a/docs/tools-reference.md b/docs/tools-reference.md index d66c781..38ea3c9 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -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_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`.* | — | -| `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` | | `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}` | @@ -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_message_table.py` | Discover a repeated global-id dispatch chain such as ITMES/SKMES, reconstruct player-facing title/description lines (including furigana surface text and readings), and emit an ID-keyed message table with bytecode provenance. | `extract_message_table.py [OUTNAME]` | `.BIN` → `build/data/.json` | -| `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer 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 [OUTNAME] [--mode …]` | `
.BIN` plus matching `.BIN` when supported → `build/data/.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
[--build] [--limit N]` | `build/data/
.json` + corpus → stdout; with `--build`, `build/data/
-field-profile.{json,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
[OUTNAME] [--mode …]` | `
.BIN` plus matching `.BIN` when supported + `build/globals.json` → `build/data/.json`, `build/data/README.md` | +| `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric 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
[--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 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}` | diff --git a/tools/extract_init.py b/tools/extract_init.py index 778b9a6..eafbf31 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -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: """Regenerate the disposable build/data index from current table JSONs.""" tables = [] @@ -323,6 +357,8 @@ def write_data_index(data_dir: Path) -> None: "Where a matching `*MES` dispatcher exists, `message` preserves its player-facing", "title, description, furigana, and bytecode dispatch offset separately from the", "short description stored by the INIT script.", + "Top-level `field_semantics` maps raw array/row-column keys to canonical machine-readable", + "names from `vm-map/globals.toml`; raw keys remain intact as bytecode provenance.", "", "Use `tools/init_table_profile.py
--build` to generate value/population and", "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)) + semantics = field_semantics(recs) out = {"table": name, "source": scr.path.name, "magic": scr.magic, "mode": mode, "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.parent.mkdir(parents=True, exist_ok=True) outpath.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8") diff --git a/tools/globals_build.py b/tools/globals_build.py index b3864d8..43cb4f5 100644 --- a/tools/globals_build.py +++ b/tools/globals_build.py @@ -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}") if src == "auto-shape" and conf == "high": 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", []): if _parse_addr(dep) not in all_addrs: 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"), "depends_on": [f"0x{_parse_addr(d):x}" for d in e.get("depends_on", [])], "provenance": "curated"} + if e.get("columns"): + out[addr]["columns"] = { + str(column): name for column, name in e["columns"].items() + } return out @@ -121,6 +139,14 @@ def emit_reference_md(merged: dict[int, dict]) -> str: e = merged[addr] name = e["name"] or "—" 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("") return "\n".join(L) + "\n" diff --git a/tools/init_table_profile.py b/tools/init_table_profile.py index e852636..2a7079c 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -19,6 +19,7 @@ from __future__ import annotations import argparse import collections import json +import re import sys from pathlib import Path @@ -49,6 +50,7 @@ def value_key(value) -> str: def profile_columns(data: dict) -> list[dict]: records = data["records"] + field_semantics = data.get("field_semantics", {}) values: dict[str, list] = collections.defaultdict(list) examples: dict[str, list[dict]] = collections.defaultdict(list) identities: dict[str, dict] = {} @@ -59,14 +61,18 @@ def profile_columns(data: dict) -> list[dict]: identities[key] = { "key": key, "kind": "parallel-array", "base": key, "stride": None, "column": None, + "semantic_name": field_semantics.get(key), } values[key].append(value) if len(examples[key]) < 5: - examples[key].append({ + example = { "id": record["id"], "name": record.get("name", ""), "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(): base_text, stride_text, column_text = key.split("/") base = int(base_text, 16) @@ -79,14 +85,18 @@ def profile_columns(data: dict) -> list[dict]: "base": f"0x{base:x}", "stride": stride, "column": column, + "semantic_name": field_semantics.get(normalized_key), } values[normalized_key].append(value) if len(examples[normalized_key]) < 5: - examples[normalized_key].append({ + example = { "id": record["id"], "name": record.get("name", ""), "value": value, - }) + } + if message := record.get("message"): + example["message_description"] = message.get("description", "") + examples[normalized_key].append(example) rows = [] 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: by_base: dict[int, list[dict]] = collections.defaultdict(list) 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"- 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: 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] ).replace("|", "\\|") 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['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("--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( + "--message-query", + metavar="REGEX", + help="show matching names/player-facing messages beside all populated fields", + ) args = parser.parse_args() name = args.table.upper().removesuffix(".JSON").removesuffix(".BIN") @@ -231,6 +289,8 @@ def main() -> int: )), } markdown = render_markdown(data, rows, args.limit) + if args.message_query: + print(render_message_matches(data, args.message_query)) print(markdown) if args.build: stem = paths.BUILD / "data" / f"{name}-field-profile" diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 1869aef..552cabe 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -130,11 +130,22 @@ def test_message_join() -> None: "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__": test_real_name_tables() test_static_negative_write() test_real_message_tables() test_message_join() + test_field_semantics() if FAILS: raise SystemExit(f"{len(FAILS)} failed checks") print("all extract_init checks passed") diff --git a/tools/test_globals.py b/tools/test_globals.py index bbee05e..ae40af5 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -22,10 +22,13 @@ def test_load_and_lint(): def test_lint_catches_bad_vocab(): 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}) 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("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(): 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]["category"] == "story-flag", "curated 0xa57 category overrides auto string-table") 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 auto_only = next((a for a in auto.get("globals", {}) if int(a, 16) not in curated and auto["globals"][a].get("label")), None) diff --git a/tools/test_init_table_profile.py b/tools/test_init_table_profile.py index 27e45b4..9a7989f 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -11,6 +11,11 @@ import init_table_profile as profile def main() -> int: fixture = { + "table": "TEST", + "field_semantics": { + "0x10": "test_parallel", + "0x30/3/0": "test_record.zero", + }, "records": [ {"id": 1, "name": "one", "fields": {"0x10": 2, "0x20": 0}, "record_fields": {"0x30/3/0": 9}, @@ -27,17 +32,25 @@ def main() -> int: assert rows["0x10"]["distinct_values"] == 2 assert rows["0x10"]["min"] == 2 and rows["0x10"]["max"] == 5 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"]["examples"][0]["name"] == "one" assert rows["0x30/3/0"]["kind"] == "record-column" assert rows["0x30/3/0"]["base"] == "0x30" assert rows["0x30/3/0"]["stride"] == 3 + assert rows["0x30/3/0"]["semantic_name"] == "test_record.zero" assert rows["0x30/3/2"]["column"] == 2 messages = profile.profile_messages(fixture) assert messages["population"] == 1 assert messages["coverage"] == 1 / 3 assert messages["furigana_records"] == 1 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") return 0 diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 3aac2a3..b9ea30d 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -131,6 +131,7 @@ address = "0x8e7b9" name = "item_character_whitelist" category = "data-table" 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" 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" @@ -142,15 +143,16 @@ name = "item_sex_restriction_mask" category = "data-table" type = "int[1000]" 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" -confidence = "med" +confidence = "high" [[global]] address = "0x906f9" name = "item_status_delta_levels" category = "data-table" 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" 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" @@ -161,6 +163,7 @@ address = "0x97c29" name = "item_equipped_status_levels" category = "data-table" type = "int[1000][30]" +columns = { "9" = "poison", "11" = "fear", "13" = "regeneration", "14" = "exaltation" } 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." source = "investigation" @@ -171,6 +174,7 @@ address = "0x9f541" name = "item_stat_modifiers" category = "data-table" 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" 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" @@ -181,6 +185,7 @@ address = "0xa2bf1" name = "item_tuning_curve_ids" category = "data-table" 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" 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" @@ -191,6 +196,7 @@ address = "0xa5301" name = "item_resource_recovery_amounts" category = "data-table" type = "int[1000][3]" +columns = { "0" = "hp", "1" = "sp", "2" = "fs" } 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)." source = "investigation" @@ -206,6 +212,26 @@ usage = "Sparse ITINIT consumable field. Item 106, Blood Price Healing Hand, des source = "investigation" 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]] address = "0xa6e59" name = "current_skill_id" @@ -236,6 +262,16 @@ usage = "SKINIT category for all 131 skills. The records establish 1 movement/ex source = "investigation" 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]] address = "0xa71de" name = "skill_min_range_encoded" @@ -271,6 +307,7 @@ address = "0xa7562" name = "skill_status_levels" category = "data-table" 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" 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" @@ -291,6 +328,7 @@ address = "0xa99b6" name = "skill_combat_stat_deltas" category = "data-table" 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" 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" @@ -301,6 +339,7 @@ address = "0xaa56e" name = "skill_resource_deltas" category = "data-table" type = "int[300][3]" +columns = { "0" = "hp_recovery", "1" = "sp_cost" } 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." source = "investigation"