From 7bec889eedb8e146a603b025583d5b8dab19359a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 19:42:32 -0400 Subject: [PATCH] Decode AFINIT and CTINIT data tables --- docs/global-reference.md | 23 +- docs/name-resolution.md | 9 +- docs/phase-a-slice-plan.md | 30 ++- docs/script-inventory.md | 7 + docs/tools-reference.md | 8 +- docs/vm-mapping-plan.md | 5 +- tools/extract_init.py | 457 +++++++++++++++++++++++++++++++++++++ tools/test_extract_init.py | 140 ++++++++++++ vm-map/globals.toml | 121 +++++++++- 9 files changed, 771 insertions(+), 29 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 036ac20..353231e 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5048 globals (307 curated, 4741 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5049 globals (316 curated, 4733 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -20,8 +20,12 @@ | address | name | conf | source | usage | |---|---|---|---|---| | `0x671b` | shared_spendable_points | high | investigation | Shared spendable point balance used by the alchemy, study, evolution, and summoning systems. ALCHEMY requires and deducts each recipe's alchemy_recipe_point_costs value; STAGECLEAR awards points to this balance and caps it at 999. The exact player-facing Japanese resource label remains unresolved. | +| `0x671c` | item_tuning_facility_level | high | investigation | Current equipment-tuning facility level. IMPROVE indexes facility_level_progress_thresholds row 0 with this value and raises it, up to 6, as item_tuning_facility_progress accumulates. | | `0x671d` | alchemy_level | high | investigation | Current alchemy level. ALCHEMY hides recipes whose alchemy_recipe_minimum_levels value exceeds this level and raises it, up to 6, when alchemy_level_progress reaches the current threshold. | +| `0x671e` | magic_facility_level | high | investigation | Current magic/research facility level. MAGIC and USEMAGIC index facility_level_progress_thresholds row 2 with this level; USEMAGIC raises it up to 6 when magic_facility_progress reaches the current threshold. | +| `0x671f` | item_tuning_facility_progress | high | investigation | Progress toward the next equipment-tuning facility level. IMPROVE adds the selected item's tuning-level increases, applies row 0 of facility_level_progress_thresholds, and carries or clamps progress when the level rises. | | `0x6720` | alchemy_level_progress | high | investigation | Progress toward the next alchemy level. Each successful ALCHEMY synthesis increments it; reaching the current threshold raises alchemy_level and resets this counter. | +| `0x6721` | magic_facility_progress | high | investigation | Progress toward the next magic/research facility level. USEMAGIC adds the selected action's progress award and applies row 2 of facility_level_progress_thresholds; MAGIC renders the same current/threshold pair. | | `0x2052e` | scjump_progress_b | med | inference | Second SCJUMP switch input (1223 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. | | `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. | @@ -161,7 +165,10 @@ | `0xaacf0` | condition_duration_turns_by_level | high | investigation | ILINIT's five-column duration row for each condition. ADDILLSUB indexes it by the resulting condition level minus one and stores the value in entity_condition_remaining_turns. Columns: 0=level_1_turns, 1=level_2_turns, 2=level_3_turns, 3=level_4_turns, 4=level_5_turns. | | `0xaad86` | condition_stat_deltas | high | investigation | ILINIT's five-level by eleven-stat matrix for each condition. CALCREVISE indexes (condition level - 1) * 11 + stat column and adds the result to entity_effective_stats columns 0..10. Shipped rows populate curse, confusion, paralysis, and exaltation. Columns: 0=level_1_accuracy, 1=level_1_evasion, 2=level_1_physical_attack, 3=level_1_physical_defense, 4=level_1_magic_attack, 5=level_1_magic_defense, 6=level_1_speed, 7=level_1_luck, 8=level_1_critical_chance, 9=level_1_capture_power, 10=level_1_movement, 11=level_2_accuracy, 12=level_2_evasion, 13=level_2_physical_attack, 14=level_2_physical_defense, 15=level_2_magic_attack, 16=level_2_magic_defense, 17=level_2_speed, 18=level_2_luck, 19=level_2_critical_chance, 20=level_2_capture_power, 21=level_2_movement, 22=level_3_accuracy, 23=level_3_evasion, 24=level_3_physical_attack, 25=level_3_physical_defense, 26=level_3_magic_attack, 27=level_3_magic_defense, 28=level_3_speed, 29=level_3_luck, 30=level_3_critical_chance, 31=level_3_capture_power, 32=level_3_movement, 33=level_4_accuracy, 34=level_4_evasion, 35=level_4_physical_attack, 36=level_4_physical_defense, 37=level_4_magic_attack, 38=level_4_magic_defense, 39=level_4_speed, 40=level_4_luck, 41=level_4_critical_chance, 42=level_4_capture_power, 43=level_4_movement, 44=level_5_accuracy, 45=level_5_evasion, 46=level_5_physical_attack, 47=level_5_physical_defense, 48=level_5_magic_attack, 49=level_5_magic_defense, 50=level_5_speed, 51=level_5_luck, 52=level_5_critical_chance, 53=level_5_capture_power, 54=level_5_movement. | | `0xab3f8` | condition_resource_deltas | high | investigation | ILINIT's five-level by three-resource matrix for each condition. FIELD applies the active row each turn; CALCDMG also consumes the HP/SP/FS drain condition rows during battle. Positive values restore/drain to the acting side as defined by the caller, while negative values are periodic damage or loss. Columns: 0=level_1_hp, 1=level_1_sp, 2=level_1_fs, 3=level_2_hp, 4=level_2_sp, 5=level_2_fs, 6=level_3_hp, 7=level_3_sp, 8=level_3_fs, 9=level_4_hp, 10=level_4_sp, 11=level_4_fs, 12=level_5_hp, 13=level_5_sp, 14=level_5_fs. | -| `0xab5ba` | attack_element_effectiveness_percent | high | investigation | AFINIT-authored element matchup table. Positive values make an action eligible in SETMVWORK and RTN_M051/052; CALCBTPARAM multiplies battle parameters by the selected percentage and handles negative values as special/immunity cases. | +| `0xab5ba` | attack_element_effectiveness_percent | high | investigation | AFINIT-authored element matchup table with thirteen authored defense rows and eighteen authored cells per row inside the reserved 20-by-20 layout. 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. INFOAF displays eight attack columns for its selected defense rows. | +| `0xab6fa` | item_tuning_stat_bonus_curves | high | investigation | Eighteen usable equipment-growth curves selected by item_tuning_curve_ids, plus an explicitly zeroed reserved row 19. TUNE and IMPROVE use the nonzero prefix as the available tuning-level range; CALCREVISE and DRAWTIP add the selected zero-based tuning level's bonus to the corresponding stat. Columns: 0=tuning_level_1, 1=tuning_level_2, 2=tuning_level_3, 3=tuning_level_4, 4=tuning_level_5, 5=tuning_level_6, 6=tuning_level_7, 7=tuning_level_8, 8=tuning_level_9, 9=tuning_level_10, 10=reserved. | +| `0xab7d6` | item_tuning_point_cost_curves | high | investigation | Point-cost curves paired by curve id and tuning level with item_tuning_stat_bonus_curves. IMPROVE sums costs for newly selected levels, subtracts refunds for removed levels, checks the resulting total against the tuning-point balance, and deducts it on confirmation. Columns: 0=tuning_level_1, 1=tuning_level_2, 2=tuning_level_3, 3=tuning_level_4, 4=tuning_level_5, 5=tuning_level_6, 6=tuning_level_7, 7=tuning_level_8, 8=tuning_level_9, 9=tuning_level_10, 10=reserved. | +| `0xab8b2` | facility_level_progress_thresholds | high | investigation | AFINIT's three facility-progression rows. IMPROVE uses row 0 for equipment tuning, ALCHEMY uses row 1, and MAGIC/USEMAGIC use row 2; each indexes the row by the current level and caps advancement at level 6. Columns: 0=level_0_to_1, 1=level_1_to_2, 2=level_2_to_3, 3=level_3_to_4, 4=level_4_to_5, 5=level_5_to_6, 6=level_6_cap. | | `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. | | `0xaba96` | pathfinding_remaining_route_steps | high | investigation | MVSEEK's row-major reachability grid. Mode 0 searches from the current entity with movement+1 at the origin; modes 1/2 search from the caller-supplied coordinate with 9999 at the origin. Each traversed edge decrements the value, so a larger positive value is nearer to the origin. RTN_M006/007/015 use origin-minus-target values as route-step radii; RTN_M010 ranks Healing Feathers by the negated value. | @@ -244,8 +251,6 @@ | `0x69e` | — | low | auto-shape | array | | `0x6fe` | — | low | auto-shape | array | | `0x712` | — | low | auto-shape | array | -| `0x2690` | — | low | auto-shape | array | -| `0x26a4` | — | low | auto-shape | array | | `0x26b5` | — | low | auto-shape | array | | `0x26d3` | — | low | auto-shape | array | | `0x27bd` | — | low | auto-shape | array | @@ -260,8 +265,6 @@ | `0x3ad6` | — | low | auto-shape | array | | `0x4315` | — | low | auto-shape | array | | `0x4379` | — | low | auto-shape | array | -| `0x671c` | — | low | auto-shape | array | -| `0x671f` | — | low | auto-shape | array | | `0x6727` | — | low | auto-shape | array | | `0x6804` | — | low | auto-shape | array | | `0x6868` | — | low | auto-shape | array | @@ -438,7 +441,6 @@ | `0x3231` | game_mode | med | inference | Game-mode/phase selector in the 0x3231-0x3234 progression-state cluster (chapter_mode is 0x3234). Enum 1..9, written by the gameplay scripts (AIM/ALCHEMY/BTL/BUNKI), branch-read in 136 scenes. Distinct from chapter; likely current sub-mode/screen. INFERENCE — confirm with a listing/sweep before relying on the exact meaning. | | `0x3234` | chapter_mode | high | investigation | Progression chapter/mode selector. SCJUMP's top-level switch keys on it; branch-read by progression scripts (FIELD etc.), not directly by SC/SP scenes. | | `0x3275` | — | med | auto-shape | TODO: confirm. Branch-read in 136 scenes / 143 scripts; compared against [0]; writers=['CAMP.BIN', 'DEBUGADV.BIN', 'DEBUGADV2.BIN', 'FIELD.BIN']. | -| `0x671e` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [5, 6]; writers=['USEMAGIC.BIN']. | | `0x6722` | — | med | auto-shape | TODO: confirm. Branch-read in 13 scenes / 18 scripts; compared against [0, 1, 5, 99]; writers=['GAMESTART.BIN', 'SALLY.BIN', 'SC0010.BIN', 'SC0140.BIN']. | | `0x6724` | — | med | auto-shape | TODO: confirm. Branch-read in 4 scenes / 10 scripts; compared against [0, 25, 50, 99]; writers=['DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']. | | `0x7679` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 4 scripts; compared against [0, 1, 4, 8, 16, 256, 512, 1024]; writers=['FIELD.BIN']. | @@ -475,6 +477,8 @@ | `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. | +| `0x2690` | attack_element_names | high | investigation | AFINIT attack-side affinity vocabulary. DRAWTIP indexes ids 1..8 from ITINIT, while INFOAF displays those same eight ordinary attack elements above attack_element_effectiveness_percent. Columns: 1=physical, 2=universal, 3=fire, 4=ice, 5=lightning, 6=earth, 7=holy, 8=dark, 11=resistance_1, 12=resistance_2, 13=resistance_3, 14=resistance_4, 15=resistance_5, 16=resistance_6, 17=resistance_7. | +| `0x26a4` | defense_element_names | high | investigation | AFINIT defense-side affinity vocabulary. DRAWENP and DRAWTIP index it with unit/item defense element ids; the same id selects a row of attack_element_effectiveness_percent. Columns: 1=physical, 2=universal, 3=fire, 4=ice, 5=lightning, 6=earth, 7=holy, 8=dark, 9=divinity, 10=demon, 11=spirit, 12=undead. | | `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. | @@ -482,6 +486,7 @@ | `0x27ba` | stage_victory_condition_2 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders nonempty values after stage_victory_condition_1, and FIELD copies the slot into the current mission-condition display. | | `0x27bb` | stage_defeat_condition_1 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders this line first in the defeat-condition section, and FIELD copies it into the current mission-condition display. | | `0x27bc` | stage_defeat_condition_2 | high | investigation | STINIT writes one value for each of its 74 stage records. AIM renders nonempty values after stage_defeat_condition_1, and FIELD copies the slot into the current mission-condition display. | +| `0x43dd` | name_entry_character_palette | high | investigation | INPUTNAME's five 70-cell character pages: row 0 hiragana, row 1 katakana, row 2 full-width Latin letters, row 3 Arabic/Kanji/Roman/circled numerals, and row 4 symbols. Cursor slots 70..74 select the page; selecting a populated cell copies it into the seven-character name buffer. | | `0x45b9` | magic_action_names | high | investigation | MAINIT's one-based action-name array: eleven shipped records occupy ids 1..11 in a reserved 30-cell span. MAGIC, STUDY, and EVOLVE render the selected entry. | | `0x45d7` | character_profile_names | high | investigation | CIINIT writes the character-information screen's 24 display names. INFOCH lists them by profile id, masks unrevealed rows, and uses the same id to select the backing unit metadata and CIMES biography. | | `0x463b` | glossary_topic_titles | high | investigation | VIINIT writes the sparse glossary title table. INFOVO renders the selected title, and extract_init joins all 65 populated ids to VIMES's full help text. | @@ -3228,7 +3233,6 @@ | `0x43c8` | — | med | auto-shape | string-table (written by CDINIT2) | | `0x43c9` | — | med | auto-shape | string-table (written by CDINIT2) | | `0x43ca` | — | med | auto-shape | string-table (written by CDINIT2) | -| `0x43dd` | — | med | auto-shape | string-table (written by CTINIT) | | `0x43de` | — | med | auto-shape | string-table (written by CTINIT) | | `0x43df` | — | med | auto-shape | string-table (written by CTINIT) | | `0x43e0` | — | med | auto-shape | string-table (written by CTINIT) | @@ -5041,9 +5045,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] | -| `0xab6fa` | — | med | auto-shape | record-table[stride 11] | -| `0xab7d6` | — | med | auto-shape | record-table[stride 11] | -| `0xab8b2` | — | med | auto-shape | record-table[stride 7] | | `0xc6077` | — | med | auto-shape | record-table[stride 27] | | `0xcc9f0` | — | low | auto-shape | index/counter? | | `0xccbdc` | — | med | auto-shape | record-table[stride 14] | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index 2643ba0..c028162 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -669,15 +669,16 @@ 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.** ITINIT/SKINIT, EBINIT, STINIT, CCINIT, SCINIT, RTINIT, MAINIT, ILINIT, CGINIT, and ALINIT now have +1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, EBINIT, STINIT, CCINIT, SCINIT, RTINIT, MAINIT, ILINIT, CGINIT, ALINIT, AFINIT, and CTINIT now have machine-readable investigation surfaces and semantic joins; EBINIT's populated schema is fully named, STINIT's joined object/enemy payloads are decoded, and CCINIT's 71 class-change rules expose predicates and effects. SCINIT closes the progression decision-to-scene join, and RTINIT's twenty movement/battle banks are structurally decoded with every populated movement-parameter cell classified and all 1,043 shipped movement steps joined to provider behavior. ILINIT/RECOVER closes the condition ABI, CGINIT - closes the gallery registry, and ALINIT closes the 107-recipe alchemy registry with complete ITINIT - joins. With RTINIT's used-provider surface closed and ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and - INFOMES/MES classified, audit the malformed AFINIT/CTINIT outputs next; + closes the gallery registry, ALINIT closes the 107-recipe alchemy registry with complete ITINIT + joins, AFINIT closes the signed affinity/tuning/facility tables, and CTINIT closes INPUTNAME's + five-page character palette. With RTINIT's used-provider surface closed and + ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and INFOMES/MES classified, audit CVINIT next; never assign one universal meaning to a parameter bank whose meaning varies by provider selector. 2. **Label remaining 2D record tables by their readers** — cross-reference which scripts read each `rec[sN]` table and infer purpose from context, preserving reserved rows and sparse cells. RECOVER's diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 07afa99..85baf40 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -2900,5 +2900,31 @@ now names the shared spendable-point balance, alchemy level, and alchemy-level p protect the full write accounting, sparse layout, item joins, story gates, paired ingredient cells, and raw-to-semantic projection. -**Next:** audit the remaining small malformed INIT outputs, especially AFINIT and CTINIT, before -spending a larger slice on structurally regular but semantically shallow MPINIT. +## Data-semantics sidebar: AFINIT affinity/tuning tables and CTINIT name palette (2026-07-23) + +AFINIT and CTINIT were not malformed record tables; both were vocabulary/table initializers that the +generic name heuristic could not segment. AFINIT contains 27 Japanese element labels and 54 +length-prefixed integer rows. Its affinity section is a reserved `20 × 20` defense-by-attack matrix +with thirteen authored defense rows and eighteen authored columns. The dedicated extractor converts +the footer's unsigned representations back to signed values, preserving the `-100` immunity cases, +and joins rows 1..12 to the physical/universal/elemental/divinity/demon/spirit/undead defense names. + +AFINIT's remaining rows are two paired `20 × 11` item-tuning tables and one `3 × 7` facility +progression table. Curve ids 1..18 provide ten tuning-level stat bonuses and matching point costs; +row 19 is explicitly zero/reserved and the eleventh column remains reserved. TUNE, IMPROVE, DRAWTIP, +and CALCREVISE establish the bonus/cost contract. The three facility rows belong to equipment tuning, +alchemy, and magic/research respectively, each with six thresholds leading to the level-6 cap. The +registry now also names the equipment-tuning and magic facility level/progress counters. + +CTINIT is INPUTNAME's complete `5 × 70` character palette. Its rows are hiragana, katakana, full-width +Latin letters, four numeral styles, and symbols; their populated counts are 56, 56, 52, 40, and 69, +for 273 authored cells total. INPUTNAME uses cursor slots 70..74 to select a page, rejects an empty +cell, and copies a selected character into its seven-character name buffer. The generated JSON keeps +all 350 positions so layout gaps remain explicit rather than collapsing into a flat character list. + +Regressions account for every instruction in both scripts, signed affinity values, all tuning and +facility curves, representative characters and gaps, and raw-to-semantic projection. + +**Next:** audit CVINIT. Its generic output is already small (37 writes, thirteen rows, ten apparent +fields), so direct consumer tracing should quickly distinguish a valid sparse registry from the last +remaining ownership artifact before moving to MPINIT. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 73a8b9e..1ff35e7 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -108,6 +108,13 @@ layout, backed by output item, minimum level, required/forbidden story flags, po ingredient-id/quantity slots. The dedicated extraction classifies every write, joins all 107 output items and 286 ingredient references to ITINIT, and retains the seven raw table coordinates. +AFINIT is an affinity and progression initializer, not a name-record table. Its 27 strings form sparse +attack- and defense-element vocabularies; its 54 footer copies form a signed `20 × 20` effectiveness +matrix (thirteen by eighteen cells authored), eighteen usable item-tuning bonus/cost curves plus a +reserved zero row, and equipment-tuning/alchemy/magic facility progression thresholds. CTINIT is +INPUTNAME's separate `5 × 70` character palette: 273 authored hiragana, katakana, Latin, numeral, and +symbol cells with all reserved gaps retained. + RTINIT is a seventh banked shape. Its 3,336 writes populate twenty parallel `1000 × 20` banks: ten movement banks consumed by MVRTN/RTN_M providers and ten battle banks consumed by BTRTN/RTN_B providers. The generated 172 routine-set records assemble 1,043 movement steps and fourteen battle steps, join diff --git a/docs/tools-reference.md b/docs/tools-reference.md index e5f7765..6024cd3 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -57,7 +57,7 @@ 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, RTINIT banked, ILINIT condition-matrix, CNINIT unit-name/voice-family, CGINIT gallery, and ALINIT alchemy-recipe 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 | — | +| `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based, mixed selector-dispatched, CCINIT rule, SCINIT dispatch, RTINIT banked, ILINIT condition-matrix, CNINIT unit-name/voice-family, CGINIT gallery, ALINIT alchemy-recipe, AFINIT affinity/progression, and CTINIT name-entry-palette 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, @@ -86,6 +86,12 @@ required and forbidden story flags, point cost, and four paired ingredient-id/qu and ingredient ids must resolve through ITINIT. The generated record adds a nested `ingredients` view while retaining every raw scalar and `base/stride/column` coordinate. +Name-mode AFINIT and CTINIT bypass the generic name-record heuristic because their strings are +vocabularies rather than record boundaries. AFINIT classifies its 27 element labels and 54 footer +copies into the signed affinity matrix, paired item-tuning bonus/cost curves, and three facility +progression rows. CTINIT emits five reserved 70-cell INPUTNAME pages with 273 authored characters and +every empty slot preserved. Both retain raw global/stride/footer provenance beside their semantic views. + ## VM | Tool | Purpose | Run | Reads → Writes | diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index 1edb3ce..feb35b0 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -53,14 +53,15 @@ - [x] **2.0 — Project structure.** Established `docs/`, `build/{disasm,text,data,scripts-json}/`, `godot/`; game install stays read-only in place. Also relaxed the loader magic check to the `SYS4` family (`SYS4424` patch scripts now parse — was silently skipping 5 scripts). - [x] **2.1 — Text corpora.** `tools/extract_phase2.py` → 481/481 scripts: full disassembly (`build/disasm/*.asm`), per-script strings, `build/text/dialogue.jsonl` (**30,057 show-text lines** — the translation corpus), `build/text/strings.jsonl` (38,449 strings tagged by source opcode), `build/manifest.json`. -- [x] **2.2 — `*INIT` data and rule sources → JSON.** `tools/extract_init.py` auto-detects shape (`name`/`numeric`/`footer`/`mixed`/`rules`/`dispatch`/`banked`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions), CIINIT (24 character-information profiles)** [name: sparse one-based name/description/fields, with CIINIT's dedicated profile registry], **CGINIT (851 sparse gallery images)** [numeric: full image, optional save/stage preview, thumbnail sheet/slot, and variant ordinal in a reserved 2,000-row layout], **ALINIT (107 sparse alchemy recipes)** [numeric: output, level/story gates, point cost, and four paired ingredient slots in a reserved 1,000-row layout], **MPINIT (1472 map records)** [footer: length-prefixed arrays], **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays], **CCINIT (71 class-change rules over 33 units)** [rules: unit/level/state predicates plus title/cost/stat/skill effects], **SCINIT (1,209 final decision rows)** [dispatch: scene resource plus authored chapter metadata and overwrite history], and **RTINIT (172 routine sets)** [banked: twenty movement/battle step banks, provider joins, and overwrite history]. Validated; see `build/data/README.md`. Raw addresses remain bytecode provenance; confirmed semantics come from `vm-map/globals.toml`. +- [x] **2.2 — `*INIT` data and rule sources → JSON.** `tools/extract_init.py` auto-detects shape (`name`/`numeric`/`footer`/`mixed`/`rules`/`dispatch`/`banked`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions), CIINIT (24 character-information profiles)** [name: sparse one-based name/description/fields, with CIINIT's dedicated profile registry], **CGINIT (851 sparse gallery images)** [numeric: full image, optional save/stage preview, thumbnail sheet/slot, and variant ordinal in a reserved 2,000-row layout], **ALINIT (107 sparse alchemy recipes)** [numeric: output, level/story gates, point cost, and four paired ingredient slots in a reserved 1,000-row layout], **AFINIT (13 authored affinity rows plus tuning/facility curves)** [name/footer specialization], **CTINIT (five 70-cell name-entry pages)** [string-matrix specialization], **MPINIT (1472 map records)** [footer: length-prefixed arrays], **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays], **CCINIT (71 class-change rules over 33 units)** [rules: unit/level/state predicates plus title/cost/stat/skill effects], **SCINIT (1,209 final decision rows)** [dispatch: scene resource plus authored chapter metadata and overwrite history], and **RTINIT (172 routine sets)** [banked: twenty movement/battle step banks, provider joins, and overwrite history]. Validated; see `build/data/README.md`. Raw addresses remain bytecode provenance; confirmed semantics come from `vm-map/globals.toml`. - [x] **2.3 — Extract `STINIT`'s 74 sparse stage records.** The mixed mode identifies the dominant `scjump_progress_a` dispatch, recovers 29 preallocated buffer layouts (including six consumer-confirmed row strides), and keeps four condition strings, six scalars, fixed-buffer writes, and all 1,396 footer-array copies separated by stage id. Profiles supply population/value and direct-consumer evidence. Header/map semantics cover conditions, BGM, turn/replay/clear-reward settings, and map texture overrides. The object join assembles 2,312 placements with type/position/gates, OBINIT names and available descriptions, 604 reinforcement schedules, and typed initial-faction, teleport, treasure, card-list, non-triggering-faction, and initial-object-state payloads; FIELD's dedicated special-spawn path proves the final three populated type-27 tagged writes are engine-dead, so they remain visible as ignored provenance rather than unresolved semantics. The 30-cell enemy family contributes 1,378 joined spawns with unit/faction, placement, difficulty/story gates, level scaling, weighted selection, difficulty-specific movement/battle routine sets, and 485 first-clear-only gates. Raw address views remain alongside generated `semantic_fields`. - [x] **2.3a — Extract `CCINIT`'s 71 class-change rules.** Rules mode preserves source-order guards and detects the unit id, minimum level, clear applied-state slot, selected title/level, deployment-cost delta, named fourteen-stat bonuses, awarded SKINIT skills, and state slot set by each rule. EBINIT/SKINIT definition joins and the common global semantic projection coexist with raw addresses. CALCCC/ADDEXP establish the apply/report ABI; EVOLVE explains the two level-independent empty-title Lily rules. The generated profile covers 33 units, 69 titled rules, 30 skill awards, three used promotion slots, and 19 populated output fields. - [x] **2.3b — Extract `SCINIT`'s scene-dispatch registry.** Dispatch mode recognizes 2,179 alternating writes to two 10,000-cell arrays, preserves all 710 overwritten decision ids, and emits 1,209 final rows. The primary column maps decisions to 135 packed SYS4INI ids, all resolved to numbered SC scripts; the parallel column is authored chapter metadata. All 847 live SCJUMP decisions join to rows and 844 final chapter tags agree with the independently decoded paths; three legacy/stale mismatches remain explicit. - [x] **2.3c — Extract `RTINIT`'s routine-step banks.** Banked mode recognizes twenty parallel 1000-by-20 tables and emits 172 sparse routine-set rows with all 3,336 source assignments and 3,307 final cells. Movement banks join all 1,043 steps to all 19 used RTN_M providers (22 dispatchable); battle banks join fourteen steps to all four RTN_B providers. Activation percentages, progress gates, and required/forbidden story flags are consumer-proven; six empty banks remain explicit. RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061 add selector-specific progress, randomized roaming, object-slot, coordinate, enemy/ally, treasure and object search, Healing Feather, waypoint, faction-terrain, retreat, normal-attack routing, offensive target/action selection, and immediate allied-healing semantics. All 977 populated movement-parameter cells are classified: 974 semantic inputs and three M001/M008 cells proven unread; thirteen unwritten defaults are projected separately while the generic raw banks remain intact. - [x] **2.3d — Extract `ALINIT`'s alchemy recipes.** Numeric specialization recognizes seven recipe-indexed structures and emits 107 sparse recipes from all 914 static writes. ALCHEMY proves the output, minimum level, required/forbidden story gates, point cost, and four paired ingredient-id/quantity slots. Every output and all 286 ingredient references join to ITINIT; raw coordinates remain beside the semantic and nested ingredient views. +- [x] **2.3e — Extract `AFINIT` affinity/progression data and `CTINIT` name-entry characters.** AFINIT classifies 27 element strings and 54 footer arrays into thirteen signed effectiveness rows, paired tuning bonus/cost curves, and three facility-progress rows. CTINIT preserves 273 authored characters and 77 empty cells across INPUTNAME's five reserved 70-cell pages. Both schemas account for every instruction and retain raw coordinates. - [x] **2.4 — Partial global-var map BUILT + wired into the disassembler.** `tools/global_map.py` → `build/global-var-map.{json,md}` (16,354/49,435 globals labelled: string tables, `*INIT` field arrays, 122 record tables w/ strides, current-entity index pointers). `sys4load` renders the labels inline (`=rec[s30]`, `=current-entity-index?`). See `docs/name-resolution.md`. -- [ ] **2.5 — Grow the global-var map (future, incremental).** Static first: RTINIT is closed at 1,043/1,043 movement steps; the RECOVER/ILINIT condition ABI and ALINIT recipe registry are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, audit the malformed small AFINIT/CTINIT outputs by their readers, then use Frida only for semantics static consumers cannot settle. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids, SCJUMP decision-to-scene dispatch, the shipped RTINIT movement-provider join, and the six completed message joins are resolved. +- [ ] **2.5 — Grow the global-var map (future, incremental).** Static first: RTINIT is closed at 1,043/1,043 movement steps; the RECOVER/ILINIT condition ABI, ALINIT recipes, AFINIT affinity/progression tables, and CTINIT name palette are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, audit CVINIT's small thirteen-row output, then use Frida only for semantics static consumers cannot settle. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids, SCJUMP decision-to-scene dispatch, the shipped RTINIT movement-provider join, and the six completed message joins are resolved. ## Phase 3 — Name the unnamed opcodes *(top ~20 BEFORE Phase 4; the rest on demand)* diff --git a/tools/extract_init.py b/tools/extract_init.py index 55834d5..43b1a6e 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -599,6 +599,33 @@ ALCHEMY_RECIPE_RECORD_SPAN = 1000 ALCHEMY_RECIPE_STORY_FLAG_STRIDE = 2 ALCHEMY_RECIPE_INGREDIENT_STRIDE = 4 +AFFINITY_ATTACK_ELEMENT_NAME_BASE = 0x2690 +AFFINITY_DEFENSE_ELEMENT_NAME_BASE = 0x26A4 +AFFINITY_ELEMENT_NAME_SPAN = 20 +AFFINITY_EFFECTIVENESS_BASE = 0xAB5BA +AFFINITY_EFFECTIVENESS_STRIDE = 20 +AFFINITY_EFFECTIVENESS_ROW_COUNT = 13 +AFFINITY_EFFECTIVENESS_AUTHORED_COLUMNS = 18 +ITEM_TUNING_BONUS_CURVE_BASE = 0xAB6FA +ITEM_TUNING_COST_CURVE_BASE = 0xAB7D6 +ITEM_TUNING_CURVE_STRIDE = 11 +ITEM_TUNING_CURVE_COUNT = 19 +ITEM_TUNING_AUTHORED_LEVELS = 10 +FACILITY_LEVEL_THRESHOLD_BASE = 0xAB8B2 +FACILITY_LEVEL_THRESHOLD_STRIDE = 7 +FACILITY_LEVEL_THRESHOLD_ROW_COUNT = 3 +FACILITY_LEVEL_THRESHOLD_AUTHORED_LEVELS = 6 + +NAME_ENTRY_CHARACTER_PALETTE_BASE = 0x43DD +NAME_ENTRY_CHARACTER_PALETTE_STRIDE = 70 +NAME_ENTRY_CHARACTER_PALETTE_ROW_NAMES = ( + "hiragana", + "katakana", + "latin", + "numerals", + "symbols", +) + RECOVER_CURRENT_ENTITY = 0x152616 RECOVER_EFFECTIVE_STATS = 0x4E11B RECOVER_CURRENT_RESOURCES = 0x4E085 @@ -2257,6 +2284,424 @@ def extract_alchemy_recipes(scr): } +def extract_affinity_definitions(scr): + """Extract AFINIT's element, tuning-curve, and facility-threshold tables.""" + attack_names = {} + defense_names = {} + effectiveness_rows = {} + tuning_bonus_rows = {} + tuning_cost_rows = {} + facility_threshold_rows = {} + string_write_count = 0 + footer_array_count = 0 + exit_count = 0 + + def signed_values(values): + return [ + value - 0x100000000 if value >= 0x80000000 else value + for value in values + ] + + for ins in scr.instructions: + if ( + ins.opcode == SET_STRING + and len(ins.args) >= 2 + and ins.args[0][0] == T_GLOBAL_STRING + ): + destination = ins.args[0][1] + text = scr.strings.get(ins.args[1][1], (None,))[0] + for base, target in ( + (AFFINITY_ATTACK_ELEMENT_NAME_BASE, attack_names), + (AFFINITY_DEFENSE_ELEMENT_NAME_BASE, defense_names), + ): + element_id = destination - base + if 0 <= element_id < AFFINITY_ELEMENT_NAME_SPAN: + _store_unique(target, element_id, text, element_id) + string_write_count += 1 + break + else: + raise ValueError( + f"{scr.path.name}: unexpected string destination " + f"0x{destination:x}" + ) + continue + + if ( + ins.opcode == COPY_LOCAL_ARRAY + and len(ins.args) >= 2 + and ins.args[0][0] == T_GLOBAL_INT + and ins.args[1][0] == T_IMM + ): + destination = ins.args[0][1] + footer_off = ins.args[1][1] + values = read_footer_array(scr, footer_off) + if values is None: + raise ValueError( + f"{scr.path.name}: invalid footer array 0x{footer_off:x}" + ) + values = signed_values(values) + classified = False + + relative = destination - AFFINITY_EFFECTIVENESS_BASE + if ( + relative % AFFINITY_EFFECTIVENESS_STRIDE == 0 + and 0 <= relative + < AFFINITY_EFFECTIVENESS_ROW_COUNT + * AFFINITY_EFFECTIVENESS_STRIDE + ): + row = relative // AFFINITY_EFFECTIVENESS_STRIDE + if len(values) != AFFINITY_EFFECTIVENESS_AUTHORED_COLUMNS: + raise ValueError( + f"{scr.path.name}: effectiveness row {row} has " + f"{len(values)} values" + ) + _store_unique( + effectiveness_rows, row, (footer_off, values), row + ) + classified = True + + if not classified: + for base, target in ( + (ITEM_TUNING_BONUS_CURVE_BASE, tuning_bonus_rows), + (ITEM_TUNING_COST_CURVE_BASE, tuning_cost_rows), + ): + relative = destination - base + if ( + relative % ITEM_TUNING_CURVE_STRIDE == 0 + and ITEM_TUNING_CURVE_STRIDE + <= relative + <= ITEM_TUNING_CURVE_COUNT + * ITEM_TUNING_CURVE_STRIDE + ): + curve_id = relative // ITEM_TUNING_CURVE_STRIDE + if len(values) != ITEM_TUNING_AUTHORED_LEVELS: + raise ValueError( + f"{scr.path.name}: tuning curve {curve_id} has " + f"{len(values)} values" + ) + _store_unique( + target, curve_id, (footer_off, values), curve_id + ) + classified = True + break + + if not classified: + relative = destination - FACILITY_LEVEL_THRESHOLD_BASE + if ( + relative % FACILITY_LEVEL_THRESHOLD_STRIDE == 0 + and 0 <= relative + < FACILITY_LEVEL_THRESHOLD_ROW_COUNT + * FACILITY_LEVEL_THRESHOLD_STRIDE + ): + row = relative // FACILITY_LEVEL_THRESHOLD_STRIDE + if len(values) != FACILITY_LEVEL_THRESHOLD_AUTHORED_LEVELS: + raise ValueError( + f"{scr.path.name}: facility row {row} has " + f"{len(values)} values" + ) + _store_unique( + facility_threshold_rows, row, (footer_off, values), row + ) + classified = True + + if not classified: + raise ValueError( + f"{scr.path.name}: unexpected footer destination " + f"0x{destination:x}" + ) + footer_array_count += 1 + continue + + if sys4load.display_label(ins.opcode) == "exit": + exit_count += 1 + else: + raise ValueError( + f"{scr.path.name}: unexpected opcode " + f"{sys4load.display_label(ins.opcode)} at 0x{ins.offset:x}" + ) + + expected_effectiveness_rows = set(range(AFFINITY_EFFECTIVENESS_ROW_COUNT)) + expected_tuning_curves = set(range(1, ITEM_TUNING_CURVE_COUNT + 1)) + expected_facility_rows = set(range(FACILITY_LEVEL_THRESHOLD_ROW_COUNT)) + if set(effectiveness_rows) != expected_effectiveness_rows: + raise ValueError(f"{scr.path.name}: incomplete effectiveness matrix") + if ( + set(tuning_bonus_rows) != expected_tuning_curves + or set(tuning_cost_rows) != expected_tuning_curves + ): + raise ValueError(f"{scr.path.name}: incomplete tuning curves") + if set(facility_threshold_rows) != expected_facility_rows: + raise ValueError(f"{scr.path.name}: incomplete facility thresholds") + if exit_count != 1: + raise ValueError(f"{scr.path.name}: expected one exit, got {exit_count}") + + records = [] + for defense_element_id in sorted(effectiveness_rows): + footer_off, values = effectiveness_rows[defense_element_id] + record = { + "id": defense_element_id, + "name": defense_names.get(defense_element_id, ""), + "defense_element_id": defense_element_id, + "footer_arrays": { + ( + f"0x{AFFINITY_EFFECTIVENESS_BASE:x}/" + f"{defense_element_id * AFFINITY_EFFECTIVENESS_STRIDE}" + ): { + "footer_off": f"0x{footer_off:x}", + "values": values, + } + }, + "attack_effectiveness": [ + { + "attack_element_id": attack_element_id, + "attack_element_name": attack_names.get( + attack_element_id, "" + ), + "percent": percent, + } + for attack_element_id, percent in enumerate(values) + ], + } + if record["name"]: + record["string_fields"] = { + f"0x{AFFINITY_DEFENSE_ELEMENT_NAME_BASE:x}": record["name"] + } + records.append(record) + + tuning_curves = [] + for curve_id in sorted(tuning_bonus_rows): + bonus_footer_off, bonuses = tuning_bonus_rows[curve_id] + cost_footer_off, costs = tuning_cost_rows[curve_id] + tuning_curves.append({ + "curve_id": curve_id, + "level_bonuses": bonuses, + "level_costs": costs, + "bonus_raw_key": ( + f"0x{ITEM_TUNING_BONUS_CURVE_BASE:x}/" + f"{curve_id * ITEM_TUNING_CURVE_STRIDE}" + ), + "bonus_footer_off": f"0x{bonus_footer_off:x}", + "cost_raw_key": ( + f"0x{ITEM_TUNING_COST_CURVE_BASE:x}/" + f"{curve_id * ITEM_TUNING_CURVE_STRIDE}" + ), + "cost_footer_off": f"0x{cost_footer_off:x}", + }) + + facility_names = ("item_tuning", "alchemy", "magic") + facility_thresholds = [] + for row in sorted(facility_threshold_rows): + footer_off, thresholds = facility_threshold_rows[row] + facility_thresholds.append({ + "system_id": row, + "system": facility_names[row], + "level_progress_thresholds": thresholds, + "raw_key": ( + f"0x{FACILITY_LEVEL_THRESHOLD_BASE:x}/" + f"{row * FACILITY_LEVEL_THRESHOLD_STRIDE}" + ), + "footer_off": f"0x{footer_off:x}", + }) + + return records, { + "schema": "affinity-and-progression-tables", + "attack_element_names": [ + {"id": element_id, "name": name} + for element_id, name in sorted(attack_names.items()) + ], + "defense_element_names": [ + {"id": element_id, "name": name} + for element_id, name in sorted(defense_names.items()) + ], + "effectiveness_matrix": { + "base": f"0x{AFFINITY_EFFECTIVENESS_BASE:x}", + "reserved_shape": [ + AFFINITY_EFFECTIVENESS_STRIDE, + AFFINITY_EFFECTIVENESS_STRIDE, + ], + "authored_rows": AFFINITY_EFFECTIVENESS_ROW_COUNT, + "authored_columns": AFFINITY_EFFECTIVENESS_AUTHORED_COLUMNS, + }, + "item_tuning_curves": tuning_curves, + "usable_item_tuning_curve_ids": [ + curve["curve_id"] + for curve in tuning_curves + if any(curve["level_bonuses"]) + ], + "reserved_item_tuning_curve_ids": [ + curve["curve_id"] + for curve in tuning_curves + if not any(curve["level_bonuses"]) + and not any(curve["level_costs"]) + ], + "facility_level_thresholds": facility_thresholds, + "string_write_count": string_write_count, + "footer_array_count": footer_array_count, + "exit_count": exit_count, + "classified_instruction_count": ( + string_write_count + footer_array_count + exit_count + ), + "array_layouts": { + f"0x{AFFINITY_EFFECTIVENESS_BASE:x}": { + "length": ( + AFFINITY_EFFECTIVENESS_STRIDE + * AFFINITY_EFFECTIVENESS_STRIDE + ), + "stride": AFFINITY_EFFECTIVENESS_STRIDE, + "rows": AFFINITY_EFFECTIVENESS_STRIDE, + }, + f"0x{ITEM_TUNING_BONUS_CURVE_BASE:x}": { + "length": ( + (ITEM_TUNING_CURVE_COUNT + 1) + * ITEM_TUNING_CURVE_STRIDE + ), + "stride": ITEM_TUNING_CURVE_STRIDE, + "rows": ITEM_TUNING_CURVE_COUNT + 1, + }, + f"0x{ITEM_TUNING_COST_CURVE_BASE:x}": { + "length": ( + (ITEM_TUNING_CURVE_COUNT + 1) + * ITEM_TUNING_CURVE_STRIDE + ), + "stride": ITEM_TUNING_CURVE_STRIDE, + "rows": ITEM_TUNING_CURVE_COUNT + 1, + }, + f"0x{FACILITY_LEVEL_THRESHOLD_BASE:x}": { + "length": ( + FACILITY_LEVEL_THRESHOLD_ROW_COUNT + * FACILITY_LEVEL_THRESHOLD_STRIDE + ), + "stride": FACILITY_LEVEL_THRESHOLD_STRIDE, + "rows": FACILITY_LEVEL_THRESHOLD_ROW_COUNT, + }, + }, + "consumer_contract": { + "affinity": ( + "CALCBTPARAM and AI providers index the effectiveness matrix " + "by defense element then attack element; INFOAF displays the " + "consumer-selected rows and the eight shipped attack elements." + ), + "item_tuning": ( + "TUNE, IMPROVE, DRAWTIP, and CALCREVISE combine each ITINIT " + "curve id with a zero-based tuning level to obtain the stat " + "bonus and point cost." + ), + "facility_progression": ( + "IMPROVE, ALCHEMY, and MAGIC/USEMAGIC index rows 0, 1, and 2 " + "respectively by current facility level." + ), + }, + } + + +def extract_name_entry_palette(scr): + """Extract CTINIT's five-page, 70-cell name-entry character palette.""" + rows = [ + [None] * NAME_ENTRY_CHARACTER_PALETTE_STRIDE + for _ in NAME_ENTRY_CHARACTER_PALETTE_ROW_NAMES + ] + string_write_count = 0 + exit_count = 0 + + for ins in scr.instructions: + if ( + ins.opcode == SET_STRING + and len(ins.args) >= 2 + and ins.args[0][0] == T_GLOBAL_STRING + ): + destination = ins.args[0][1] + relative = destination - NAME_ENTRY_CHARACTER_PALETTE_BASE + if not ( + 0 <= relative + < len(rows) * NAME_ENTRY_CHARACTER_PALETTE_STRIDE + ): + raise ValueError( + f"{scr.path.name}: unexpected character destination " + f"0x{destination:x}" + ) + row, column = divmod( + relative, NAME_ENTRY_CHARACTER_PALETTE_STRIDE + ) + if rows[row][column] is not None: + raise ValueError( + f"{scr.path.name}: duplicate character cell {row}/{column}" + ) + rows[row][column] = scr.strings.get( + ins.args[1][1], (None,) + )[0] + string_write_count += 1 + continue + + if sys4load.display_label(ins.opcode) == "exit": + exit_count += 1 + else: + raise ValueError( + f"{scr.path.name}: unexpected opcode " + f"{sys4load.display_label(ins.opcode)} at 0x{ins.offset:x}" + ) + + if exit_count != 1: + raise ValueError(f"{scr.path.name}: expected one exit, got {exit_count}") + + records = [] + for row_id, (name, characters) in enumerate(zip( + NAME_ENTRY_CHARACTER_PALETTE_ROW_NAMES, rows + )): + populated = [ + {"slot": slot, "character": character} + for slot, character in enumerate(characters) + if character is not None + ] + records.append({ + "id": row_id, + "name": name, + "characters": characters, + "populated_characters": populated, + "string_fields": { + ( + f"0x{NAME_ENTRY_CHARACTER_PALETTE_BASE:x}/" + f"{NAME_ENTRY_CHARACTER_PALETTE_STRIDE}/{entry['slot']}" + ): entry["character"] + for entry in populated + }, + }) + + return records, { + "schema": "name-entry-character-palette", + "palette_base": f"0x{NAME_ENTRY_CHARACTER_PALETTE_BASE:x}", + "reserved_shape": [ + len(NAME_ENTRY_CHARACTER_PALETTE_ROW_NAMES), + NAME_ENTRY_CHARACTER_PALETTE_STRIDE, + ], + "row_names": list(NAME_ENTRY_CHARACTER_PALETTE_ROW_NAMES), + "string_write_count": string_write_count, + "exit_count": exit_count, + "classified_instruction_count": string_write_count + exit_count, + "populated_cells_per_row": [ + sum(character is not None for character in row) for row in rows + ], + "empty_slots_per_row": [ + [ + slot + for slot, character in enumerate(row) + if character is None + ] + for row in rows + ], + "consumer_contract": { + "script": "INPUTNAME.BIN", + "lookup": ( + "INPUTNAME selects one of five palette pages, indexes its " + "70-cell row by cursor slot, rejects empty cells, and copies " + "a selected character into the seven-character name buffer." + ), + "page_selection": ( + "Cursor slots 70..74 select palette rows 0..4." + ), + }, + } + + @cache def gallery_thumbnail_sheet_assets() -> dict[int, int]: """Read CGMODE's enabled thumbnail-sheet assets from INIT2.""" @@ -2990,6 +3435,14 @@ def write_data_index(data_dir: Path) -> None: "point cost, required/forbidden story flags, and four fixed ingredient slots retain", "their raw parallel-array and row-table coordinates.", "", + "AFINIT's dedicated affinity/progression schema exposes its attack and defense", + "element vocabularies, signed effectiveness matrix, eighteen usable item-tuning", + "bonus/cost curves plus a reserved zero row, and three facility progression rows.", + "", + "CTINIT's dedicated name-entry schema exposes INPUTNAME's five 70-cell palette", + "pages (hiragana, katakana, Latin, numerals, and symbols), preserving all reserved", + "empty slots beside the 273 authored characters.", + "", "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`", @@ -3065,6 +3518,10 @@ def main() -> int: extractor = extract_magic_actions elif mode == "name" and name == "ILINIT": extractor = extract_condition_definitions + elif mode == "name" and name == "AFINIT": + extractor = extract_affinity_definitions + elif mode == "name" and name == "CTINIT": + extractor = extract_name_entry_palette elif mode == "numeric" and name == "CGINIT": extractor = extract_gallery_definitions elif mode == "numeric" and name == "ALINIT": diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 4bbec5d..1253195 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -1134,6 +1134,144 @@ def test_alchemy_recipes() -> None: ) +def test_affinity_definitions() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["AFINIT.BIN"]) + check( + extract_init.detect_mode(script) == "name", + "AFINIT remains compatible with name-mode auto-detection", + ) + records, meta = extract_init.extract_affinity_definitions(script) + by_id = {record["id"]: record for record in records} + check( + len(records) == 13 + and meta["string_write_count"] == 27 + and meta["footer_array_count"] == 54 + and meta["classified_instruction_count"] == 82, + "AFINIT classifies every vocabulary, footer-array, and exit instruction", + ) + check( + [entry["id"] for entry in meta["attack_element_names"]] + == [*range(1, 9), *range(11, 18)] + and [entry["id"] for entry in meta["defense_element_names"]] + == list(range(1, 13)), + "AFINIT preserves its sparse attack and defense element vocabularies", + ) + check( + by_id[3]["name"] == "火炎" + and by_id[3]["attack_effectiveness"][3]["percent"] == -100 + and by_id[3]["attack_effectiveness"][4]["percent"] == 150 + and by_id[11]["attack_effectiveness"][1]["percent"] == 1 + and by_id[11]["attack_effectiveness"][7]["percent"] == 200, + "AFINIT exposes signed elemental immunities, weaknesses, and resistances", + ) + + tuning = { + curve["curve_id"]: curve for curve in meta["item_tuning_curves"] + } + check( + len(tuning) == 19 + and tuning[1]["level_bonuses"] == [1, 0, 0, 0, 0, 0, 0, 0, 0, 0] + and tuning[9]["level_bonuses"] == list(range(1, 11)) + and tuning[18]["level_bonuses"] == list(range(3, 31, 3)) + and tuning[18]["level_costs"] + == [10, 25, 45, 70, 100, 140, 190, 250, 320, 400] + and tuning[19]["level_bonuses"] == [0] * 10 + and tuning[19]["level_costs"] == [0] * 10 + and meta["usable_item_tuning_curve_ids"] == list(range(1, 19)) + and meta["reserved_item_tuning_curve_ids"] == [19], + "AFINIT pairs all nineteen item-tuning bonus and point-cost curves", + ) + check( + [ + row["level_progress_thresholds"] + for row in meta["facility_level_thresholds"] + ] == [ + [40, 80, 120, 160, 200, 300], + [20, 40, 60, 90, 120, 200], + [20, 50, 100, 150, 200, 400], + ], + "AFINIT exposes the tuning, alchemy, and magic progression rows", + ) + + semantics = extract_init.field_semantics( + records, meta["array_layouts"] + ) + check( + len(semantics) == 14 + and semantics["0x26a4"] == "defense_element_names" + and semantics["0xab5ba/0"] + == "attack_element_effectiveness_percent.row_0" + and semantics["0xab5ba/240"] + == "attack_element_effectiveness_percent.row_12", + "AFINIT raw vocabulary and matrix rows join to canonical semantics", + ) + extract_init.attach_semantic_fields(records, semantics) + check( + by_id[3]["semantic_fields"]["defense_element_names"] == "火炎" + and by_id[3]["semantic_fields"][ + "attack_element_effectiveness_percent.row_3" + ][3] == -100, + "AFINIT retains raw footer provenance beside signed semantic rows", + ) + + +def test_name_entry_palette() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["CTINIT.BIN"]) + check( + extract_init.detect_mode(script) == "name", + "CTINIT remains compatible with name-mode auto-detection", + ) + records, meta = extract_init.extract_name_entry_palette(script) + by_id = {record["id"]: record for record in records} + check( + len(records) == 5 + and meta["reserved_shape"] == [5, 70] + and meta["string_write_count"] == 273 + and meta["classified_instruction_count"] == 274, + "CTINIT classifies all five reserved palette pages and every instruction", + ) + check( + meta["row_names"] + == ["hiragana", "katakana", "latin", "numerals", "symbols"] + and meta["populated_cells_per_row"] == [56, 56, 52, 40, 69], + "CTINIT names each page and preserves its authored cell population", + ) + check( + by_id[0]["characters"][0] == "あ" + and by_id[0]["characters"][17] is None + and by_id[1]["characters"][50] == "ァ" + and by_id[2]["characters"][0] == "A" + and by_id[2]["characters"][30] == "a" + and by_id[3]["characters"][20] == "Ⅰ" + and by_id[3]["characters"][30] == "①" + and by_id[4]["characters"][68] == "ω" + and by_id[4]["characters"][69] is None, + "CTINIT retains representative characters and intentional empty slots", + ) + + semantics = extract_init.field_semantics(records) + check( + len(semantics) == 69 + and semantics["0x43dd/70/0"] + == "name_entry_character_palette.column_0" + and semantics["0x43dd/70/68"] + == "name_entry_character_palette.column_68", + "CTINIT raw palette slots join to one canonical table name", + ) + extract_init.attach_semantic_fields(records, semantics) + check( + by_id[0]["semantic_fields"][ + "name_entry_character_palette.column_0" + ] == "あ" + and by_id[4]["semantic_fields"][ + "name_entry_character_palette.column_68" + ] == "ω", + "CTINIT retains raw cells beside the semantic palette view", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -1345,6 +1483,8 @@ if __name__ == "__main__": test_character_names() test_gallery_definitions() test_alchemy_recipes() + test_affinity_definitions() + test_name_entry_palette() test_condition_definitions() test_field_semantics() if FAILS: diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 590723a..efb4656 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -1798,10 +1798,10 @@ name = "attack_element_effectiveness_percent" category = "data-table" type = "int[20][20]" value_domain = "signed effectiveness percentage indexed by defense element then attack element" -usage = "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." +usage = "AFINIT-authored element matchup table with thirteen authored defense rows and eighteen authored cells per row inside the reserved 20-by-20 layout. 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. INFOAF displays eight attack columns for its selected defense rows." source = "investigation" confidence = "high" -depends_on = ["0x77c6e", "0x8e3d1", "0x8dfe9", "0xa7436"] +depends_on = ["0x2690", "0x26a4", "0x77c6e", "0x8e3d1", "0x8dfe9", "0xa7436"] [[global]] address = "0xcc9f4" @@ -2194,14 +2194,47 @@ depends_on = ["0x671d"] [[global]] address = "0x671e" -name = "" -category = "story-flag" +name = "magic_facility_level" +category = "counter" type = "int" -value_domain = "one of {5, 6}" -usage = "TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [5, 6]; writers=['USEMAGIC.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +value_domain = "0..6" +usage = "Current magic/research facility level. MAGIC and USEMAGIC index facility_level_progress_thresholds row 2 with this level; USEMAGIC raises it up to 6 when magic_facility_progress reaches the current threshold." +source = "investigation" +confidence = "high" +depends_on = ["0xab8b2", "0x6721"] + +[[global]] +address = "0x671c" +name = "item_tuning_facility_level" +category = "counter" +type = "int" +value_domain = "0..6" +usage = "Current equipment-tuning facility level. IMPROVE indexes facility_level_progress_thresholds row 0 with this value and raises it, up to 6, as item_tuning_facility_progress accumulates." +source = "investigation" +confidence = "high" +depends_on = ["0xab8b2", "0x671f"] + +[[global]] +address = "0x671f" +name = "item_tuning_facility_progress" +category = "counter" +type = "int" +value_domain = "non-negative tuning progress" +usage = "Progress toward the next equipment-tuning facility level. IMPROVE adds the selected item's tuning-level increases, applies row 0 of facility_level_progress_thresholds, and carries or clamps progress when the level rises." +source = "investigation" +confidence = "high" +depends_on = ["0x671c", "0xab8b2"] + +[[global]] +address = "0x6721" +name = "magic_facility_progress" +category = "counter" +type = "int" +value_domain = "non-negative magic/research progress" +usage = "Progress toward the next magic/research facility level. USEMAGIC adds the selected action's progress award and applies row 2 of facility_level_progress_thresholds; MAGIC renders the same current/threshold pair." +source = "investigation" +confidence = "high" +depends_on = ["0x671e", "0xab8b2"] [[global]] address = "0x6722" @@ -3348,3 +3381,73 @@ usage = "Required ingredient quantities paired cell-for-cell with alchemy_recipe source = "investigation" confidence = "high" depends_on = ["0x158154"] + +[[global]] +address = "0x2690" +name = "attack_element_names" +category = "string-table" +type = "string[20]" +columns = { "1" = "physical", "2" = "universal", "3" = "fire", "4" = "ice", "5" = "lightning", "6" = "earth", "7" = "holy", "8" = "dark", "11" = "resistance_1", "12" = "resistance_2", "13" = "resistance_3", "14" = "resistance_4", "15" = "resistance_5", "16" = "resistance_6", "17" = "resistance_7" } +value_domain = "Japanese element/attribute display strings; unused cells are empty" +usage = "AFINIT attack-side affinity vocabulary. DRAWTIP indexes ids 1..8 from ITINIT, while INFOAF displays those same eight ordinary attack elements above attack_element_effectiveness_percent." +source = "investigation" +confidence = "high" +depends_on = ["0x8dfe9", "0xa7436", "0xab5ba"] + +[[global]] +address = "0x26a4" +name = "defense_element_names" +category = "string-table" +type = "string[20]" +columns = { "1" = "physical", "2" = "universal", "3" = "fire", "4" = "ice", "5" = "lightning", "6" = "earth", "7" = "holy", "8" = "dark", "9" = "divinity", "10" = "demon", "11" = "spirit", "12" = "undead" } +value_domain = "Japanese defense-affinity display strings; unused cells are empty" +usage = "AFINIT defense-side affinity vocabulary. DRAWENP and DRAWTIP index it with unit/item defense element ids; the same id selects a row of attack_element_effectiveness_percent." +source = "investigation" +confidence = "high" +depends_on = ["0x77c6e", "0x8e3d1", "0xab5ba"] + +[[global]] +address = "0x43dd" +name = "name_entry_character_palette" +category = "string-table" +type = "string[5][70]" +value_domain = "one authored input character or empty reserved cell" +usage = "INPUTNAME's five 70-cell character pages: row 0 hiragana, row 1 katakana, row 2 full-width Latin letters, row 3 Arabic/Kanji/Roman/circled numerals, and row 4 symbols. Cursor slots 70..74 select the page; selecting a populated cell copies it into the seven-character name buffer." +source = "investigation" +confidence = "high" + +[[global]] +address = "0xab6fa" +name = "item_tuning_stat_bonus_curves" +category = "data-table" +type = "int[20][11]" +columns = { "0" = "tuning_level_1", "1" = "tuning_level_2", "2" = "tuning_level_3", "3" = "tuning_level_4", "4" = "tuning_level_5", "5" = "tuning_level_6", "6" = "tuning_level_7", "7" = "tuning_level_8", "8" = "tuning_level_9", "9" = "tuning_level_10", "10" = "reserved" } +value_domain = "non-negative stat bonus; zero means that tuning level is unavailable" +usage = "Eighteen usable equipment-growth curves selected by item_tuning_curve_ids, plus an explicitly zeroed reserved row 19. TUNE and IMPROVE use the nonzero prefix as the available tuning-level range; CALCREVISE and DRAWTIP add the selected zero-based tuning level's bonus to the corresponding stat." +source = "investigation" +confidence = "high" +depends_on = ["0xa2bf1"] + +[[global]] +address = "0xab7d6" +name = "item_tuning_point_cost_curves" +category = "data-table" +type = "int[20][11]" +columns = { "0" = "tuning_level_1", "1" = "tuning_level_2", "2" = "tuning_level_3", "3" = "tuning_level_4", "4" = "tuning_level_5", "5" = "tuning_level_6", "6" = "tuning_level_7", "7" = "tuning_level_8", "8" = "tuning_level_9", "9" = "tuning_level_10", "10" = "reserved" } +value_domain = "non-negative point cost; shipped nonzero values are 10..400" +usage = "Point-cost curves paired by curve id and tuning level with item_tuning_stat_bonus_curves. IMPROVE sums costs for newly selected levels, subtracts refunds for removed levels, checks the resulting total against the tuning-point balance, and deducts it on confirmation." +source = "investigation" +confidence = "high" +depends_on = ["0xab6fa", "0xa2bf1"] + +[[global]] +address = "0xab8b2" +name = "facility_level_progress_thresholds" +category = "data-table" +type = "int[3][7]" +columns = { "0" = "level_0_to_1", "1" = "level_1_to_2", "2" = "level_2_to_3", "3" = "level_3_to_4", "4" = "level_4_to_5", "5" = "level_5_to_6", "6" = "level_6_cap" } +value_domain = "required progress; final cap column is zero/unwritten" +usage = "AFINIT's three facility-progression rows. IMPROVE uses row 0 for equipment tuning, ALCHEMY uses row 1, and MAGIC/USEMAGIC use row 2; each indexes the row by the current level and caps advancement at level 6." +source = "investigation" +confidence = "high" +depends_on = ["0x671c", "0x671d", "0x671e", "0x671f", "0x6720", "0x6721"]