From 8a1c11ebed7c884c8a350a33304cd32eb3421ec0 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 18:45:53 -0400 Subject: [PATCH] Decode ILINIT condition and RECOVER schemas --- docs/global-reference.md | 27 +-- docs/phase-a-slice-plan.md | 29 +++ docs/tools-reference.md | 10 +- tools/extract_init.py | 338 ++++++++++++++++++++++++++++++- tools/init_table_profile.py | 19 +- tools/test_extract_init.py | 92 +++++++++ tools/test_init_table_profile.py | 6 + vm-map/globals.toml | 150 ++++++++++++++ 8 files changed, 650 insertions(+), 21 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index b9870ba..4e9e56e 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # 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 @@ -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. | | `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. | +| `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. | | `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. | @@ -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. | | `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. | +| `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. | | `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. | @@ -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. | | `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. | +| `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. | | `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. | @@ -287,10 +298,6 @@ | `0x665d8` | — | low | auto-shape | array | | `0x8fb41` | — | 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 | | `0xaba43` | — | 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. | | `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. | +| `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. | | `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). | @@ -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. | | `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. | +| `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. | | `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. | @@ -3702,7 +3711,6 @@ | `0x1fba` | — | high | auto-shape | item-desc-table | | `0x23a2` | — | high | auto-shape | skill-name-table | | `0x24ce` | — | high | auto-shape | skill-desc-table | -| `0x25fa` | — | med | auto-shape | record-table[stride 5] | | `0x2ba5` | — | med | auto-shape | record-table[stride 6] | | `0x3239` | — | med | auto-shape | record-table[stride 3] | | `0x32f0` | — | med | auto-shape | current-entity-index? | @@ -3711,9 +3719,6 @@ | `0x7e54` | — | med | auto-shape | record-table[stride 50] | | `0x341ab` | — | med | auto-shape | record-table[stride 53] | | `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] | | `0x53edd` | — | low | auto-shape | index/counter? | | `0x53ef5` | — | low | auto-shape | index/counter? | @@ -5030,10 +5035,6 @@ | `0x6638b` | — | med | auto-shape | record-table[stride 15] | | `0x81c96` | — | 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] | | `0xab7d6` | — | med | auto-shape | record-table[stride 11] | | `0xab8b2` | — | med | auto-shape | record-table[stride 7] | diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index d0224c5..af15bc2 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -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. **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. diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 738fe5e..6075389 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -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 [OUTNAME]` | `.BIN` → `build/data/.json` | | `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules / dispatch / banked shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, statically evaluate direct and negative-value writes, and separate parallel `fields` from linked row-major `record_fields`; when flat global ranges overlap, a parallel base established by other records wins over a coincidental row-table cell calculation. Dedicated name-mode schemas recover VIINIT's pre-title glossary prerequisites and CIINIT's profile name, backing-unit, optional portrait, and reserved placement columns. Mixed tables recover selector-dispatched records, condition strings, scalars, preallocated buffer cells, consumer-confirmed strides, and length-prefixed footer arrays; STINIT additionally assembles confirmed buffers into `object_placements` and `enemy_spawns`, joins object type names and available descriptions from OBINIT, and exposes consumer-proven tagged payloads (teleport, treasure, card, initial faction, non-triggering hazard/barrier faction, and OBINIT-gated initial object state), preserves engine-dead type-27 writes under `ignored_payload_fields`, and exposes reinforcement schedules and first-clear-only enemy gates. Rules mode decodes CCINIT's source-ordered class-change predicates and output accumulators, joining EBINIT unit names, SKINIT skill names, titles, named stat bonuses, cost deltas, and applied-state slots. Dispatch mode decodes SCINIT's paired decision-to-packed-scene and authored-chapter arrays, preserving source-order overwrites, resolving SYS4INI script names, and cross-checking chapter tags against decoded SCJUMP paths. Banked mode decodes RTINIT's twenty parallel 1000-by-20 movement/battle routine banks, retains all assignments and overwrites, assembles final per-slot steps, resolves provider selectors to RTN_M/RTN_B scripts, and applies selector-scoped schemas where proven. Current schemas cover all nineteen used providers, RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061, including progress-only, randomized roaming, object-slot, coordinate, enemy/ally, treasure and Magic Pillar search, Healing Feather, waypoint, faction-terrain, retreat, reachable normal-attack routing, immediate offensive target/action selection, and immediate allied healing. Parameterless behavior schemas publish an empty `parameter_fields` mapping while still joining `provider_behavior`, target selection, action selection, and completion rules. Provider defaults such as M004's unwritten slot 0, M010's zero/HP resource index, and M013's any-foreign-faction filter are projected explicitly and counted separately from populated source cells. Authored cells proven unread by M001/M008 remain under per-step `ignored_movement_parameters` and contribute to top-level `ignored_movement_parameter_count`; they are not silently discarded or assigned invented meanings. Raw provider banks remain intact, and top-level `movement_provider_parameter_schemas` documents the projection. ITINIT, SKINIT, VIINIT, EBINIT, and CIINIT join their matching MES text. Top-level `field_semantics` maps raw keys to canonical global/column names, while each record's `semantic_fields` is the generated name-keyed join; complete footer copies map to a row and expose its values without the raw provenance wrapper. Raw keys and unresolved tagged payload cells remain provenance. OUTNAME accepts a stem or one `.json` suffix and rejects paths. Refreshes the generated data index. | `extract_init.py [OUTNAME] [--mode …]` | `
.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, `build/callscript-names.json` for RTINIT/SCINIT, `build/scjump-decisions.json` for SCINIT, matching `.BIN` when supported, and `build/globals.json` → `build/data/.json`, `build/data/README.md` | | `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. Name tables report message coverage for title/description, summary/strategy, biography, and description-only layouts; rule tables summarize covered units, titled and level-independent rules, threshold/slot distributions, and skill awards; dispatch tables summarize assignments, overwrites, script-name resolution, and SCJUMP chapter agreement; banked tables summarize populated/reserved banks, movement/battle steps and providers, selector-specific semantic coverage, and overwrite conflicts. `--message-query REGEX` searches every supported message field beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py
[--build] [--limit N] [--message-query REGEX]` | `build/data/
.json` + corpus → stdout; with `--build`, `build/data/
-field-profile.{json,md}` | -| `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based, mixed selector-dispatched, CCINIT rule, SCINIT dispatch, and RTINIT banked extraction; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked profiling. | run each directly | — | +| `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}` | `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 -eleven populated magic-action rows within a reserved 30-cell layout. Matching message joins are +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, 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 intentionally sparse across unrelated unit definitions; MAINIT intentionally lacks MAMES bodies for growth ritual ids 10 and 11. diff --git a/tools/extract_init.py b/tools/extract_init.py index ec2c9e5..4d42b1f 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """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, 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 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": value}} or, for footer tables, {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. @@ -529,6 +532,57 @@ VOCABULARY_RECORD_TABLE_BASE = 0x15A2A9 VOCABULARY_RECORD_STRIDE = 3 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: 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): """Extract CIINIT's profile-id keyed character-information registry.""" 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", "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,", "scalar fields, cells within preallocated buffers, and length-prefixed footer arrays.", "STINIT additionally joins confirmed parallel buffers into per-slot `object_placements`", @@ -2165,6 +2499,8 @@ def main() -> int: extractor = extract_character_profiles elif mode == "name" and name == "MAINIT": extractor = extract_magic_actions + elif mode == "name" and name == "ILINIT": + extractor = extract_condition_definitions recs, meta = extractor(scr) if mode == "name" and name in MESSAGE_TABLES: message_name = MESSAGE_TABLES[name] diff --git a/tools/init_table_profile.py b/tools/init_table_profile.py index a58f51e..37ce7fa 100644 --- a/tools/init_table_profile.py +++ b/tools/init_table_profile.py @@ -107,11 +107,22 @@ def profile_columns(data: dict) -> list[dict]: "semantic_name": field_semantics.get(key), }, value, record) for address, value in record.get("string_fields", {}).items(): - base = int(address, 16) - key = f"0x{base:x}" + parts = address.split("/") + base = int(parts[0], 16) + if len(parts) == 1: + stride = column = None + 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, { - "key": key, "kind": "string-field", "base": key, - "stride": None, "column": None, + "key": key, "kind": kind, "base": f"0x{base:x}", + "stride": stride, "column": column, "semantic_name": field_semantics.get(key), }, value, record) for key, value in record.get("record_fields", {}).items(): diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index d1e6ba5..1d7b587 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -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"] == "HP吸1" + and by_id[2]["string_fields"]["0x25fa/5/4"] == "HP吸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: scripts = paths.scripts() items, _ = extract_init.extract_name(sys4load.load(scripts["ITINIT.BIN"])) @@ -1028,6 +1119,7 @@ if __name__ == "__main__": test_real_message_tables() test_message_infrastructure() test_message_join() + test_condition_definitions() test_field_semantics() if FAILS: raise SystemExit(f"{len(FAILS)} failed checks") diff --git a/tools/test_init_table_profile.py b/tools/test_init_table_profile.py index bc8cfbd..a4e6600 100644 --- a/tools/test_init_table_profile.py +++ b/tools/test_init_table_profile.py @@ -15,9 +15,11 @@ def main() -> int: "field_semantics": { "0x10": "test_parallel", "0x30/3/0": "test_record.zero", + "0x40/5/4": "test_names.level_5", }, "records": [ {"id": 1, "name": "one", "fields": {"0x10": 2, "0x20": 0}, + "string_fields": {"0x40/5/4": "One5"}, "record_fields": {"0x30/3/0": 9}, "message": {"title": "One", "description": "First", "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"]["semantic_name"] == "test_record.zero" 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 = { "table": "MIXED", diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 416fee8..8c0331b 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -1364,6 +1364,156 @@ source = "investigation" confidence = "high" 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]] address = "0x53e13" name = "entity_carried_item_ids"