diff --git a/docs/global-reference.md b/docs/global-reference.md index 83e879a..612e1f5 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5023 globals (94 curated, 4929 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5023 globals (119 curated, 4904 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -29,6 +29,19 @@ | `0x2e49` | character_voice_suppressed | high | investigation | Base of the per-character voice enable/suppress settings. INITCONFIG zero-fills all 13 cells and registers each with the shared profile service; LOADCONFIG restores them. CONFIG indexes the table to preview a character voice and write 0/1. ROOM reads cell 0 before assigning its selected greeter's greeting/farewell voice ids, so the port's former scalar interpretation of zero-int-range (writing 13 into the base cell) suppressed those voices on every natural boot. This names the script-visible setting array without choosing a persistence backend for op 0x1a2/0x1a3. | | `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. | | `0x67a0` | party_slot_character_id | high | investigation | Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster. | +| `0x70f0e` | unit_sort_key | high | investigation | EBINIT ordering key populated for 248 units. CHMENU, EXILE, SALLY, INFOEN, and SUMMON pass this array to op 0x12f's stable index sort or combine it with a unit id to build menu ordering keys, mirroring item_sort_key and skill_sort_key. | +| `0x716de` | unit_icon_id | high | investigation | EBINIT icon selector populated for all 277 units. READICON returns it for unit ids; CHMENU, DRAWCHP, EXILE, SALLY, and SUMMON convert the value into icon-atlas page and cell coordinates. | +| `0x71ac6` | unit_species_category | med | investigation | Broad EBINIT species/allegiance category for 198 units: value 1 covers humans and allied heroines, 2 covers demons including Emilio and Lily as well as many demonic monsters, and 3 covers other monsters, dragons, and elementals. CHMENU uses it for equipment restrictions, CALCDMG for category-sensitive effects, and INFOCH/INFOEN for display. | +| `0x71eae` | unit_sex_category | high | investigation | EBINIT sex category for 251 units. Named character records establish the three values; CHMENU tests it against item_sex_restriction_mask, while INFOCH and INFOEN use the same category in unit presentation. | +| `0x77c6e` | unit_defense_element | high | investigation | EBINIT defensive affinity for 250 units. DRAWENP renders it through the same twelve-value defense-attribute vocabulary as item_defense_element; CALCBTPARAM, BTRTN, and route scripts consume it during battle resolution. | +| `0x78056` | unit_default_attack_item_id | high | investigation | EBINIT default attack for 250 units. Every populated value cross-resolves to ITINIT's innate-attack records; SELACT, BTL, CALCBTPARAM, CALCDMG, and CHMENU use it when no equipped weapon overrides the unit's natural attack. | +| `0x78826` | unit_weapon_item_category | high | investigation | EBINIT weapon/equipment-family restriction for 179 units. CHMENU compares an item's item_category directly with this value when deciding whether the selected unit can equip it; Lily's forms store -1 for unrestricted handling. | +| `0x78ff6` | unit_starting_skill_ids | high | investigation | EBINIT four-slot starting skill record. The shipped table populates the first three columns. SETEN, UNITECH, and SALLY copy the row into each runtime unit's skill list; GAMESTART, FORT, FIELD, and DRAWENP inspect the same ids, all of which cross-resolve to SKINIT. | +| `0x79f96` | unit_deployment_cost_base | high | investigation | Base deployment/command cost for 204 units. SETCH, SETEN, and ADDEXP copy it plus the per-unit adjustment at 0x57357 into runtime cost 0x5f0bb; SALLY checks the prospective total against party capacity, while FIELD/READY add it to and REMOVECH subtracts it from the deployed-cost aggregate. | +| `0x7a37e` | unit_starting_level | high | investigation | EBINIT initial level for 242 units. Lily's three forms store levels 1, 20, and 40. SETEN and SALLY copy or compare it while constructing a runtime unit, and UNITECH uses it when synchronizing form and character state. | +| `0x7a766` | unit_level_cap | high | investigation | EBINIT maximum level for 243 units. ADDEXP permits level growth only while the runtime level is below this field, and SETEN uses it as the default upper bound when no scenario-specific enemy cap is supplied. | +| `0x7af36` | unit_base_stats | high | investigation | EBINIT fourteen-column base-stat record copied wholesale into each runtime unit by SETEN, UNITECH, and SALLY. Its layout matches item_stat_modifiers: columns 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 10 movement, 11 max HP, 12 max SP, and 13 max FS; accuracy/evasion and other unpopulated columns begin at zero. | +| `0x7e5e6` | unit_stat_growth_rates | high | investigation | EBINIT per-level growth record with the same fourteen-column layout as unit_base_stats. ADDEXP adds each rate to a fractional accumulator, divides by 100 to award whole stat points, and retains the remainder; SETEN and UNITECH apply the same rates when materializing units above their starting level. | | `0x8c879` | item_sort_key | high | investigation | ITINIT field for all 287 populated item ids. CHMENU, IMPROVE, and INFOIT pass this array as the primary key to op 0x12f's stable index sort, establishing it as the catalog/display ordering key. The runtime lookup base is one cell before ITINIT's first write because item ids are one-based. | | `0x8cc61` | item_random_tier | high | investigation | ITINIT field for all 287 items. ADDRANDOMITEM and LOSTRANDOMITEM bucket eligible item ids by this value before choosing a random gain/loss; IMPROVE compares it with the current progression rank. This is the random-item availability/rarity tier, distinct from item_category. | | `0x8d049` | item_category | high | investigation | ITINIT field for all 287 items and ITMES's top-level behavior dispatch. Observed groups: 0 innate attacks, 1 key/story items, 2 consumables, 4 stat stones, 8 synthesis materials, 9 coins, 10..17 weapon families, 19 boots, 20 armor, 21 shields, 22 accessories, and 23 capture ropes. | @@ -39,8 +52,8 @@ | `0x8e3d1` | item_defense_element | high | investigation | Populated for the 13 shields. Values match descriptions such as physical, universal, holy, dark, spirit, and divinity defense; DRAWTIP renders them through AFINIT's defense string table at GStr[0x26a4 + value]. | | `0x8e7b9` | item_character_whitelist | high | investigation | Sparse ITINIT row-major table with stride 5. CHMENU rejects an item when column 0 is populated and the selected party slot's character id is absent from the row. Unique accessories 471/472 allow one character each, while crossover accessories 480..485 allow character ids 2, 3, and 4. Unused trailing columns remain zero. | | `0x8ff29` | item_sex_restriction_mask | med | investigation | Sparse ITINIT equipment restriction. CHMENU tests this mask against EBINIT field 0x71eae, whose values partition male, female, and sexless units; both populated items carry bit 2 and are therefore female-only. | -| `0x906f9` | item_attack_status_levels | high | investigation | Sparse ITINIT row-major table consumed by USEITEM and CALCILL when applying an item's attack effects. Confirmed condition columns from item descriptions are 2 HP drain, 3 SP drain, 4 FS drain, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear. | -| `0x97c29` | item_equipped_status_levels | high | investigation | Sparse ITINIT row-major table added to a unit's 30-column condition state by CALCREVISE. Item descriptions identify populated columns 9 poison, 11 fear, 13 regeneration, and 14 exaltation; these are passive equipped effects, distinct from item_attack_status_levels. | +| `0x906f9` | item_status_delta_levels | high | investigation | Sparse ITINIT row-major table consumed by USEITEM and CALCILL when applying an item's effects. Positive values inflict or drain; -5 removes a condition (for example paralysis-removal item 108 stores -5 in column 8). Confirmed columns are 2 HP drain, 3 SP drain, 4 FS drain, 5 curse, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear. | +| `0x97c29` | item_equipped_status_levels | high | investigation | Sparse ITINIT row-major table added to a unit's 30-column condition state by CALCREVISE. Item descriptions identify populated columns 9 poison, 11 fear, 13 regeneration, and 14 exaltation; these are passive equipped effects, distinct from item_status_delta_levels. | | `0x9f159` | item_granted_skill_id | high | investigation | Populated for 85 equipment items. Values cross-resolve to SKINIT (for example flying bracelet=1 Flying, transfer bracelet=21 Transfer, thief key=22 Lockpick, ropes=156 Capture Attack); CALCREVISE applies the linked skill and UI scripts display it. | | `0x9f541` | item_stat_modifiers | high | investigation | ITINIT row-major equipment modifiers added directly to the unit's 14-column stat record by CALCREVISE. Descriptions and consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 9 capture power, 10 movement, 11 max HP, 12 max SP, and 13 max FS; column 8 remains unnamed. | | `0xa2bf1` | item_tuning_curve_ids | high | investigation | ITINIT row-major table selecting an equipment-growth curve for each of the ten tunable fields. TUNE, IMPROVE, DRAWTIP, and CALCREVISE combine each nonzero curve id with the item's corresponding tuning level and index the shared curve-value table at 0xab6fa. Columns align with item_stat_modifiers columns 0..9. | @@ -48,6 +61,18 @@ | `0xa62a1` | item_essence_recovery_amount | high | investigation | Sparse ITINIT consumable field. Item 106, Blood Price Healing Hand, describes essence recovery 50 and stores 50 here; USEITEM follows the dedicated essence-recovery path and scales the value before updating the selected unit. | | `0xa6689` | item_weapon_class | high | investigation | Populated for 104 weapons/innate attacks. Stable values identify weapon families (1 unarmed, 2 staff, 3 claw, 4 dagger, 5 sword, 6 chain blade, 7 spear, 8 axe, 9 bow, 11 blade boots; later values are monster/natural attack classes). CALCDMG consumes it. | | `0xa6a71` | item_handler_script_id | high | investigation | ITINIT field for all 287 items. Item menus look up this value and feed it directly to call-script; the packed id 0x319b resolves to ITMES.BIN, the shared per-item behavior/description dispatcher. | +| `0xa6e5a` | skill_sort_key | high | investigation | SKINIT field populated for 129 of 131 skills. ADDEXP, ADDSKILL, CHMENU, INFOIT, and SETCH combine it with skill_category to construct deterministic skill-list ordering keys; higher-level sort operations consume the resulting indices. | +| `0xa6f86` | skill_category | high | investigation | SKINIT category for all 131 skills. The records establish 1 movement/exploration, 2 defense/special attack, 3 utility/passive, 4 combat passive, 5 physical special, 6 offensive magic, and 7 healing magic. Combat and menu scripts dispatch directly on these values. | +| `0xa71de` | skill_min_range_encoded | high | investigation | Populated for the 94 active skills. CALCSCOPE and CALCREVISE subtract one before using it as the lower range bound; SELACT and DRAWTIP use the same encoded bound. | +| `0xa730a` | skill_max_range_encoded | high | investigation | Populated for the same 94 active skills as skill_min_range_encoded. CALCSCOPE subtracts one to obtain the upper bound, exactly matching descriptions such as range 3 -> stored 4 and range 9 -> stored 10. | +| `0xa7436` | skill_attack_element | high | investigation | Populated for 60 elemental attacks and spells. Values match the Japanese descriptions and the same eight-value attack-element enum used by item_attack_element; CALCSCOPE, CALCBTPARAM, and SELACT consume it. | +| `0xa7562` | skill_status_levels | high | investigation | Sparse SKINIT row-major condition table consumed by CALCILL when applying a skill's effects to the target unit. Skill names and descriptions identify columns 1 instant death, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear; the column layout matches item_status_delta_levels. | +| `0xa988a` | skill_icon_id | high | investigation | SKINIT icon id for all 131 skills. CHMENU and INFOIT translate it into the shared icon atlas; the six ids group movement, defense, utility, passive, physical-special, and magic icons. | +| `0xa99b6` | skill_combat_stat_deltas | high | investigation | Sparse SKINIT row-major table applied by CALCBTPARAM for the selected action. Descriptions directly establish columns 0 accuracy, 2 physical attack, 3 physical defense, 4 magic attack, and 9 capture power; columns 1, 5, 6, and 8 share the unit combat-stat layout but need final display-name confirmation. | +| `0xaa56e` | skill_resource_deltas | high | investigation | Sparse SKINIT resource table used throughout skill selection and resolution. Column 0 is HP recovery for the three healing spells; column 1 is the negative SP cost for all 95 active skills, exactly matching each description. Column 2 is unpopulated in the shipped table. | +| `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. | | `0x5` | — | low | auto-shape | array | | `0xd2` | — | low | auto-shape | array | | `0xd7` | — | low | auto-shape | array | @@ -4939,11 +4964,7 @@ | `0x6fb86` | — | med | auto-shape | unit-field | | `0x6ff6e` | — | med | auto-shape | record-table[stride 2] | | `0x7073e` | — | med | auto-shape | record-table[stride 2] | -| `0x70f0e` | — | med | auto-shape | unit-field | | `0x712f6` | — | med | auto-shape | unit-field | -| `0x716de` | — | med | auto-shape | unit-field | -| `0x71ac6` | — | med | auto-shape | unit-field | -| `0x71eae` | — | med | auto-shape | unit-field | | `0x72296` | — | low | auto-shape | unit-field? | | `0x7267e` | — | low | auto-shape | unit-field? | | `0x72a66` | — | low | auto-shape | unit-field? | @@ -4956,18 +4977,9 @@ | `0x770b6` | — | low | auto-shape | unit-field? | | `0x7749e` | — | low | auto-shape | unit-field? | | `0x77886` | — | low | auto-shape | unit-field? | -| `0x77c6e` | — | med | auto-shape | unit-field | -| `0x78056` | — | med | auto-shape | unit-field | | `0x7843e` | — | med | auto-shape | unit-field | -| `0x78826` | — | med | auto-shape | unit-field | | `0x78c0e` | — | low | auto-shape | unit-field? | -| `0x78ff6` | — | med | auto-shape | record-table[stride 4] | -| `0x79f96` | — | med | auto-shape | unit-field | -| `0x7a37e` | — | med | auto-shape | unit-field | -| `0x7a766` | — | med | auto-shape | unit-field | | `0x7ab4e` | — | med | auto-shape | unit-field | -| `0x7af36` | — | med | auto-shape | record-table[stride 14] | -| `0x7e5e6` | — | med | auto-shape | record-table[stride 14] | | `0x81c96` | — | med | auto-shape | record-table[stride 3] | | `0x8284e` | — | med | auto-shape | record-table[stride 3] | | `0x83406` | — | med | auto-shape | unit-field | @@ -4975,19 +4987,7 @@ | `0x8572e` | — | med | auto-shape | record-table[stride 8] | | `0x8766e` | — | low | auto-shape | unit-field? | | `0x8c877` | — | low | auto-shape | index/counter? | -| `0xa6e5a` | — | med | auto-shape | skill-field | -| `0xa6f86` | — | med | auto-shape | skill-field | | `0xa70b2` | — | low | auto-shape | skill-field? | -| `0xa71de` | — | med | auto-shape | skill-field | -| `0xa730a` | — | med | auto-shape | skill-field | -| `0xa7436` | — | low | auto-shape | skill-field? | -| `0xa7562` | — | med | auto-shape | record-table[stride 30] | -| `0xa988a` | — | med | auto-shape | skill-field | -| `0xa99b6` | — | med | auto-shape | record-table[stride 10] | -| `0xaa56e` | — | med | auto-shape | record-table[stride 3] | -| `0xaa8f2` | — | low | auto-shape | skill-field? | -| `0xaaa1e` | — | med | auto-shape | skill-field | -| `0xaab4a` | — | med | auto-shape | skill-field | | `0xaac76` | — | low | auto-shape | index/counter? | | `0xaacf0` | — | med | auto-shape | record-table[stride 5] | | `0xaad86` | — | med | auto-shape | record-table[stride 55] | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index 4bed3db..f9ccac8 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -128,7 +128,7 @@ Labels are prefixed `=` to mark them as inferred aliases. Regenerate the `.asm` Confidence is marked per entry; labels ending `?` are low-confidence guesses. -### INIT field-semantics workflow and ITINIT pilot (2026-07-22) +### INIT field-semantics workflow and initial item/skill/unit mappings (2026-07-22) The old name-mode extractor's boundary rule was wrong for sparse tables: it treated any increasing `global-string` destination as another description. ITINIT begins with 101 consecutive name-only records, @@ -137,7 +137,9 @@ evidence also proves the tables are one-based: scripts look up item names from ` first populated name is written to `0x1bd3`. `extract_init.py` now infers the parallel-array record span from the dominant name-to-description delta (SKINIT 300; ITINIT/EBINIT 1000), recognizes column-zero names inside that span, emits the one-based runtime id, and distinguishes the lookup base from the first written cell. -Corrected counts are **131 skills, 287 items, and 277 units**. +Corrected counts are **131 skills, 287 items, and 277 units**. Name-mode INIT scripts also encode negative +constants as `sub destination, 0, magnitude`; the extractor now evaluates that static form as well as `mov`, +recovering 113 negative item cells, 212 negative skill cells, and 86 negative unit cells. Semantic recovery is an evidence ladder, cheapest and strongest first: @@ -155,23 +157,37 @@ joins are independently human-readable: attack/defense values index AFINIT's Jap granted-skill values resolve to SKINIT, all handler values resolve to ITMES.BIN, and every min/max-range record says `range 2` in its item description. -The apparent 764 additional ITINIT field bases were a structural artifact, not 764 sparse arrays. For each -write, subtracting `item_id * stride` and comparing the destination with corpus-observed `lookup-array-2d` -consumers assigns all 764 writes unambiguously to six row-major tables and 43 populated columns: +The apparent per-record ITINIT field bases were a structural artifact, not hundreds of sparse arrays. For +each write, subtracting `item_id * stride` and comparing the destination with corpus-observed +`lookup-array-2d` consumers assigns all **877** writes (764 positive/direct writes plus 113 recovered negative +writes) unambiguously to six row-major tables and 44 populated columns: | base | stride | populated writes | semantic role | |---|---:|---:|---| | `0x8e7b9` | 5 | 20 | character-id equipment whitelist | -| `0x906f9` | 30 | 36 | attack-inflicted condition levels | +| `0x906f9` | 30 | 47 | signed condition/drain deltas (positive inflicts, `-5` cures) | | `0x97c29` | 30 | 11 | equipped/passive condition levels | -| `0x9f541` | 14 | 301 | additive equipment stat modifiers | +| `0x9f541` | 14 | 403 | signed additive equipment stat modifiers | | `0xa2bf1` | 10 | 379 | per-stat tuning curve ids | | `0xa5301` | 3 | 17 | HP/SP/FS recovery amounts | `extract_init.py` now records these as `record_fields["base/stride/column"]` rather than inventing a -one-off `fields` base for every row. Applying the same rule also exposes 17 linked SKINIT columns and 83 -linked EBINIT columns. This correction reduces the auto map's false INIT-field labels from 12,311 to 1,353; -the raw write data was valid, but its former ownership model was not. +one-off `fields` base for every row. Applying the same rule exposes 18 linked SKINIT columns and 84 linked +EBINIT columns. This correction reduces the auto map's false INIT-field labels from 12,311 to 1,353; the raw +write addresses were valid, but their former ownership model and omission of negative writes were not. + +The first SKINIT pass names the stable catalog and combat surface: sort key, seven-way category, icon and +SKMES handler, encoded minimum/maximum range, attack element, condition strengths, signed combat-stat deltas, +HP recovery/SP cost, proc chance, and battle-animation id. The negative-write fix is essential here: all 95 +active-skill SP costs are stored as `0 - cost`, so the old JSON omitted the cost column entirely. + +The first EBINIT pass names the unit schema shared by setup, menus, and combat: sort key, icon, sex category, +provisional species category, defense element, natural-attack item id, allowed weapon item category, four +starting-skill slots, deployment cost, starting level, level cap, fourteen-column base stats, and matching +per-level stat-growth rates. These joins are structural rather than positional guesses: natural attacks and +skills resolve into ITINIT/SKINIT ids, SETEN/UNITECH/SALLY copy the complete records into runtime unit state, +ADDEXP performs the growth-rate divide/modulo-100 calculation, and SALLY checks deployment cost against the +live party-capacity aggregate. Graphics, AI, reward, and sparse flag fields remain deliberately unnamed. ### The curated registry — `vm-map/globals.toml` (2026-07-07) @@ -217,10 +233,10 @@ are *not* story flags — the miner over-tags them; they are recategorized `unkn The v1 map labels *shapes and tables*; the next increments add *meaning*, cheapest first: -1. **Continue INIT semantics by evidence density.** Resolve ITINIT stat column 8 and the remaining condition - enum columns, then run the same profiler on SKINIT and EBINIT. Add explicit foreign-key joins (item → - skill, unit → attack/skill) once the target table ids are confirmed. Do not infer meaning from column - position alone. +1. **Continue INIT semantics by evidence density.** Resolve ITINIT/SKINIT's remaining stat and condition + columns, then work through EBINIT's graphics, AI, reward, and sparse-flag tables by consumer strength. + Preserve explicit item → skill and unit → attack/skill joins. Do not infer meaning from column position + alone. 2. **Fold in the `*MES` message-table writers** (`ITMES`, `SKMES`, `VIMES`, …) and any other `set-string`/`copy-to-global` writers not covered by the `*INIT` set — pure static win, extends the string/data labels. (Also: most name-table bases are *read* rarely — reads diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index ee83de9..cf49276 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -712,14 +712,20 @@ gate on the one-player-attack acceptance path. **INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static extractor itself was audited. It now preserves sparse one-based ids and corrects ITINIT from 189 malformed -records to 287 items (plus SKINIT 129→131 skills); regression checks cover the real tables. A reusable field -profiler reports distributions, examples, and direct script/opcode consumers. The ITINIT pilot has curated -thirteen parallel arrays plus six linked row-major tables in `vm-map/globals.toml`. The latter account for -all 764 writes previously misidentified as separate sparse item fields: character restrictions, attack and -equipped condition levels, stat modifiers, tuning curves, and HP/SP/FS recovery. The extractor now emits -these as `record_fields[base/stride/column]`, which also exposes 17 linked SKINIT and 83 linked EBINIT -columns. Next semantic work should profile those two tables and resolve ITINIT's remaining unnamed stat and -condition columns; STINIT's bespoke parser remains a separate extraction task. +records to 287 items (plus SKINIT 129→131 skills); regression checks cover the real tables. It also evaluates +the INIT convention `sub destination, 0, magnitude`, preserving 113 negative ITINIT cells, 212 SKINIT cells, +and 86 EBINIT cells that the former `mov`-only pass silently dropped. A reusable field profiler reports +distributions, examples, and direct script/opcode consumers. + +The first semantic tranche now curates the strongest item, skill, and unit fields in +`vm-map/globals.toml`. ITINIT has thirteen parallel arrays plus six linked row-major tables and 877 linked +writes across 44 populated columns; signed fields include equipment penalties and condition-removal deltas. +SKINIT contributes skill category/order/icon/handler, encoded range, element, status, combat/resource deltas, +proc chance, and battle animation. EBINIT contributes unit order/icon, sex and provisional species categories, +defense element, natural attack, weapon family, starting skills/level, level cap, deployment cost, base stats, +and per-level growth. Its linked-table shapes are now 18 SKINIT and 84 EBINIT populated columns. Next work +should resolve the remaining condition/stat columns and EBINIT's graphics, AI, reward, and sparse-flag tables; +STINIT's bespoke parser remains a separate extraction task. Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks: diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 26beaea..3a16d65 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -61,9 +61,11 @@ and counts are indexed in `build/data/README.md`; field semantics are curated in described by the workflow in `docs/name-resolution.md`. The same audit found that linked row-major writes must not be normalized as independent parallel arrays. -Corpus `lookup-array-2d` bases and strides assign every such ITINIT write unambiguously to six tables (43 -populated columns), while SKINIT and EBINIT expose 17 and 83 linked columns respectively. Generated records -now keep these under `record_fields[base/stride/column]`; `fields` contains only genuine parallel arrays. +Corpus `lookup-array-2d` bases and strides assign every such ITINIT write unambiguously to six tables (44 +populated columns), while SKINIT and EBINIT expose 18 and 84 linked columns respectively. The extractor also +preserves the scripts' zero-minus-immediate negative writes, which carry item penalties, condition cures, and +skill SP costs. Generated records keep linked cells under `record_fields[base/stride/column]`; `fields` +contains only genuine parallel arrays. ### Message/string tables (`*MES`) `ITMES` (64 KB — item text), `VIMES` (43 KB), `EIMES` (37 KB), `SKMES` (31 KB — skill diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 988a2f3..208bb18 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -54,7 +54,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited * | Tool | Purpose | Run | Reads → Writes | |---|---|---|---| | `extract_phase2.py` | Batch: disassembly + text corpora for every script. | `extract_phase2.py` | corpus → `build/disasm/*.asm`, `build/text/{dialogue.jsonl,strings.jsonl,*.strings.txt}`, `build/manifest.json` | -| `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, and use corpus-observed 2D consumers to separate parallel `fields` from linked `record_fields` keyed `base/stride/column`. Also refreshes the generated data index. | `extract_init.py [OUTNAME] [--mode …]` | `
.BIN` → `build/data/.json`, `build/data/README.md` | +| `extract_init.py` | Parse a `*INIT` data table (auto-detects name / numeric / footer shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, statically evaluate both direct `mov` writes and the INIT convention `sub destination, 0, magnitude` for negative values, and use corpus-observed 2D consumers to separate parallel `fields` from linked `record_fields` keyed `base/stride/column`. Also refreshes the generated data index. | `extract_init.py
[OUTNAME] [--mode …]` | `
.BIN` → `build/data/.json`, `build/data/README.md` | | `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric table: per-array and per-record-column population/value distributions, representative records, and direct opcode/script consumers. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py
[--build] [--limit N]` | `build/data/
.json` + corpus → stdout; with `--build`, `build/data/
-field-profile.{json,md}` | | `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based INIT extraction and field profiling. | run each directly | — | | `global_map.py` | Build the partial global-variable name map from static evidence. | `global_map.py` | corpus + `build/data/` → `build/global-var-map.{json,md}` | diff --git a/tools/extract_init.py b/tools/extract_init.py index b93a005..7a918b2 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -3,7 +3,7 @@ *INIT scripts populate parallel global arrays with static game data. Three shapes seen: - name — records keyed by a name string. Each record: set-string(name), mov(fields..), + 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). (SKINIT skills, ITINIT items, EBINIT units) numeric— column table with NO names: mov/copy-to-global into parallel int arrays, keyed @@ -30,6 +30,7 @@ import sys4load SET_STRING = 0x192 MOV = 0x55 +SUB = 0x51 COPY_TO_GLOBAL = 0x6C COPY_LOCAL_ARRAY = 0x64 T_GLOBAL_INT = 3 @@ -50,6 +51,23 @@ def _val(arg): return v if t == T_IMM else {"type": f"0x{t:x}", "value": f"0x{v:x}"} +def _static_global_write(ins): + """Return (destination, value) for statically evaluable global-int writes. + + The shipped name-mode INIT scripts encode positive values with `mov` and + negative values with `sub destination, 0, magnitude`. Ignoring the latter + silently drops costs and penalties from the extracted schema. + """ + if not ins.args or ins.args[0][0] != T_GLOBAL_INT: + return None + if ins.opcode == MOV and len(ins.args) >= 2: + return ins.args[0][1], _val(ins.args[1]) + if (ins.opcode == SUB and len(ins.args) >= 3 + and ins.args[1][0] == T_IMM and ins.args[2][0] == T_IMM): + return ins.args[0][1], ins.args[1][1] - ins.args[2][1] + return None + + def read_footer_array(scr, off): """Read a length-prefixed Data_Array at dword `off`: [length][v0..v_{length-1}].""" dw = scr.dwords @@ -156,14 +174,14 @@ def extract_name(scr): elif cur is not None: key = "desc" if desc_slot == 0 else f"desc{desc_slot}" cur[key] = txt; desc_bases.setdefault(key, addr - cur["id"]); desc_slot += 1 - elif ins.opcode == MOV and cur is not None and ins.args and ins.args[0][0] == T_GLOBAL_INT: - destination = ins.args[0][1] + elif cur is not None and (write := _static_global_write(ins)) is not None: + destination, value = write cell = _record_table_cell(destination, cur["id"]) if cell is None: - cur["fields"][f"0x{destination - cur['id']:x}"] = _val(ins.args[1]) + cur["fields"][f"0x{destination - cur['id']:x}"] = value else: base, stride, column = cell - cur["record_fields"][f"0x{base:x}/{stride}/{column}"] = _val(ins.args[1]) + cur["record_fields"][f"0x{base:x}/{stride}/{column}"] = value for record in records: if not record["record_fields"]: del record["record_fields"] diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 74e99fa..f6afbb3 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -47,14 +47,27 @@ def test_real_name_tables() -> None: "ITINIT columns use the runtime lookup base") check(len({key for record in items for key in record["fields"]}) == 13, "ITINIT has thirteen parallel-array fields") - check(len({key for record in items for key in record.get("record_fields", {})}) == 43, - "ITINIT linked row-major tables expose 43 populated columns") + check(len({key for record in items for key in record.get("record_fields", {})}) == 44, + "ITINIT linked row-major tables expose 44 populated columns") check(by_id[101]["record_fields"]["0xa5301/3/0"] == 30, "ITINIT item 101 stores HP recovery in row-major column zero") + check(by_id[108]["record_fields"]["0x906f9/30/8"] == -5, + "ITINIT item 108 preserves its paralysis-removal delta") + + +def test_static_negative_write() -> None: + class Instruction: + opcode = extract_init.SUB + args = [(extract_init.T_GLOBAL_INT, 0x123), + (extract_init.T_IMM, 0), (extract_init.T_IMM, 7)] + + check(extract_init._static_global_write(Instruction()) == (0x123, -7), + "INIT subtraction writes preserve negative values") if __name__ == "__main__": test_real_name_tables() + test_static_negative_write() if FAILS: raise SystemExit(f"{len(FAILS)} failed checks") print("all extract_init checks passed") diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 8281dfd..c634706 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -138,11 +138,11 @@ confidence = "med" [[global]] address = "0x906f9" -name = "item_attack_status_levels" +name = "item_status_delta_levels" category = "data-table" type = "int[1000][30]" -value_domain = "condition strength 1..5; zero means absent" -usage = "Sparse ITINIT row-major table consumed by USEITEM and CALCILL when applying an item's attack effects. Confirmed condition columns from item descriptions are 2 HP drain, 3 SP drain, 4 FS drain, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear." +value_domain = "condition delta -5..5; zero means absent" +usage = "Sparse ITINIT row-major table consumed by USEITEM and CALCILL when applying an item's effects. Positive values inflict or drain; -5 removes a condition (for example paralysis-removal item 108 stores -5 in column 8). Confirmed columns are 2 HP drain, 3 SP drain, 4 FS drain, 5 curse, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear." source = "investigation" confidence = "high" @@ -152,7 +152,7 @@ name = "item_equipped_status_levels" category = "data-table" type = "int[1000][30]" value_domain = "condition strength 1..5; zero means absent" -usage = "Sparse ITINIT row-major table added to a unit's 30-column condition state by CALCREVISE. Item descriptions identify populated columns 9 poison, 11 fear, 13 regeneration, and 14 exaltation; these are passive equipped effects, distinct from item_attack_status_levels." +usage = "Sparse ITINIT row-major table added to a unit's 30-column condition state by CALCREVISE. Item descriptions identify populated columns 9 poison, 11 fear, 13 regeneration, and 14 exaltation; these are passive equipped effects, distinct from item_status_delta_levels." source = "investigation" confidence = "high" @@ -161,7 +161,7 @@ address = "0x9f541" name = "item_stat_modifiers" category = "data-table" type = "int[1000][14]" -value_domain = "signed additive stat values; shipped populated values are positive" +value_domain = "signed additive stat values; observed -15..200" usage = "ITINIT row-major equipment modifiers added directly to the unit's 14-column stat record by CALCREVISE. Descriptions and consumers establish columns 0 accuracy, 1 evasion, 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 9 capture power, 10 movement, 11 max HP, 12 max SP, and 13 max FS; column 8 remains unnamed." source = "investigation" confidence = "high" @@ -196,6 +196,256 @@ usage = "Sparse ITINIT consumable field. Item 106, Blood Price Healing Hand, des source = "investigation" confidence = "high" +[[global]] +address = "0xa6e5a" +name = "skill_sort_key" +category = "data-table" +type = "int[300]" +value_domain = "mostly unique values 10..2002" +usage = "SKINIT field populated for 129 of 131 skills. ADDEXP, ADDSKILL, CHMENU, INFOIT, and SETCH combine it with skill_category to construct deterministic skill-list ordering keys; higher-level sort operations consume the resulting indices." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xa6f86" +name = "skill_category" +category = "data-table" +type = "int[300]" +value_domain = "1..7" +usage = "SKINIT category for all 131 skills. The records establish 1 movement/exploration, 2 defense/special attack, 3 utility/passive, 4 combat passive, 5 physical special, 6 offensive magic, and 7 healing magic. Combat and menu scripts dispatch directly on these values." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xa71de" +name = "skill_min_range_encoded" +category = "data-table" +type = "int[300]" +value_domain = "1..2; stored minimum range plus one" +usage = "Populated for the 94 active skills. CALCSCOPE and CALCREVISE subtract one before using it as the lower range bound; SELACT and DRAWTIP use the same encoded bound." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xa730a" +name = "skill_max_range_encoded" +category = "data-table" +type = "int[300]" +value_domain = "1..10; stored maximum range plus one" +usage = "Populated for the same 94 active skills as skill_min_range_encoded. CALCSCOPE subtracts one to obtain the upper bound, exactly matching descriptions such as range 3 -> stored 4 and range 9 -> stored 10." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xa7436" +name = "skill_attack_element" +category = "data-table" +type = "int[300]" +value_domain = "1..8 (physical, universal, fire, ice, lightning, earth, holy, dark)" +usage = "Populated for 60 elemental attacks and spells. Values match the Japanese descriptions and the same eight-value attack-element enum used by item_attack_element; CALCSCOPE, CALCBTPARAM, and SELACT consume it." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xa7562" +name = "skill_status_levels" +category = "data-table" +type = "int[300][30]" +value_domain = "condition strength 1..4; zero means absent" +usage = "Sparse SKINIT row-major condition table consumed by CALCILL when applying a skill's effects to the target unit. Skill names and descriptions identify columns 1 instant death, 6 charm, 7 confusion, 8 paralysis, 9 poison, 10 water-flow, and 11 fear; the column layout matches item_status_delta_levels." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xa988a" +name = "skill_icon_id" +category = "data-table" +type = "int[300]" +value_domain = "51..56" +usage = "SKINIT icon id for all 131 skills. CHMENU and INFOIT translate it into the shared icon atlas; the six ids group movement, defense, utility, passive, physical-special, and magic icons." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xa99b6" +name = "skill_combat_stat_deltas" +category = "data-table" +type = "int[300][10]" +value_domain = "signed combat parameter deltas; observed -99..999" +usage = "Sparse SKINIT row-major table applied by CALCBTPARAM for the selected action. Descriptions directly establish columns 0 accuracy, 2 physical attack, 3 physical defense, 4 magic attack, and 9 capture power; columns 1, 5, 6, and 8 share the unit combat-stat layout but need final display-name confirmation." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xaa56e" +name = "skill_resource_deltas" +category = "data-table" +type = "int[300][3]" +value_domain = "column 0 recovery 20..50; column 1 cost -20..-2" +usage = "Sparse SKINIT resource table used throughout skill selection and resolution. Column 0 is HP recovery for the three healing spells; column 1 is the negative SP cost for all 95 active skills, exactly matching each description. Column 2 is unpopulated in the shipped table." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xaa8f2" +name = "skill_proc_chance_percent" +category = "data-table" +type = "int[300]" +value_domain = "10..100 percent" +usage = "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." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xaaa1e" +name = "skill_battle_animation_id" +category = "data-table" +type = "int[300]" +value_domain = "BTANINIT animation ids 9..808" +usage = "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." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xaab4a" +name = "skill_handler_script_id" +category = "data-table" +type = "int[300]" +value_domain = "12746 (0x31ca, SKMES.BIN) in the shipped table" +usage = "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." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x70f0e" +name = "unit_sort_key" +category = "data-table" +type = "int[1000]" +value_domain = "mostly unique values 10..2070" +usage = "EBINIT ordering key populated for 248 units. CHMENU, EXILE, SALLY, INFOEN, and SUMMON pass this array to op 0x12f's stable index sort or combine it with a unit id to build menu ordering keys, mirroring item_sort_key and skill_sort_key." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x716de" +name = "unit_icon_id" +category = "data-table" +type = "int[1000]" +value_domain = "1..103" +usage = "EBINIT icon selector populated for all 277 units. READICON returns it for unit ids; CHMENU, DRAWCHP, EXILE, SALLY, and SUMMON convert the value into icon-atlas page and cell coordinates." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x71ac6" +name = "unit_species_category" +category = "data-table" +type = "int[1000]" +value_domain = "1..3" +usage = "Broad EBINIT species/allegiance category for 198 units: value 1 covers humans and allied heroines, 2 covers demons including Emilio and Lily as well as many demonic monsters, and 3 covers other monsters, dragons, and elementals. CHMENU uses it for equipment restrictions, CALCDMG for category-sensitive effects, and INFOCH/INFOEN for display." +source = "investigation" +confidence = "med" + +[[global]] +address = "0x71eae" +name = "unit_sex_category" +category = "data-table" +type = "int[1000]" +value_domain = "1 male, 2 female, 3 sexless" +usage = "EBINIT sex category for 251 units. Named character records establish the three values; CHMENU tests it against item_sex_restriction_mask, while INFOCH and INFOEN use the same category in unit presentation." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x77c6e" +name = "unit_defense_element" +category = "data-table" +type = "int[1000]" +value_domain = "1..12 (AFINIT defense-attribute enum)" +usage = "EBINIT defensive affinity for 250 units. DRAWENP renders it through the same twelve-value defense-attribute vocabulary as item_defense_element; CALCBTPARAM, BTRTN, and route scripts consume it during battle resolution." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x78056" +name = "unit_default_attack_item_id" +category = "data-table" +type = "int[1000]" +value_domain = "ITINIT innate-attack item ids 901..943" +usage = "EBINIT default attack for 250 units. Every populated value cross-resolves to ITINIT's innate-attack records; SELACT, BTL, CALCBTPARAM, CALCDMG, and CHMENU use it when no equipped weapon overrides the unit's natural attack." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x78826" +name = "unit_weapon_item_category" +category = "data-table" +type = "int[1000]" +value_domain = "-1 unrestricted, otherwise item categories 10..19" +usage = "EBINIT weapon/equipment-family restriction for 179 units. CHMENU compares an item's item_category directly with this value when deciding whether the selected unit can equip it; Lily's forms store -1 for unrestricted handling." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x78ff6" +name = "unit_starting_skill_ids" +category = "data-table" +type = "int[1000][4]" +value_domain = "SKINIT skill ids 1..249; zero means empty" +usage = "EBINIT four-slot starting skill record. The shipped table populates the first three columns. SETEN, UNITECH, and SALLY copy the row into each runtime unit's skill list; GAMESTART, FORT, FIELD, and DRAWENP inspect the same ids, all of which cross-resolve to SKINIT." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x79f96" +name = "unit_deployment_cost_base" +category = "data-table" +type = "int[1000]" +value_domain = "5..99" +usage = "Base deployment/command cost for 204 units. SETCH, SETEN, and ADDEXP copy it plus the per-unit adjustment at 0x57357 into runtime cost 0x5f0bb; SALLY checks the prospective total against party capacity, while FIELD/READY add it to and REMOVECH subtracts it from the deployed-cost aggregate." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x7a37e" +name = "unit_starting_level" +category = "data-table" +type = "int[1000]" +value_domain = "1..99" +usage = "EBINIT initial level for 242 units. Lily's three forms store levels 1, 20, and 40. SETEN and SALLY copy or compare it while constructing a runtime unit, and UNITECH uses it when synchronizing form and character state." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x7a766" +name = "unit_level_cap" +category = "data-table" +type = "int[1000]" +value_domain = "1..99" +usage = "EBINIT maximum level for 243 units. ADDEXP permits level growth only while the runtime level is below this field, and SETEN uses it as the default upper bound when no scenario-specific enemy cap is supplied." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x7af36" +name = "unit_base_stats" +category = "data-table" +type = "int[1000][14]" +value_domain = "nonnegative base stat values; populated columns 2..7 and 10..13" +usage = "EBINIT fourteen-column base-stat record copied wholesale into each runtime unit by SETEN, UNITECH, and SALLY. Its layout matches item_stat_modifiers: columns 2 physical attack, 3 physical defense, 4 magic attack, 5 magic defense, 6 speed, 7 luck, 10 movement, 11 max HP, 12 max SP, and 13 max FS; accuracy/evasion and other unpopulated columns begin at zero." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x7e5e6" +name = "unit_stat_growth_rates" +category = "data-table" +type = "int[1000][14]" +value_domain = "per-level hundredths; populated columns 2..7 and 11..13" +usage = "EBINIT per-level growth record with the same fourteen-column layout as unit_base_stats. ADDEXP adds each rate to a fractional accumulator, divides by 100 to award whole stat points, and retains the remainder; SETEN and UNITECH apply the same rates when materializing units above their starting level." +source = "investigation" +confidence = "high" + [[global]] address = "0x0" name = "system_flow_request"