Decode ILINIT condition and RECOVER schemas

This commit is contained in:
gamer147
2026-07-23 18:45:53 -04:00
parent 7f344739e6
commit 8a1c11ebed
8 changed files with 650 additions and 21 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)
5045 globals (277 curated, 4768 auto shape-inferred). Source of truth: `vm-map/globals.toml`. 5046 globals (290 curated, 4756 auto shape-inferred). Source of truth: `vm-map/globals.toml`.
## choice-output ## choice-output
@@ -23,6 +23,7 @@
| `0x4dfbc` | scjump_progress_a | med | inference | Dominant SCJUMP switch input (1609 comparison reads) — a per-chapter story-progress counter/position the progression machine branches on. INFERENCE from SCJUMP usage; confirm exact meaning via a listing/playthrough. | | `0x4dfbc` | scjump_progress_a | med | inference | Dominant SCJUMP switch input (1609 comparison reads) — a per-chapter story-progress counter/position the progression machine branches on. INFERENCE from SCJUMP usage; confirm exact meaning via a listing/playthrough. |
| `0x665d6` | modal_message_line_count | high | investigation | Number of populated strings in modal_message_lines. Dozens of menu/gameplay producers append at lines[count] and increment this value; MES renders the resulting non-selecting modal and clears the count, while SBUNKI consumes the same buffer as selectable options. | | `0x665d6` | modal_message_line_count | high | investigation | Number of populated strings in modal_message_lines. Dozens of menu/gameplay producers append at lines[count] and increment this value; MES renders the resulting non-selecting modal and clears the count, while SBUNKI consumes the same buffer as selectable options. |
| `0x665e2` | modal_annotation_count | high | investigation | Number of populated modal_annotation_texts entries. MES and SBUNKI iterate this many annotations and clear it on dismissal; no direct shipped producer was found. | | `0x665e2` | modal_annotation_count | high | investigation | Number of populated modal_annotation_texts entries. MES and SBUNKI iterate this many annotations and clear it on dismissal; no direct shipped producer was found. |
| `0xaac77` | current_condition_delta_levels | high | investigation | Shared signed level delta passed to ADDILL/ADDILLSUB for current_condition_id. Positive values apply or strengthen a condition; negative values weaken/remove it subject to the equipment/passive baseline floor. |
| `0xcc9f2` | usable_action_min_range | high | investigation | CALCSCOPE initializes this from the equipped/default attack's minimum range and widens it for usable offensive and healing skills. It is the lower bound shared by the offensive and healing action-scope tables. | | `0xcc9f2` | usable_action_min_range | high | investigation | CALCSCOPE initializes this from the equipped/default attack's minimum range and widens it for usable offensive and healing skills. It is the lower bound shared by the offensive and healing action-scope tables. |
| `0xcc9f3` | usable_action_max_range | high | investigation | CALCSCOPE initializes this from the equipped/default attack's maximum range and widens it for usable offensive and healing skills. ATSEEK bounds its action-range flood fill with this value; RTN_M051/052 and RTN_M061 use it as their upper scan bound. | | `0xcc9f3` | usable_action_max_range | high | investigation | CALCSCOPE initializes this from the equipped/default attack's maximum range and widens it for usable offensive and healing skills. ATSEEK bounds its action-range flood fill with this value; RTN_M051/052 and RTN_M061 use it as their upper scan bound. |
| `0xe6c5d` | scjump_progress_d | med | inference | SCJUMP switch input (168 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. | | `0xe6c5d` | scjump_progress_d | med | inference | SCJUMP switch input (168 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. |
@@ -53,6 +54,9 @@
| `0x522ed` | entity_faction_ids | high | investigation | Per-runtime-entity faction id. RTN_M007 restricts its injured-unit target to the acting entity's faction; MVSEEK mode 2 compares this value while masking active foreign-entity cells, and ordinary movement checks the acting faction's bit in tile passability masks. | | `0x522ed` | entity_faction_ids | high | investigation | Per-runtime-entity faction id. RTN_M007 restricts its injured-unit target to the acting entity's faction; MVSEEK mode 2 compares this value while masking active foreign-entity cells, and ordinary movement checks the acting faction's bit in tile passability masks. |
| `0x5231f` | entity_tile_x | high | investigation | Per-runtime-entity map X coordinate. FIELD movement and occupancy logic maintain it; movement providers pair it with entity_tile_y and compare or route from the current tile. | | `0x5231f` | entity_tile_x | high | investigation | Per-runtime-entity map X coordinate. FIELD movement and occupancy logic maintain it; movement providers pair it with entity_tile_y and compare or route from the current tile. |
| `0x52351` | entity_tile_y | high | investigation | Per-runtime-entity map Y coordinate paired with entity_tile_x. Values use the same stage map-space cell keys as STINIT object and enemy coordinates. | | `0x52351` | entity_tile_y | high | investigation | Per-runtime-entity map Y coordinate paired with entity_tile_x. Values use the same stage map-space cell keys as STINIT object and enemy coordinates. |
| `0x52383` | entity_condition_levels | high | investigation | Per-runtime-entity current condition levels. ADDILLSUB applies signed level deltas, clamps the minimum to entity_condition_baseline_levels and the maximum to 5, and assigns condition duration. CALCREVISE, FIELD, combat, and UI consumers index the same ILINIT-defined 30-column condition id space. Columns: 1=instant_death, 2=hp_drain, 3=sp_drain, 4=fs_drain, 5=curse, 6=charm, 7=confusion, 8=paralysis, 9=poison, 10=water_flow, 11=fear, 12=reserved, 13=regeneration, 14=exaltation. |
| `0x5295f` | entity_condition_remaining_turns | high | investigation | Per-runtime-entity duration counters paired with entity_condition_levels. FIELD decrements positive counters and removes the condition when a counter reaches zero; DRAWENP displays negative values as infinity. RECOVER writes -1 when a recoverable condition retains an equipment/passive baseline and zero when it clears completely. Columns: 1=instant_death, 2=hp_drain, 3=sp_drain, 4=fs_drain, 5=curse, 6=charm, 7=confusion, 8=paralysis, 9=poison, 10=water_flow, 11=fear, 12=reserved, 13=regeneration, 14=exaltation. |
| `0x52f3b` | entity_condition_baseline_levels | high | investigation | Per-runtime-entity minimum condition levels supplied by equipment and passive effects. SETCH/SETEN populate the row from item_equipped_status_levels, ADDILLSUB cannot reduce current levels below it, CALCREVISE separates transient current levels from this baseline, and RECOVER restores eligible conditions to it. Columns: 1=instant_death, 2=hp_drain, 3=sp_drain, 4=fs_drain, 5=curse, 6=charm, 7=confusion, 8=paralysis, 9=poison, 10=water_flow, 11=fear, 12=reserved, 13=regeneration, 14=exaltation. |
| `0x53517` | entity_movement_routine_set_ids | high | investigation | Per-runtime-entity movement routine set for easy/normal/hard. SETEN copies a stage-specific override or the EBINIT default into this row; MVRTN selects the current difficulty column and stores it in current_routine_set_id. | | `0x53517` | entity_movement_routine_set_ids | high | investigation | Per-runtime-entity movement routine set for easy/normal/hard. SETEN copies a stage-specific override or the EBINIT default into this row; MVRTN selects the current difficulty column and stores it in current_routine_set_id. |
| `0x535ad` | entity_battle_routine_set_ids | high | investigation | Per-runtime-entity battle routine set for easy/normal/hard. SETEN copies a stage-specific override or the EBINIT default into this row; BTRTN selects the current difficulty column and stores it in current_routine_set_id. | | `0x535ad` | entity_battle_routine_set_ids | high | investigation | Per-runtime-entity battle routine set for easy/normal/hard. SETEN copies a stage-specific override or the EBINIT default into this row; BTRTN selects the current difficulty column and stores it in current_routine_set_id. |
| `0x53643` | entity_movement_routine_progress | high | investigation | Per-runtime-entity movement-routine progress counters. FIELD clears all fifty rows, movement providers increment the current step, and MVRTN compares it with movement_routine_minimum_progress_counts before enabling a step. | | `0x53643` | entity_movement_routine_progress | high | investigation | Per-runtime-entity movement-routine progress counters. FIELD clears all fifty rows, movement providers increment the current step, and MVRTN compares it with movement_routine_minimum_progress_counts before enabling a step. |
@@ -140,6 +144,13 @@
| `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. |
| `0xaac78` | condition_effectiveness_element_ids | high | investigation | ILINIT condition metadata. CALCILL uses the selected condition's value as the element-table column for its application-chance effectiveness calculation. |
| `0xaac96` | condition_can_affect_bosses | high | investigation | ILINIT policy vector. CALCILL rejects a condition against a boss-class target when this cell is zero; nonzero cells bypass that boss immunity gate. |
| `0xaacb4` | condition_cleared_by_recover | high | investigation | ILINIT recovery-policy vector. RECOVER and DISARM use it to identify charm, confusion, paralysis, poison, water-flow, and fear as removable conditions; RECOVER resets each eligible current level to its equipment/passive baseline. |
| `0xaacd2` | condition_icon_ids | high | investigation | ILINIT display metadata. DRAWENP and FIELD use these ids to deduplicate active-condition indicators and select the corresponding status-icon atlas entry. |
| `0xaacf0` | condition_duration_turns_by_level | high | investigation | ILINIT's five-column duration row for each condition. ADDILLSUB indexes it by the resulting condition level minus one and stores the value in entity_condition_remaining_turns. Columns: 0=level_1_turns, 1=level_2_turns, 2=level_3_turns, 3=level_4_turns, 4=level_5_turns. |
| `0xaad86` | condition_stat_deltas | high | investigation | ILINIT's five-level by eleven-stat matrix for each condition. CALCREVISE indexes (condition level - 1) * 11 + stat column and adds the result to entity_effective_stats columns 0..10. Shipped rows populate curse, confusion, paralysis, and exaltation. Columns: 0=level_1_accuracy, 1=level_1_evasion, 2=level_1_physical_attack, 3=level_1_physical_defense, 4=level_1_magic_attack, 5=level_1_magic_defense, 6=level_1_speed, 7=level_1_luck, 8=level_1_critical_chance, 9=level_1_capture_power, 10=level_1_movement, 11=level_2_accuracy, 12=level_2_evasion, 13=level_2_physical_attack, 14=level_2_physical_defense, 15=level_2_magic_attack, 16=level_2_magic_defense, 17=level_2_speed, 18=level_2_luck, 19=level_2_critical_chance, 20=level_2_capture_power, 21=level_2_movement, 22=level_3_accuracy, 23=level_3_evasion, 24=level_3_physical_attack, 25=level_3_physical_defense, 26=level_3_magic_attack, 27=level_3_magic_defense, 28=level_3_speed, 29=level_3_luck, 30=level_3_critical_chance, 31=level_3_capture_power, 32=level_3_movement, 33=level_4_accuracy, 34=level_4_evasion, 35=level_4_physical_attack, 36=level_4_physical_defense, 37=level_4_magic_attack, 38=level_4_magic_defense, 39=level_4_speed, 40=level_4_luck, 41=level_4_critical_chance, 42=level_4_capture_power, 43=level_4_movement, 44=level_5_accuracy, 45=level_5_evasion, 46=level_5_physical_attack, 47=level_5_physical_defense, 48=level_5_magic_attack, 49=level_5_magic_defense, 50=level_5_speed, 51=level_5_luck, 52=level_5_critical_chance, 53=level_5_capture_power, 54=level_5_movement. |
| `0xab3f8` | condition_resource_deltas | high | investigation | ILINIT's five-level by three-resource matrix for each condition. FIELD applies the active row each turn; CALCDMG also consumes the HP/SP/FS drain condition rows during battle. Positive values restore/drain to the acting side as defined by the caller, while negative values are periodic damage or loss. Columns: 0=level_1_hp, 1=level_1_sp, 2=level_1_fs, 3=level_2_hp, 4=level_2_sp, 5=level_2_fs, 6=level_3_hp, 7=level_3_sp, 8=level_3_fs, 9=level_4_hp, 10=level_4_sp, 11=level_4_fs, 12=level_5_hp, 13=level_5_sp, 14=level_5_fs. |
| `0xab5ba` | attack_element_effectiveness_percent | high | investigation | AFINIT-authored element matchup table. Positive values make an action eligible in SETMVWORK and RTN_M051/052; CALCBTPARAM multiplies battle parameters by the selected percentage and handles negative values as special/immunity cases. | | `0xab5ba` | attack_element_effectiveness_percent | high | investigation | AFINIT-authored element matchup table. Positive values make an action eligible in SETMVWORK and RTN_M051/052; CALCBTPARAM multiplies battle parameters by the selected percentage and handles negative values as special/immunity cases. |
| `0xab8c7` | class_change_rule_script_ids | high | investigation | CALCCC iterates these 32 cells and call-scripts every positive entry to evaluate class-change providers. CCINIT is the shipped rule program decoded into build/data/CCINIT.json. | | `0xab8c7` | class_change_rule_script_ids | high | investigation | CALCCC iterates these 32 cells and call-scripts every positive entry to evaluate class-change providers. CCINIT is the shipped rule program decoded into build/data/CCINIT.json. |
| `0xaba64` | stage_object_runtime_flags | high | investigation | Per-current-stage object flags. RTN_M010 and RTN_M015 require bit 1 before considering Healing Feathers or Magic Pillars as movement targets; FIELD and object rendering maintain the broader type-dependent bitfield. | | `0xaba64` | stage_object_runtime_flags | high | investigation | Per-current-stage object flags. RTN_M010 and RTN_M015 require bit 1 before considering Healing Feathers or Magic Pillars as movement targets; FIELD and object rendering maintain the broader type-dependent bitfield. |
@@ -287,10 +298,6 @@
| `0x665d8` | — | low | auto-shape | array | | `0x665d8` | — | low | auto-shape | array |
| `0x8fb41` | — | low | auto-shape | array | | `0x8fb41` | — | low | auto-shape | array |
| `0xa5eb9` | — | low | auto-shape | array | | `0xa5eb9` | — | low | auto-shape | array |
| `0xaac78` | — | low | auto-shape | array |
| `0xaac96` | — | low | auto-shape | array |
| `0xaacb4` | — | low | auto-shape | array |
| `0xaacd2` | — | low | auto-shape | array |
| `0xaba3b` | — | low | auto-shape | array | | `0xaba3b` | — | low | auto-shape | array |
| `0xaba43` | — | low | auto-shape | array | | `0xaba43` | — | low | auto-shape | array |
| `0xaba4b` | — | low | auto-shape | array | | `0xaba4b` | — | low | auto-shape | array |
@@ -399,6 +406,7 @@
| `0x66715` | current_unit_id | high | investigation | Shared current-unit selector used by character growth and setup scripts. CCINIT keys every class-change rule on this value; CALCCC, ADDEXP, SETEN, SALLY, and related scripts use it to index unit definitions and persistent per-unit state. | | `0x66715` | current_unit_id | high | investigation | Shared current-unit selector used by character growth and setup scripts. CCINIT keys every class-change rule on this value; CALCCC, ADDEXP, SETEN, SALLY, and related scripts use it to index unit definitions and persistent per-unit state. |
| `0x8c877` | current_item_id | high | investigation | Shared item-id argument/selection slot. Item menus and gameplay scripts write a chosen item id, use it to index ITINIT arrays, and dispatch through item_handler_script_id; ITMES compares it against all 287 item ids to select the matching player-facing title and description. | | `0x8c877` | current_item_id | high | investigation | Shared item-id argument/selection slot. Item menus and gameplay scripts write a chosen item id, use it to index ITINIT arrays, and dispatch through item_handler_script_id; ITMES compares it against all 287 item ids to select the matching player-facing title and description. |
| `0xa6e59` | current_skill_id | high | investigation | Shared skill-id argument/selection slot. Skill menus and combat scripts write the chosen skill id and use it to index SKINIT arrays; SKMES compares it against all 131 skill ids to select the matching player-facing title and description. | | `0xa6e59` | current_skill_id | high | investigation | Shared skill-id argument/selection slot. Skill menus and combat scripts write the chosen skill id and use it to index SKINIT arrays; SKMES compares it against all 131 skill ids to select the matching player-facing title and description. |
| `0xaac76` | current_condition_id | high | investigation | Shared condition selector consumed by CALCILL, ADDILL, ADDILLSUB, DISARM, FIELD, and related condition handlers. It indexes the 30-column runtime condition rows and ILINIT definition arrays. |
| `0xeff75` | current_routine_set_id | high | investigation | Shared RTINIT row selector. MVRTN loads the current entity's difficulty-selected movement routine set; BTRTN loads its battle routine set. Both then iterate routine_step_index across the selected twenty-slot row. | | `0xeff75` | current_routine_set_id | high | investigation | Shared RTINIT row selector. MVRTN loads the current entity's difficulty-selected movement routine set; BTRTN loads its battle routine set. Both then iterate routine_step_index across the selected twenty-slot row. |
| `0xeff76` | routine_step_index | high | investigation | Shared RTINIT step selector. MVRTN and BTRTN iterate it from zero through nineteen and use it as the column index in every routine bank and matching per-entity runtime row. | | `0xeff76` | routine_step_index | high | investigation | Shared RTINIT step selector. MVRTN and BTRTN iterate it from zero through nineteen and use it as the column index in every routine bank and matching per-entity runtime row. |
| `0x152616` | current_entity_index | med | investigation | Primary current-entity row index (RECOVER-confirmed; purity 0.51, 363 row-index uses). | | `0x152616` | current_entity_index | med | investigation | Primary current-entity row index (RECOVER-confirmed; purity 0.51, 363 row-index uses). |
@@ -459,6 +467,7 @@
| `0x27e` | unit_class_titles | high | investigation | Persistent per-unit class/title string table. CALCCC writes the selected class_change_title_output into the current unit's cell; character and status presentation scripts read the resulting title. | | `0x27e` | unit_class_titles | high | investigation | Persistent per-unit class/title string table. CALCCC writes the selected class_change_title_output into the current unit's cell; character and status presentation scripts read the resulting title. |
| `0x7db` | modal_message_lines | high | investigation | Shared ten-string modal buffer. Producers append messages or menu options at modal_message_line_count; MES measures and draws each string, and SBUNKI reuses the same entries for an interactive selection list. | | `0x7db` | modal_message_lines | high | investigation | Shared ten-string modal buffer. Producers append messages or menu options at modal_message_line_count; MES measures and draws each string, and SBUNKI reuses the same entries for an interactive selection list. |
| `0x7e5` | modal_annotation_texts | high | investigation | Optional small-font annotations rendered by MES and SBUNKI after the primary modal lines. No direct shipped producer was found; the paired count and placement arrays expose a reserved/extensible annotation ABI. | | `0x7e5` | modal_annotation_texts | high | investigation | Optional small-font annotations rendered by MES and SBUNKI after the primary modal lines. No direct shipped producer was found; the paired count and placement arrays expose a reserved/extensible annotation ABI. |
| `0x25fa` | condition_level_names | high | investigation | ILINIT's row-major five-name matrix. Runtime condition UI selects the row by condition id and the column by current level minus one; id 1 has only the unnumbered instant-death label, id 12 and ids 15..29 are reserved. Columns: 0=level_1, 1=level_2, 2=level_3, 3=level_4, 4=level_5. |
| `0x26b4` | class_change_title_output | high | investigation | CCINIT writes the title selected by each eligible class-change rule. CALCCC copies it to unit_class_titles for a successful promotion, and ADDEXP includes the same string in the level-up notification. | | `0x26b4` | class_change_title_output | high | investigation | CCINIT writes the title selected by each eligible class-change rule. CALCCC copies it to unit_class_titles for a successful promotion, and ADDEXP includes the same string in the level-up notification. |
| `0x26f1` | object_type_names | high | investigation | OBINIT writes the authoritative object names. FIELD, SETOBJ, and DRAWOBJ use STINIT's object type id to select these definitions; extract_init joins the names to stage object placements. | | `0x26f1` | object_type_names | high | investigation | OBINIT writes the authoritative object names. FIELD, SETOBJ, and DRAWOBJ use STINIT's object type id to select these definitions; extract_init joins the names to stage object placements. |
| `0x2755` | object_type_descriptions | high | investigation | OBINIT writes the short object descriptions displayed by the field object-information path. extract_init joins populated descriptions to STINIT object placements by type id. | | `0x2755` | object_type_descriptions | high | investigation | OBINIT writes the short object descriptions displayed by the field object-information path. extract_init joins populated descriptions to STINIT object placements by type id. |
@@ -3702,7 +3711,6 @@
| `0x1fba` | — | high | auto-shape | item-desc-table | | `0x1fba` | — | high | auto-shape | item-desc-table |
| `0x23a2` | — | high | auto-shape | skill-name-table | | `0x23a2` | — | high | auto-shape | skill-name-table |
| `0x24ce` | — | high | auto-shape | skill-desc-table | | `0x24ce` | — | high | auto-shape | skill-desc-table |
| `0x25fa` | — | med | auto-shape | record-table[stride 5] |
| `0x2ba5` | — | med | auto-shape | record-table[stride 6] | | `0x2ba5` | — | med | auto-shape | record-table[stride 6] |
| `0x3239` | — | med | auto-shape | record-table[stride 3] | | `0x3239` | — | med | auto-shape | record-table[stride 3] |
| `0x32f0` | — | med | auto-shape | current-entity-index? | | `0x32f0` | — | med | auto-shape | current-entity-index? |
@@ -3711,9 +3719,6 @@
| `0x7e54` | — | med | auto-shape | record-table[stride 50] | | `0x7e54` | — | med | auto-shape | record-table[stride 50] |
| `0x341ab` | — | med | auto-shape | record-table[stride 53] | | `0x341ab` | — | med | auto-shape | record-table[stride 53] |
| `0x4e3d7` | — | med | auto-shape | record-table[stride 14] | | `0x4e3d7` | — | med | auto-shape | record-table[stride 14] |
| `0x52383` | — | med | auto-shape | record-table[stride 30] |
| `0x5295f` | — | med | auto-shape | record-table[stride 30] |
| `0x52f3b` | — | med | auto-shape | record-table[stride 30] |
| `0x53a2b` | — | med | auto-shape | record-table[stride 20] | | `0x53a2b` | — | med | auto-shape | record-table[stride 20] |
| `0x53edd` | — | low | auto-shape | index/counter? | | `0x53edd` | — | low | auto-shape | index/counter? |
| `0x53ef5` | — | low | auto-shape | index/counter? | | `0x53ef5` | — | low | auto-shape | index/counter? |
@@ -5030,10 +5035,6 @@
| `0x6638b` | — | med | auto-shape | record-table[stride 15] | | `0x6638b` | — | med | auto-shape | record-table[stride 15] |
| `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] |
| `0xaac76` | — | low | auto-shape | index/counter? |
| `0xaacf0` | — | med | auto-shape | record-table[stride 5] |
| `0xaad86` | — | med | auto-shape | record-table[stride 55] |
| `0xab3f8` | — | med | auto-shape | record-table[stride 15] |
| `0xab6fa` | — | med | auto-shape | record-table[stride 11] | | `0xab6fa` | — | med | auto-shape | record-table[stride 11] |
| `0xab7d6` | — | med | auto-shape | record-table[stride 11] | | `0xab7d6` | — | med | auto-shape | record-table[stride 11] |
| `0xab8b2` | — | med | auto-shape | record-table[stride 7] | | `0xab8b2` | — | med | auto-shape | record-table[stride 7] |

View File

@@ -2806,3 +2806,32 @@ line and optional annotation buffers. Their stable globals are curated in `vm-ma
structural regressions protect the registry initialization, indirect dispatch, buffer reads, and cleanup. structural regressions protect the registry initialization, indirect dispatch, buffer reads, and cleanup.
**Next:** return to reader-proven 2D tables, beginning with RECOVER's 30-wide status/recovery structures. **Next:** return to reader-proven 2D tables, beginning with RECOVER's 30-wide status/recovery structures.
## Data-semantics sidebar: ILINIT condition matrix and RECOVER ABI (2026-07-23)
ILINIT is now a dedicated thirteen-record condition-definition schema inside the engine's reserved
30-condition by five-level layout. The generated JSON preserves every one of its 228 integer writes and
61 authored level-name strings, while adding a nested level view over duration, eleven-stat deltas, and
HP/SP/FS deltas. Condition ids 1..11, 13, and 14 are defined; id 12 and ids 15..29 remain explicit reserved
slots. Scalar metadata now identifies the effectiveness element, boss-immunity override, RECOVER policy,
and display icon for each condition. All raw address/stride/column keys remain beside their canonical
semantic joins.
The runtime side is one coherent four-table ABI. `entity_condition_levels[50][30]` holds current levels,
`entity_condition_baseline_levels[50][30]` is the equipment/passive minimum, and
`entity_condition_remaining_turns[50][30]` uses -1 for permanent/baseline, zero for inactive, and positive
turns for expiring effects. RECOVER first copies max HP/SP/FS from effective-stat columns 11..13 to the
three current resources. It then scans all 30 condition ids; for the six ILINIT-policy conditions
(charm, confusion, paralysis, poison, water-flow, and fear), it restores the current level to the passive
baseline and writes -1 when that baseline remains or zero when fully cleared. The extractor validates
those table bases, strides, loop bounds, and post-recovery CALCREVISE/DRAWCHP calls before publishing the
join.
Validation covers every ILINIT write, representative stat/resource matrices, the complete reserved id
space, level-aware semantic projection, the resource restore mapping, and the four-table reset protocol.
The global registry and generated reference now carry the condition id columns and all definition/runtime
table meanings.
**Next:** profile CNINIT as the next unresolved high-density name-mode table. Its current generic output
has 32 records but 242 address-derived fields, making it the clearest candidate for another dedicated
schema driven by reader-proven row/column ownership.

View File

@@ -57,12 +57,16 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited *
| `extract_message_table.py` | Discover repeated global-id dispatch chains in either fallthrough-body (ITMES/SKMES/MAMES) or branch-target-body (VIMES/EIMES/CIMES) form, reconstruct player-facing text and furigana, preserve guard/body offsets, and emit an ID-keyed message table. Title-bearing tables expose `title`/`description`; EIMES exposes its untitled lines as `summary`/`strategy`; CIMES exposes one untitled multiline `biography`; MAMES exposes untitled `description` text. | `extract_message_table.py <MES> [OUTNAME]` | `<MES>.BIN``build/data/<OUTNAME>.json` | | `extract_message_table.py` | Discover repeated global-id dispatch chains in either fallthrough-body (ITMES/SKMES/MAMES) or branch-target-body (VIMES/EIMES/CIMES) form, reconstruct player-facing text and furigana, preserve guard/body offsets, and emit an ID-keyed message table. Title-bearing tables expose `title`/`description`; EIMES exposes its untitled lines as `summary`/`strategy`; CIMES exposes one untitled multiline `biography`; MAMES exposes untitled `description` text. | `extract_message_table.py <MES> [OUTNAME]` | `<MES>.BIN``build/data/<OUTNAME>.json` |
| `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules / dispatch / banked shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, statically evaluate direct and negative-value writes, and separate parallel `fields` from linked row-major `record_fields`; when flat global ranges overlap, a parallel base established by other records wins over a coincidental row-table cell calculation. Dedicated name-mode schemas recover VIINIT's pre-title glossary prerequisites and CIINIT's profile name, backing-unit, optional portrait, and reserved placement columns. Mixed tables recover selector-dispatched records, condition strings, scalars, preallocated buffer cells, consumer-confirmed strides, and length-prefixed footer arrays; STINIT additionally assembles confirmed buffers into `object_placements` and `enemy_spawns`, joins object type names and available descriptions from OBINIT, and exposes consumer-proven tagged payloads (teleport, treasure, card, initial faction, non-triggering hazard/barrier faction, and OBINIT-gated initial object state), preserves engine-dead type-27 writes under `ignored_payload_fields`, and exposes reinforcement schedules and first-clear-only enemy gates. Rules mode decodes CCINIT's source-ordered class-change predicates and output accumulators, joining EBINIT unit names, SKINIT skill names, titles, named stat bonuses, cost deltas, and applied-state slots. Dispatch mode decodes SCINIT's paired decision-to-packed-scene and authored-chapter arrays, preserving source-order overwrites, resolving SYS4INI script names, and cross-checking chapter tags against decoded SCJUMP paths. Banked mode decodes RTINIT's twenty parallel 1000-by-20 movement/battle routine banks, retains all assignments and overwrites, assembles final per-slot steps, resolves provider selectors to RTN_M/RTN_B scripts, and applies selector-scoped schemas where proven. Current schemas cover all nineteen used providers, RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061, including progress-only, randomized roaming, object-slot, coordinate, enemy/ally, treasure and Magic Pillar search, Healing Feather, waypoint, faction-terrain, retreat, reachable normal-attack routing, immediate offensive target/action selection, and immediate allied healing. Parameterless behavior schemas publish an empty `parameter_fields` mapping while still joining `provider_behavior`, target selection, action selection, and completion rules. Provider defaults such as M004's unwritten slot 0, M010's zero/HP resource index, and M013's any-foreign-faction filter are projected explicitly and counted separately from populated source cells. Authored cells proven unread by M001/M008 remain under per-step `ignored_movement_parameters` and contribute to top-level `ignored_movement_parameter_count`; they are not silently discarded or assigned invented meanings. Raw provider banks remain intact, and top-level `movement_provider_parameter_schemas` documents the projection. ITINIT, SKINIT, VIINIT, EBINIT, and CIINIT join their matching MES text. Top-level `field_semantics` maps raw keys to canonical global/column names, while each record's `semantic_fields` is the generated name-keyed join; complete footer copies map to a row and expose its values without the raw provenance wrapper. Raw keys and unresolved tagged payload cells remain provenance. OUTNAME accepts a stem or one `.json` suffix and rejects paths. Refreshes the generated data index. | `extract_init.py <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, `build/callscript-names.json` for RTINIT/SCINIT, `build/scjump-decisions.json` for SCINIT, matching `<MES>.BIN` when supported, and `build/globals.json``build/data/<OUTNAME>.json`, `build/data/README.md` | | `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules / dispatch / banked shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, statically evaluate direct and negative-value writes, and separate parallel `fields` from linked row-major `record_fields`; when flat global ranges overlap, a parallel base established by other records wins over a coincidental row-table cell calculation. Dedicated name-mode schemas recover VIINIT's pre-title glossary prerequisites and CIINIT's profile name, backing-unit, optional portrait, and reserved placement columns. Mixed tables recover selector-dispatched records, condition strings, scalars, preallocated buffer cells, consumer-confirmed strides, and length-prefixed footer arrays; STINIT additionally assembles confirmed buffers into `object_placements` and `enemy_spawns`, joins object type names and available descriptions from OBINIT, and exposes consumer-proven tagged payloads (teleport, treasure, card, initial faction, non-triggering hazard/barrier faction, and OBINIT-gated initial object state), preserves engine-dead type-27 writes under `ignored_payload_fields`, and exposes reinforcement schedules and first-clear-only enemy gates. Rules mode decodes CCINIT's source-ordered class-change predicates and output accumulators, joining EBINIT unit names, SKINIT skill names, titles, named stat bonuses, cost deltas, and applied-state slots. Dispatch mode decodes SCINIT's paired decision-to-packed-scene and authored-chapter arrays, preserving source-order overwrites, resolving SYS4INI script names, and cross-checking chapter tags against decoded SCJUMP paths. Banked mode decodes RTINIT's twenty parallel 1000-by-20 movement/battle routine banks, retains all assignments and overwrites, assembles final per-slot steps, resolves provider selectors to RTN_M/RTN_B scripts, and applies selector-scoped schemas where proven. Current schemas cover all nineteen used providers, RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061, including progress-only, randomized roaming, object-slot, coordinate, enemy/ally, treasure and Magic Pillar search, Healing Feather, waypoint, faction-terrain, retreat, reachable normal-attack routing, immediate offensive target/action selection, and immediate allied healing. Parameterless behavior schemas publish an empty `parameter_fields` mapping while still joining `provider_behavior`, target selection, action selection, and completion rules. Provider defaults such as M004's unwritten slot 0, M010's zero/HP resource index, and M013's any-foreign-faction filter are projected explicitly and counted separately from populated source cells. Authored cells proven unread by M001/M008 remain under per-step `ignored_movement_parameters` and contribute to top-level `ignored_movement_parameter_count`; they are not silently discarded or assigned invented meanings. Raw provider banks remain intact, and top-level `movement_provider_parameter_schemas` documents the projection. ITINIT, SKINIT, VIINIT, EBINIT, and CIINIT join their matching MES text. Top-level `field_semantics` maps raw keys to canonical global/column names, while each record's `semantic_fields` is the generated name-keyed join; complete footer copies map to a row and expose its values without the raw provenance wrapper. Raw keys and unresolved tagged payload cells remain provenance. OUTNAME accepts a stem or one `.json` suffix and rejects paths. Refreshes the generated data index. | `extract_init.py <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, `build/callscript-names.json` for RTINIT/SCINIT, `build/scjump-decisions.json` for SCINIT, matching `<MES>.BIN` when supported, and `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/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. Name tables report message coverage for title/description, summary/strategy, biography, and description-only layouts; rule tables summarize covered units, titled and level-independent rules, threshold/slot distributions, and skill awards; dispatch tables summarize assignments, overwrites, script-name resolution, and SCJUMP chapter agreement; banked tables summarize populated/reserved banks, movement/battle steps and providers, selector-specific semantic coverage, and overwrite conflicts. `--message-query REGEX` searches every supported message field beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py <TABLE> [--build] [--limit N] [--message-query REGEX]` | `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/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. Name tables report message coverage for title/description, summary/strategy, biography, and description-only layouts; rule tables summarize covered units, titled and level-independent rules, threshold/slot distributions, and skill awards; dispatch tables summarize assignments, overwrites, script-name resolution, and SCJUMP chapter agreement; banked tables summarize populated/reserved banks, movement/battle steps and providers, selector-specific semantic coverage, and overwrite conflicts. `--message-query REGEX` searches every supported message field beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py <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, mixed selector-dispatched, CCINIT rule, SCINIT dispatch, and RTINIT banked extraction; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked profiling. | run each directly | — | | `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based, mixed selector-dispatched, CCINIT rule, SCINIT dispatch, RTINIT banked, and ILINIT condition-matrix extraction; RECOVER ABI validation; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked 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}` |
`extract_init.py` also has name-mode specializations for VIINIT's sparse 200-row glossary table, `extract_init.py` also has name-mode specializations for VIINIT's sparse 200-row glossary table,
CIINIT's 24 populated character-information profiles within four reserved 100-cell columns, and MAINIT's CIINIT's 24 populated character-information profiles within four reserved 100-cell columns, MAINIT's
eleven populated magic-action rows within a reserved 30-cell layout. Matching message joins are eleven populated magic-action rows within a reserved 30-cell layout, and ILINIT's thirteen populated
condition ids within a reserved 30-by-5 level layout. ILINIT classifies every authored write, adds nested
names/durations/stat/resource deltas, and validates/joins RECOVER's current-level, passive-baseline,
remaining-turn, recovery-policy, and full-resource-restore protocol. The profiler accepts the same
`base/stride/column` provenance for row-major string fields as for integer record columns. Matching message joins are
ITINIT→ITMES, SKINIT→SKMES, VIINIT→VIMES, EBINIT→EIMES, CIINIT→CIMES, and MAINIT→MAMES. EBINIT is ITINIT→ITMES, SKINIT→SKMES, VIINIT→VIMES, EBINIT→EIMES, CIINIT→CIMES, and MAINIT→MAMES. EBINIT is
intentionally sparse across unrelated unit definitions; MAINIT intentionally lacks MAMES bodies for growth intentionally sparse across unrelated unit definitions; MAINIT intentionally lacks MAMES bodies for growth
ritual ids 10 and 11. ritual ids 10 and 11.

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Extract a *INIT data table to JSON. Auto-detects the table's shape. """Extract a *INIT data table to JSON. Auto-detects the table's shape.
*INIT scripts populate global arrays and work buffers with static game data. Seven shapes seen: *INIT scripts populate global arrays and work buffers with static game data. Eight shapes seen:
name — records keyed by a name string. Each record: set-string(name), static field writes, name — records keyed by a name string. Each record: set-string(name), static field writes,
set-string(desc). Arrays indexed by record id in lockstep (+1/record). set-string(desc). Arrays indexed by record id in lockstep (+1/record).
@@ -19,6 +19,9 @@
banked —twenty parallel 1000-by-20 banks define sparse movement and battle routine banked —twenty parallel 1000-by-20 banks define sparse movement and battle routine
step records, including provider joins and source overwrites. (RTINIT routines) step records, including provider joins and source overwrites. (RTINIT routines)
ILINIT is a special name-mode matrix: 30 reserved condition ids by five authored
levels, joined to the runtime condition-state ABI and RECOVER policy.
Records are {id, name?, desc?, fields:{"0x<col_base>": value}} or, for footer tables, Records are {id, name?, desc?, fields:{"0x<col_base>": value}} or, for footer tables,
{id, global_addr, footer_off, values:[...]}. Column addresses are raw engine globals; {id, global_addr, footer_off, values:[...]}. Column addresses are raw engine globals;
confirmed names come from the generated engine global registry while raw keys remain provenance. confirmed names come from the generated engine global registry while raw keys remain provenance.
@@ -529,6 +532,57 @@ VOCABULARY_RECORD_TABLE_BASE = 0x15A2A9
VOCABULARY_RECORD_STRIDE = 3 VOCABULARY_RECORD_STRIDE = 3
VOCABULARY_RECORD_SPAN = 200 VOCABULARY_RECORD_SPAN = 200
CONDITION_RECORD_SPAN = 30
CONDITION_LEVEL_COUNT = 5
CONDITION_LEVEL_NAME_BASE = 0x25FA
CONDITION_COLUMNS = {
1: "instant_death",
2: "hp_drain",
3: "sp_drain",
4: "fs_drain",
5: "curse",
6: "charm",
7: "confusion",
8: "paralysis",
9: "poison",
10: "water_flow",
11: "fear",
12: "reserved",
13: "regeneration",
14: "exaltation",
}
CONDITION_SCALAR_ARRAYS = {
0xAAC78: "effectiveness_element_id",
0xAAC96: "can_affect_bosses",
0xAACB4: "cleared_by_recover",
0xAACD2: "icon_id",
}
CONDITION_DURATION_BASE = 0xAACF0
CONDITION_STAT_DELTA_BASE = 0xAAD86
CONDITION_RESOURCE_DELTA_BASE = 0xAB3F8
CONDITION_STAT_COLUMNS = (
"accuracy",
"evasion",
"physical_attack",
"physical_defense",
"magic_attack",
"magic_defense",
"speed",
"luck",
"critical_chance",
"capture_power",
"movement",
)
CONDITION_RESOURCE_COLUMNS = ("hp", "sp", "fs")
RECOVER_CURRENT_ENTITY = 0x152616
RECOVER_EFFECTIVE_STATS = 0x4E11B
RECOVER_CURRENT_RESOURCES = 0x4E085
RECOVER_CURRENT_LEVELS = 0x52383
RECOVER_REMAINING_TURNS = 0x5295F
RECOVER_BASELINE_LEVELS = 0x52F3B
RECOVER_POLICY = 0xAACB4
def resolve(name: str) -> Path: def resolve(name: str) -> Path:
for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"): for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"):
@@ -1199,6 +1253,280 @@ def extract_vocabulary(scr):
} }
def extract_recovery_protocol(scr) -> dict:
"""Validate and describe RECOVER's resource/condition reset ABI."""
lookups_2d = {
(ins.args[1][1], ins.args[3][1])
for ins in scr.instructions
if (
sys4load.display_label(ins.opcode) == "lookup-array-2d"
and len(ins.args) >= 5
and ins.args[1][0] == T_GLOBAL_INT
and ins.args[3][0] == T_IMM
)
}
lookups_1d = {
ins.args[1][1]
for ins in scr.instructions
if (
sys4load.display_label(ins.opcode) == "lookup-array"
and len(ins.args) >= 3
and ins.args[1][0] == T_GLOBAL_INT
)
}
loop_bounds = {
ins.args[2][1]
for ins in scr.instructions
if (
sys4load.display_label(ins.opcode) == "lt"
and len(ins.args) >= 3
and ins.args[2][0] == T_IMM
)
}
calls = {
ins.args[0][1]
for ins in scr.instructions
if sys4load.display_label(ins.opcode) == "call-script" and ins.args
}
required_2d = {
(RECOVER_EFFECTIVE_STATS, 14),
(RECOVER_CURRENT_RESOURCES, 3),
(RECOVER_CURRENT_LEVELS, CONDITION_RECORD_SPAN),
(RECOVER_REMAINING_TURNS, CONDITION_RECORD_SPAN),
(RECOVER_BASELINE_LEVELS, CONDITION_RECORD_SPAN),
}
failures = []
if not required_2d <= lookups_2d:
failures.append(f"missing 2d lookups {sorted(required_2d - lookups_2d)}")
if RECOVER_POLICY not in lookups_1d:
failures.append("missing recovery-policy lookup")
if not {3, CONDITION_RECORD_SPAN} <= loop_bounds:
failures.append("missing resource or condition loop bound")
if not {0x329D, 0x2ADE} <= calls:
failures.append("missing CALCREVISE or DRAWCHP post-call")
if failures:
raise ValueError(f"{scr.path.name}: " + "; ".join(failures))
return {
"source": scr.path.name,
"current_entity_selector": f"0x{RECOVER_CURRENT_ENTITY:x}",
"resource_restore": {
"source_table": f"0x{RECOVER_EFFECTIVE_STATS:x}",
"source_columns": ["max_hp", "max_sp", "max_fs"],
"destination_table": f"0x{RECOVER_CURRENT_RESOURCES:x}",
"destination_columns": ["current_hp", "current_sp", "current_fs"],
},
"condition_reset": {
"column_count": CONDITION_RECORD_SPAN,
"current_level_table": f"0x{RECOVER_CURRENT_LEVELS:x}",
"baseline_level_table": f"0x{RECOVER_BASELINE_LEVELS:x}",
"remaining_turns_table": f"0x{RECOVER_REMAINING_TURNS:x}",
"recovery_policy_table": f"0x{RECOVER_POLICY:x}",
"policy": (
"For each active condition whose policy cell is nonzero, copy "
"the equipment/passive baseline into the current level and set "
"remaining turns to -1 when that baseline is nonzero, otherwise 0."
),
},
"post_recovery_scripts": ["CALCREVISE.BIN", "DRAWCHP.BIN"],
}
def _condition_family_name(level_names: dict[int, str]) -> str | None:
"""Collapse authored `name1`..`name5` strings to their shared family."""
if not level_names:
return None
ordered = [level_names[level] for level in sorted(level_names)]
prefixes = [
text[:-1]
for level, text in sorted(level_names.items())
if text.endswith(str(level))
]
if len(prefixes) == len(ordered) and len(set(prefixes)) == 1:
return prefixes[0]
return ordered[0]
def extract_condition_definitions(scr):
"""Extract ILINIT's sparse 30-condition, five-level definition matrix."""
level_names: dict[int, dict[int, str]] = collections.defaultdict(dict)
records_by_id: dict[int, dict] = {}
classified_writes = 0
static_write_count = 0
def record_for(condition_id: int) -> dict:
if not (1 <= condition_id < CONDITION_RECORD_SPAN):
raise ValueError(
f"{scr.path.name}: condition id outside reserved span: {condition_id}"
)
return records_by_id.setdefault(condition_id, {
"id": condition_id,
"condition": CONDITION_COLUMNS.get(condition_id, f"reserved_{condition_id}"),
"string_fields": {},
"fields": {},
"record_fields": {},
})
for ins in scr.instructions:
if (
ins.opcode == SET_STRING
and len(ins.args) >= 2
and ins.args[0][0] == T_GLOBAL_STRING
):
relative = ins.args[0][1] - CONDITION_LEVEL_NAME_BASE
condition_id, level_index = divmod(relative, CONDITION_LEVEL_COUNT)
if not (
1 <= condition_id < CONDITION_RECORD_SPAN
and 0 <= level_index < CONDITION_LEVEL_COUNT
):
raise ValueError(
f"{scr.path.name}: unexpected condition name destination "
f"0x{ins.args[0][1]:x}"
)
text = scr.strings.get(ins.args[1][1], (None,))[0]
level_names[condition_id][level_index + 1] = text
_store_unique(
record_for(condition_id)["string_fields"],
(
f"0x{CONDITION_LEVEL_NAME_BASE:x}/"
f"{CONDITION_LEVEL_COUNT}/{level_index}"
),
text,
condition_id,
)
continue
write = _static_global_write(ins)
if write is None:
continue
static_write_count += 1
destination, value = write
matched = False
for base in CONDITION_SCALAR_ARRAYS:
condition_id = destination - base
if 1 <= condition_id < CONDITION_RECORD_SPAN:
_store_unique(
record_for(condition_id)["fields"],
f"0x{base:x}",
value,
condition_id,
)
matched = True
break
if not matched:
layouts = (
(CONDITION_DURATION_BASE, CONDITION_LEVEL_COUNT),
(
CONDITION_STAT_DELTA_BASE,
CONDITION_LEVEL_COUNT * len(CONDITION_STAT_COLUMNS),
),
(
CONDITION_RESOURCE_DELTA_BASE,
CONDITION_LEVEL_COUNT * len(CONDITION_RESOURCE_COLUMNS),
),
)
for base, stride in layouts:
relative = destination - base
condition_id, column = divmod(relative, stride)
if 1 <= condition_id < CONDITION_RECORD_SPAN:
_store_unique(
record_for(condition_id)["record_fields"],
f"0x{base:x}/{stride}/{column}",
value,
condition_id,
)
matched = True
break
if not matched:
raise ValueError(
f"{scr.path.name}: unclassified static write 0x{destination:x}"
)
classified_writes += 1
records = []
for condition_id in sorted(records_by_id):
record = records_by_id[condition_id]
names = level_names.get(condition_id, {})
record["name"] = _condition_family_name(names)
record["level_names"] = [
names.get(level) for level in range(1, CONDITION_LEVEL_COUNT + 1)
]
levels = []
for level in range(1, CONDITION_LEVEL_COUNT + 1):
level_record = {"level": level}
if level in names:
level_record["name"] = names[level]
duration_key = (
f"0x{CONDITION_DURATION_BASE:x}/{CONDITION_LEVEL_COUNT}/{level - 1}"
)
if duration_key in record["record_fields"]:
level_record["duration_turns"] = record["record_fields"][duration_key]
stat_deltas = {}
for stat_index, stat_name in enumerate(CONDITION_STAT_COLUMNS):
column = (level - 1) * len(CONDITION_STAT_COLUMNS) + stat_index
key = (
f"0x{CONDITION_STAT_DELTA_BASE:x}/"
f"{CONDITION_LEVEL_COUNT * len(CONDITION_STAT_COLUMNS)}/{column}"
)
if key in record["record_fields"]:
stat_deltas[stat_name] = record["record_fields"][key]
if stat_deltas:
level_record["stat_deltas"] = stat_deltas
resource_deltas = {}
for resource_index, resource_name in enumerate(CONDITION_RESOURCE_COLUMNS):
column = (level - 1) * len(CONDITION_RESOURCE_COLUMNS) + resource_index
key = (
f"0x{CONDITION_RESOURCE_DELTA_BASE:x}/"
f"{CONDITION_LEVEL_COUNT * len(CONDITION_RESOURCE_COLUMNS)}/{column}"
)
if key in record["record_fields"]:
resource_deltas[resource_name] = record["record_fields"][key]
if resource_deltas:
level_record["resource_deltas"] = resource_deltas
if len(level_record) > 1:
levels.append(level_record)
record["levels"] = levels
records.append(record)
defined_ids = [record["id"] for record in records]
record_columns = sorted(
{
key
for record in records
for key in record.get("record_fields", {})
},
key=lambda key: tuple(int(part, 0) for part in key.split("/")),
)
return records, {
"schema": "condition-definitions",
"record_span": CONDITION_RECORD_SPAN,
"level_count": CONDITION_LEVEL_COUNT,
"condition_columns": {
str(index): name for index, name in CONDITION_COLUMNS.items()
},
"defined_condition_ids": defined_ids,
"reserved_condition_ids": [
condition_id
for condition_id in range(1, CONDITION_RECORD_SPAN)
if condition_id not in defined_ids
],
"level_name_table": {
"base": f"0x{CONDITION_LEVEL_NAME_BASE:x}",
"stride": CONDITION_LEVEL_COUNT,
},
"record_field_columns": record_columns,
"static_write_count": static_write_count,
"classified_static_write_count": classified_writes,
"recovery_protocol": extract_recovery_protocol(
sys4load.load(resolve("RECOVER"))
),
}
def extract_character_profiles(scr): def extract_character_profiles(scr):
"""Extract CIINIT's profile-id keyed character-information registry.""" """Extract CIINIT's profile-id keyed character-information registry."""
records = [] records = []
@@ -2094,6 +2422,12 @@ def write_data_index(data_dir: Path) -> None:
"name-keyed convenience view. Complete footer copies expose their row values there while", "name-keyed convenience view. Complete footer copies expose their row values there while",
"raw keys and footer metadata remain intact as bytecode provenance.", "raw keys and footer metadata remain intact as bytecode provenance.",
"", "",
"ILINIT's dedicated condition schema exposes thirteen authored condition ids in the",
"reserved 30-by-5 layout. Each record keeps its raw scalar and row-table cells while",
"joining level names, durations, eleven-stat deltas, three-resource deltas, boss/recovery",
"policies, and icon ids. Top-level `recovery_protocol` validates RECOVER.BIN and links",
"current levels, equipment/passive baselines, remaining turns, and full resource restore.",
"",
"Mixed-mode tables preserve the sparse selector id, branch offset, condition strings,", "Mixed-mode tables preserve the sparse selector id, branch offset, condition strings,",
"scalar fields, cells within preallocated buffers, and length-prefixed footer arrays.", "scalar fields, cells within preallocated buffers, and length-prefixed footer arrays.",
"STINIT additionally joins confirmed parallel buffers into per-slot `object_placements`", "STINIT additionally joins confirmed parallel buffers into per-slot `object_placements`",
@@ -2165,6 +2499,8 @@ def main() -> int:
extractor = extract_character_profiles extractor = extract_character_profiles
elif mode == "name" and name == "MAINIT": elif mode == "name" and name == "MAINIT":
extractor = extract_magic_actions extractor = extract_magic_actions
elif mode == "name" and name == "ILINIT":
extractor = extract_condition_definitions
recs, meta = extractor(scr) recs, meta = extractor(scr)
if mode == "name" and name in MESSAGE_TABLES: if mode == "name" and name in MESSAGE_TABLES:
message_name = MESSAGE_TABLES[name] message_name = MESSAGE_TABLES[name]

View File

@@ -107,11 +107,22 @@ def profile_columns(data: dict) -> list[dict]:
"semantic_name": field_semantics.get(key), "semantic_name": field_semantics.get(key),
}, value, record) }, value, record)
for address, value in record.get("string_fields", {}).items(): for address, value in record.get("string_fields", {}).items():
base = int(address, 16) parts = address.split("/")
base = int(parts[0], 16)
if len(parts) == 1:
stride = column = None
key = f"0x{base:x}" key = f"0x{base:x}"
kind = "string-field"
elif len(parts) == 3:
stride = int(parts[1])
column = int(parts[2])
key = f"0x{base:x}/{stride}/{column}"
kind = "string-record-column"
else:
raise ValueError(f"bad string-field key: {address}")
add(key, { add(key, {
"key": key, "kind": "string-field", "base": key, "key": key, "kind": kind, "base": f"0x{base:x}",
"stride": None, "column": None, "stride": stride, "column": column,
"semantic_name": field_semantics.get(key), "semantic_name": field_semantics.get(key),
}, value, record) }, value, record)
for key, value in record.get("record_fields", {}).items(): for key, value in record.get("record_fields", {}).items():

View File

@@ -911,6 +911,97 @@ def test_message_join() -> None:
) )
def test_condition_definitions() -> None:
scripts = paths.scripts()
script = sys4load.load(scripts["ILINIT.BIN"])
check(
extract_init.detect_mode(script) == "name",
"ILINIT remains compatible with name-mode auto-detection",
)
records, meta = extract_init.extract_condition_definitions(script)
by_id = {record["id"]: record for record in records}
check(
list(by_id) == [*range(1, 12), 13, 14],
"ILINIT extracts all thirteen authored condition ids",
)
check(
meta["record_span"] == 30
and meta["level_count"] == 5
and meta["reserved_condition_ids"] == [12, *range(15, 30)],
"ILINIT exposes its complete 30-by-5 reserved layout",
)
check(
meta["static_write_count"] == 228
and meta["classified_static_write_count"] == 228,
"ILINIT classifies every static integer write",
)
check(
by_id[1]["name"] == "即死"
and by_id[2]["name"] == "HP吸"
and by_id[14]["condition"] == "exaltation",
"condition records preserve authored names and canonical ids",
)
check(
by_id[2]["string_fields"]["0x25fa/5/0"] == "吸1"
and by_id[2]["string_fields"]["0x25fa/5/4"] == "吸5",
"condition level names retain raw base/stride/column provenance",
)
check(
[level["duration_turns"] for level in by_id[6]["levels"]]
== [2, 3, 4, 5, 6]
and by_id[6]["fields"]["0xaacb4"] == 1
and by_id[6]["fields"]["0xaacd2"] == 10,
"charm exposes level durations, RECOVER policy, and icon id",
)
check(
by_id[5]["levels"][4]["stat_deltas"]["accuracy"] == -25
and by_id[14]["levels"][4]["stat_deltas"]["physical_attack"] == 10
and by_id[14]["levels"][4]["stat_deltas"]["physical_defense"] == -10,
"curse and exaltation expose their five-level stat matrices",
)
check(
by_id[9]["levels"][4]["resource_deltas"]["hp"] == -5
and by_id[13]["levels"][4]["resource_deltas"]["hp"] == 5,
"poison and regeneration expose opposing periodic HP deltas",
)
recovery = meta["recovery_protocol"]
check(
recovery["resource_restore"]["source_columns"]
== ["max_hp", "max_sp", "max_fs"]
and recovery["resource_restore"]["destination_columns"]
== ["current_hp", "current_sp", "current_fs"],
"RECOVER joins max-stat columns to current HP/SP/FS",
)
check(
recovery["condition_reset"]["current_level_table"] == "0x52383"
and recovery["condition_reset"]["baseline_level_table"] == "0x52f3b"
and recovery["condition_reset"]["remaining_turns_table"] == "0x5295f"
and recovery["condition_reset"]["recovery_policy_table"] == "0xaacb4",
"RECOVER publishes the four-table condition reset protocol",
)
semantics = extract_init.field_semantics(records)
check(
semantics["0xaacf0/5/4"]
== "condition_duration_turns_by_level.level_5_turns"
and semantics["0x25fa/5/4"] == "condition_level_names.level_5"
and semantics["0xaad86/55/46"]
== "condition_stat_deltas.level_5_physical_attack"
and semantics["0xab3f8/15/12"]
== "condition_resource_deltas.level_5_hp",
"ILINIT raw cells join to level-aware condition semantics",
)
extract_init.attach_semantic_fields(records, semantics)
check(
by_id[6]["semantic_fields"]["condition_cleared_by_recover"] == 1
and by_id[14]["semantic_fields"][
"condition_stat_deltas.level_5_physical_attack"
] == 10,
"condition definitions retain raw addresses beside the semantic view",
)
def test_field_semantics() -> None: def test_field_semantics() -> None:
scripts = paths.scripts() scripts = paths.scripts()
items, _ = extract_init.extract_name(sys4load.load(scripts["ITINIT.BIN"])) items, _ = extract_init.extract_name(sys4load.load(scripts["ITINIT.BIN"]))
@@ -1028,6 +1119,7 @@ if __name__ == "__main__":
test_real_message_tables() test_real_message_tables()
test_message_infrastructure() test_message_infrastructure()
test_message_join() test_message_join()
test_condition_definitions()
test_field_semantics() test_field_semantics()
if FAILS: if FAILS:
raise SystemExit(f"{len(FAILS)} failed checks") raise SystemExit(f"{len(FAILS)} failed checks")

View File

@@ -15,9 +15,11 @@ def main() -> int:
"field_semantics": { "field_semantics": {
"0x10": "test_parallel", "0x10": "test_parallel",
"0x30/3/0": "test_record.zero", "0x30/3/0": "test_record.zero",
"0x40/5/4": "test_names.level_5",
}, },
"records": [ "records": [
{"id": 1, "name": "one", "fields": {"0x10": 2, "0x20": 0}, {"id": 1, "name": "one", "fields": {"0x10": 2, "0x20": 0},
"string_fields": {"0x40/5/4": "One5"},
"record_fields": {"0x30/3/0": 9}, "record_fields": {"0x30/3/0": 9},
"message": {"title": "One", "description": "First", "message": {"title": "One", "description": "First",
"furigana": [{"line": 0, "text": "One", "reading": "one"}]}}, "furigana": [{"line": 0, "text": "One", "reading": "one"}]}},
@@ -41,6 +43,10 @@ def main() -> int:
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/0"]["semantic_name"] == "test_record.zero"
assert rows["0x30/3/2"]["column"] == 2 assert rows["0x30/3/2"]["column"] == 2
assert rows["0x40/5/4"]["kind"] == "string-record-column"
assert rows["0x40/5/4"]["stride"] == 5
assert rows["0x40/5/4"]["column"] == 4
assert rows["0x40/5/4"]["semantic_name"] == "test_names.level_5"
mixed_fixture = { mixed_fixture = {
"table": "MIXED", "table": "MIXED",

View File

@@ -1364,6 +1364,156 @@ source = "investigation"
confidence = "high" confidence = "high"
depends_on = ["0x5231f"] depends_on = ["0x5231f"]
[[global]]
address = "0x25fa"
name = "condition_level_names"
category = "string-table"
type = "string[30][5]"
columns = { "0" = "level_1", "1" = "level_2", "2" = "level_3", "3" = "level_4", "4" = "level_5" }
value_domain = "ILINIT-authored display strings for condition ids 1..11, 13, and 14"
usage = "ILINIT's row-major five-name matrix. Runtime condition UI selects the row by condition id and the column by current level minus one; id 1 has only the unnumbered instant-death label, id 12 and ids 15..29 are reserved."
source = "investigation"
confidence = "high"
depends_on = ["0x52383"]
[[global]]
address = "0x52383"
name = "entity_condition_levels"
category = "data-table"
type = "int[50][30]"
columns = { "1" = "instant_death", "2" = "hp_drain", "3" = "sp_drain", "4" = "fs_drain", "5" = "curse", "6" = "charm", "7" = "confusion", "8" = "paralysis", "9" = "poison", "10" = "water_flow", "11" = "fear", "12" = "reserved", "13" = "regeneration", "14" = "exaltation" }
value_domain = "condition level 0..5; zero means inactive"
usage = "Per-runtime-entity current condition levels. ADDILLSUB applies signed level deltas, clamps the minimum to entity_condition_baseline_levels and the maximum to 5, and assigns condition duration. CALCREVISE, FIELD, combat, and UI consumers index the same ILINIT-defined 30-column condition id space."
source = "investigation"
confidence = "high"
depends_on = ["0x52f3b", "0x5295f", "0xaacf0"]
[[global]]
address = "0x5295f"
name = "entity_condition_remaining_turns"
category = "data-table"
type = "int[50][30]"
columns = { "1" = "instant_death", "2" = "hp_drain", "3" = "sp_drain", "4" = "fs_drain", "5" = "curse", "6" = "charm", "7" = "confusion", "8" = "paralysis", "9" = "poison", "10" = "water_flow", "11" = "fear", "12" = "reserved", "13" = "regeneration", "14" = "exaltation" }
value_domain = "-1 permanent/baseline, 0 inactive, positive remaining turns"
usage = "Per-runtime-entity duration counters paired with entity_condition_levels. FIELD decrements positive counters and removes the condition when a counter reaches zero; DRAWENP displays negative values as infinity. RECOVER writes -1 when a recoverable condition retains an equipment/passive baseline and zero when it clears completely."
source = "investigation"
confidence = "high"
depends_on = ["0x52383", "0x52f3b"]
[[global]]
address = "0x52f3b"
name = "entity_condition_baseline_levels"
category = "data-table"
type = "int[50][30]"
columns = { "1" = "instant_death", "2" = "hp_drain", "3" = "sp_drain", "4" = "fs_drain", "5" = "curse", "6" = "charm", "7" = "confusion", "8" = "paralysis", "9" = "poison", "10" = "water_flow", "11" = "fear", "12" = "reserved", "13" = "regeneration", "14" = "exaltation" }
value_domain = "equipment/passive condition level 0..5"
usage = "Per-runtime-entity minimum condition levels supplied by equipment and passive effects. SETCH/SETEN populate the row from item_equipped_status_levels, ADDILLSUB cannot reduce current levels below it, CALCREVISE separates transient current levels from this baseline, and RECOVER restores eligible conditions to it."
source = "investigation"
confidence = "high"
depends_on = ["0x52383", "0x97c29"]
[[global]]
address = "0xaac76"
name = "current_condition_id"
category = "index-pointer"
type = "int"
value_domain = "condition id 1..14 in shipped scripts"
usage = "Shared condition selector consumed by CALCILL, ADDILL, ADDILLSUB, DISARM, FIELD, and related condition handlers. It indexes the 30-column runtime condition rows and ILINIT definition arrays."
source = "investigation"
confidence = "high"
depends_on = ["0x52383"]
[[global]]
address = "0xaac77"
name = "current_condition_delta_levels"
category = "counter"
type = "int"
value_domain = "signed condition-level adjustment, commonly -5..5"
usage = "Shared signed level delta passed to ADDILL/ADDILLSUB for current_condition_id. Positive values apply or strengthen a condition; negative values weaken/remove it subject to the equipment/passive baseline floor."
source = "investigation"
confidence = "high"
depends_on = ["0xaac76", "0x52383", "0x52f3b"]
[[global]]
address = "0xaac78"
name = "condition_effectiveness_element_ids"
category = "data-table"
type = "int[30]"
value_domain = "element id 11..17 for shipped condition definitions"
usage = "ILINIT condition metadata. CALCILL uses the selected condition's value as the element-table column for its application-chance effectiveness calculation."
source = "investigation"
confidence = "high"
depends_on = ["0xaac76"]
[[global]]
address = "0xaac96"
name = "condition_can_affect_bosses"
category = "data-table"
type = "int[30]"
value_domain = "boolean; nonzero for HP/SP/FS drain, regeneration, and exaltation"
usage = "ILINIT policy vector. CALCILL rejects a condition against a boss-class target when this cell is zero; nonzero cells bypass that boss immunity gate."
source = "investigation"
confidence = "high"
depends_on = ["0xaac76"]
[[global]]
address = "0xaacb4"
name = "condition_cleared_by_recover"
category = "data-table"
type = "int[30]"
value_domain = "boolean; shipped nonzero ids are 6..11"
usage = "ILINIT recovery-policy vector. RECOVER and DISARM use it to identify charm, confusion, paralysis, poison, water-flow, and fear as removable conditions; RECOVER resets each eligible current level to its equipment/passive baseline."
source = "investigation"
confidence = "high"
depends_on = ["0x52383", "0x52f3b", "0x5295f"]
[[global]]
address = "0xaacd2"
name = "condition_icon_ids"
category = "data-table"
type = "int[30]"
value_domain = "condition icon id 1..10; zero means no icon"
usage = "ILINIT display metadata. DRAWENP and FIELD use these ids to deduplicate active-condition indicators and select the corresponding status-icon atlas entry."
source = "investigation"
confidence = "high"
depends_on = ["0x52383"]
[[global]]
address = "0xaacf0"
name = "condition_duration_turns_by_level"
category = "data-table"
type = "int[30][5]"
columns = { "0" = "level_1_turns", "1" = "level_2_turns", "2" = "level_3_turns", "3" = "level_4_turns", "4" = "level_5_turns" }
value_domain = "-1 permanent or a positive turn count"
usage = "ILINIT's five-column duration row for each condition. ADDILLSUB indexes it by the resulting condition level minus one and stores the value in entity_condition_remaining_turns."
source = "investigation"
confidence = "high"
depends_on = ["0x5295f", "0x52383"]
[[global]]
address = "0xaad86"
name = "condition_stat_deltas"
category = "data-table"
type = "int[30][55]"
columns = { "0" = "level_1_accuracy", "1" = "level_1_evasion", "2" = "level_1_physical_attack", "3" = "level_1_physical_defense", "4" = "level_1_magic_attack", "5" = "level_1_magic_defense", "6" = "level_1_speed", "7" = "level_1_luck", "8" = "level_1_critical_chance", "9" = "level_1_capture_power", "10" = "level_1_movement", "11" = "level_2_accuracy", "12" = "level_2_evasion", "13" = "level_2_physical_attack", "14" = "level_2_physical_defense", "15" = "level_2_magic_attack", "16" = "level_2_magic_defense", "17" = "level_2_speed", "18" = "level_2_luck", "19" = "level_2_critical_chance", "20" = "level_2_capture_power", "21" = "level_2_movement", "22" = "level_3_accuracy", "23" = "level_3_evasion", "24" = "level_3_physical_attack", "25" = "level_3_physical_defense", "26" = "level_3_magic_attack", "27" = "level_3_magic_defense", "28" = "level_3_speed", "29" = "level_3_luck", "30" = "level_3_critical_chance", "31" = "level_3_capture_power", "32" = "level_3_movement", "33" = "level_4_accuracy", "34" = "level_4_evasion", "35" = "level_4_physical_attack", "36" = "level_4_physical_defense", "37" = "level_4_magic_attack", "38" = "level_4_magic_defense", "39" = "level_4_speed", "40" = "level_4_luck", "41" = "level_4_critical_chance", "42" = "level_4_capture_power", "43" = "level_4_movement", "44" = "level_5_accuracy", "45" = "level_5_evasion", "46" = "level_5_physical_attack", "47" = "level_5_physical_defense", "48" = "level_5_magic_attack", "49" = "level_5_magic_defense", "50" = "level_5_speed", "51" = "level_5_luck", "52" = "level_5_critical_chance", "53" = "level_5_capture_power", "54" = "level_5_movement" }
value_domain = "signed additive deltas"
usage = "ILINIT's five-level by eleven-stat matrix for each condition. CALCREVISE indexes (condition level - 1) * 11 + stat column and adds the result to entity_effective_stats columns 0..10. Shipped rows populate curse, confusion, paralysis, and exaltation."
source = "investigation"
confidence = "high"
depends_on = ["0x52383", "0x4e11b"]
[[global]]
address = "0xab3f8"
name = "condition_resource_deltas"
category = "data-table"
type = "int[30][15]"
columns = { "0" = "level_1_hp", "1" = "level_1_sp", "2" = "level_1_fs", "3" = "level_2_hp", "4" = "level_2_sp", "5" = "level_2_fs", "6" = "level_3_hp", "7" = "level_3_sp", "8" = "level_3_fs", "9" = "level_4_hp", "10" = "level_4_sp", "11" = "level_4_fs", "12" = "level_5_hp", "13" = "level_5_sp", "14" = "level_5_fs" }
value_domain = "signed per-turn HP/SP/FS deltas"
usage = "ILINIT's five-level by three-resource matrix for each condition. FIELD applies the active row each turn; CALCDMG also consumes the HP/SP/FS drain condition rows during battle. Positive values restore/drain to the acting side as defined by the caller, while negative values are periodic damage or loss."
source = "investigation"
confidence = "high"
depends_on = ["0x52383", "0x4e085"]
[[global]] [[global]]
address = "0x53e13" address = "0x53e13"
name = "entity_carried_item_ids" name = "entity_carried_item_ids"