From d0b1a97b473d4218c66766518d742737f66ceb5a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 23 Jul 2026 20:01:53 -0400 Subject: [PATCH] Decode CVINIT voice configuration --- docs/global-reference.md | 10 +- docs/name-resolution.md | 9 +- docs/phase-a-slice-plan.md | 30 +++++- docs/script-inventory.md | 6 ++ docs/tools-reference.md | 8 +- docs/vm-mapping-plan.md | 5 +- tools/extract_init.py | 201 +++++++++++++++++++++++++++++++++++++ tools/test_extract_init.py | 88 ++++++++++++++++ vm-map/globals.toml | 41 +++++++- 9 files changed, 380 insertions(+), 18 deletions(-) diff --git a/docs/global-reference.md b/docs/global-reference.md index 353231e..b3d0029 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5049 globals (316 curated, 4733 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5049 globals (319 curated, 4730 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -69,6 +69,7 @@ | `0x53643` | entity_movement_routine_progress | high | investigation | Per-runtime-entity movement-routine progress counters. FIELD clears all fifty rows, movement providers increment the current step, and MVRTN compares it with movement_routine_minimum_progress_counts before enabling a step. | | `0x53e13` | entity_carried_item_ids | high | investigation | Two carried/drop-item slots per runtime entity. SETEN initializes slot 0 from the unit's starting equipment item, FIELD fills an empty or matching slot when treasure is collected, and RTN_M009 requires an empty slot or an item-id match before approaching a chest/treasure object. Columns: 0=slot_0, 1=slot_1. | | `0x53e77` | entity_carried_item_counts | high | investigation | Quantities for the two per-entity carried/drop-item slots. SETEN seeds a starting item with quantity one; FIELD adds treasure quantities to the matching slot and caps the result at 999. Columns: 0=slot_0, 1=slot_1. | +| `0x56223` | unit_story_speaker_seen_flags | high | investigation | Scene scripts set and persist a unit's cell when drawing that speaker's name. CONFIG reloads the twelve CVINIT-mapped unit cells and enables each named character-voice control only after its speaker has been encountered. | | `0x5660b` | skill_info_revealed_flags | high | investigation | Persistent skill-information visibility flags. ADDSKILL sets the selected skill, BTL marks every equipped skill when it is observed in combat, and INFOIT suppresses a skill's icon/handler-driven details until this flag is nonzero. This is broader than skill_acquired_flags. | | `0x56738` | entity_battle_routine_random_rolls | high | investigation | Per-runtime-entity battle-step random rolls. FIELD and BTL fill every active entity's twenty cells with random-modulo-100; BTRTN executes a step when the matching roll is below battle_routine_activation_percents. | | `0x56b20` | entity_patrol_waypoint_indices | high | investigation | RTN_M011 compares each step's one-based waypoint_ordinal minus one with the current entity's value. Reaching the selected destination advances this value modulo the largest RTN_M011 waypoint ordinal in the routine set. | @@ -77,7 +78,9 @@ | `0x573bb` | unit_class_change_state | high | investigation | Persistent ten-slot class-change state for each playable unit. CALCCC copies the current unit's row to class_change_state_work before invoking the rule scripts and persists the updated row; CCINIT requires the rule's slot to be zero so each promotion is applied once. Columns: 0=promotion_slot_1, 1=promotion_slot_2, 2=promotion_slot_3, 3=promotion_slot_4, 4=promotion_slot_5, 5=promotion_slot_6, 6=promotion_slot_7, 7=promotion_slot_8, 8=promotion_slot_9, 9=promotion_slot_10. | | `0x5f0ed` | scene_decision_seen_flags | high | investigation | Persistent seen-state for progression decisions. Numbered scene scripts set the current scjump_decision_out cell after playback and synchronize it through the persistence opcode; INFOVO uses VIINIT's one-based prerequisites to unlock glossary topics. | | `0x624bf` | unit_voice_family_unit_ids | high | investigation | CNINIT's sparse unit-id keyed voice-family normalization map. Story, history, field, and battle voice paths map the current unit variant through this table before selecting the family-level voice-suppression state; 175 of 277 authored rows alias a variant to another representative unit id. | -| `0x628a7` | unit_voice_suppression_flag_ids | high | investigation | Second stage of the shared voice-eligibility chain. Story, history, field, and battle paths normalize the current unit through unit_voice_family_unit_ids, index this table by that representative unit id, and suppress voice playback when the selected character_voice_suppressed cell is nonzero. | +| `0x628a7` | unit_voice_suppression_flag_ids | high | investigation | CVINIT's unit-to-setting inverse map for the twelve named character voice controls. Story, history, field, and battle paths normalize the current unit through unit_voice_family_unit_ids, index this table by that representative unit id, and suppress voice playback when the selected character_voice_suppressed cell is nonzero. | +| `0x62c8f` | character_voice_setting_unit_ids | high | investigation | CVINIT's CONFIG-row join. CONFIG iterates setting slots 1..12, resolves each unit id through unit_story_display_names for its label, and tests the same unit's unit_story_speaker_seen_flags cell before exposing the row. CVINIT also writes the exact inverse mapping to unit_voice_suppression_flag_ids. Columns: 0=system_or_reserved, 1=lily, 2=sylphine, 3=sasune, 4=vidal, 5=estelle, 6=nelly, 7=tiofania, 8=colette, 9=bridget, 10=octavia, 11=fam, 12=deirdre. | +| `0x62cad` | character_voice_preview_asset_ids | high | investigation | CVINIT's thirteen voice-setting preview clips. CONFIG indexes this array by setting slot and plays the selected clip; slot 0 is the non-unit system voice, while slots 1..12 join to character_voice_setting_unit_ids. Columns: 0=system_voice, 1=lily, 2=sylphine, 3=sasune, 4=vidal, 5=estelle, 6=nelly, 7=tiofania, 8=colette, 9=bridget, 10=octavia, 11=fam, 12=deirdre. | | `0x62cd1` | gallery_image_assets | high | investigation | CGINIT's sparse gallery asset rows. CGMODE tests and displays column 0 as the full-size gallery image. SAVE and SELSTAGE scan column 0 for the current image and, when column 1 is populated, load that 112x84 preview instead of capturing the current screen. Columns: 0=gallery_image_asset_id, 1=save_stage_preview_asset_id. | | `0x63c71` | gallery_thumbnail_sheet_ids | high | investigation | CGINIT's gallery thumbnail-atlas selector. CGMODE subtracts one, remaps the configured sheet through gallery_thumbnail_sheet_asset_ids, and groups every populated gallery record under that sheet. | | `0x64441` | gallery_thumbnail_slot_ids | high | investigation | CGINIT's slot within the selected 6x5 thumbnail atlas. CGMODE subtracts one, draws that one of thirty 126x95 cells, and reports the unlocked/total variant counts associated with the slot. | @@ -285,7 +288,6 @@ | `0x522bb` | — | low | auto-shape | array | | `0x53ede` | — | low | auto-shape | array | | `0x55e3b` | — | low | auto-shape | array | -| `0x56223` | — | low | auto-shape | array | | `0x56b52` | — | low | auto-shape | array | | `0x56f6d` | — | low | auto-shape | array | | `0x577a3` | — | low | auto-shape | array | @@ -300,8 +302,6 @@ | `0x62469` | — | low | auto-shape | array | | `0x6247d` | — | low | auto-shape | array | | `0x624a3` | — | low | auto-shape | array | -| `0x62c8f` | — | low | auto-shape | array | -| `0x62cad` | — | low | auto-shape | array | | `0x653e1` | — | low | auto-shape | array | | `0x65bb1` | — | low | auto-shape | array | | `0x66421` | — | low | auto-shape | array | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index c028162..d4b9895 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -669,16 +669,17 @@ 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, ALINIT, AFINIT, and CTINIT now have +1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, EBINIT, STINIT, CCINIT, SCINIT, RTINIT, MAINIT, ILINIT, CGINIT, ALINIT, AFINIT, CTINIT, and CVINIT 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, 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; + joins, AFINIT closes the signed affinity/tuning/facility tables, CTINIT closes INPUTNAME's + five-page character palette, and CVINIT closes CONFIG's thirteen preview clips plus the + bidirectional character-to-suppression-setting join. With RTINIT's used-provider surface closed and + ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and INFOMES/MES classified, audit MPINIT 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 85baf40..760bcb9 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -2925,6 +2925,30 @@ all 350 positions so layout gaps remain explicit rather than collapsing into a f 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. +## Data-semantics sidebar: CVINIT character-voice configuration (2026-07-23) + +CVINIT's generic thirteen-row shape was close but incomplete: it grouped 35 of the script's 37 writes +and presented a twelve-entry inverse map as eight unrelated address-derived fields. The dedicated +schema accounts for all 38 instructions (37 static writes plus `exit`) as three coordinated arrays: +thirteen CONFIG preview-voice assets at `0x62cad`, twelve setting-slot-to-EBINIT-unit ids at +`0x62c8f + slot`, and twelve inverse unit-to-setting ids at `0x628a7 + unit`. + +CONFIG proves the presentation contract. Slot 0 is a non-unit system voice preview; slots 1..12 map +to Lily, Sylphine, Sasune, Vidal, Estelle, Nelly, Tiofania, Colette, Bridget, Octavia, Fam, and +Deirdre. CONFIG resolves those unit ids through the shared story-display-name table, reloads each +unit's persisted speaker-seen flag to decide whether the row is available, plays the slot's preview +OGG, and edits the matching cell in the thirteen-entry `character_voice_suppressed` settings array. +All thirteen preview ids resolve to shipped audio assets. + +The inverse map closes the runtime voice-filter chain rather than duplicating the CONFIG row map. +Story, history, field, and battle paths first normalize the active EBINIT unit through CNINIT's +voice-family unit table, then map that representative unit through CVINIT's inverse table and test +the selected suppression cell. The twelve forward and inverse entries round-trip exactly. Scene +scripts independently set and persist the unit-keyed speaker-seen flags when each speaker first +appears, explaining CONFIG's unlock gate. + +Regressions protect complete instruction accounting, all asset and EBINIT joins, the system slot, +all twelve round trips, speaker-seen addresses, and raw-to-semantic projection. + +**Next:** audit MPINIT's 1,472 structurally regular footer-array records by their direct map readers, +grouping rows into consumer-proven families before naming any shallow address-only fields. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 1ff35e7..0a93ff3 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -115,6 +115,12 @@ reserved zero row, and equipment-tuning/alchemy/magic facility progression thres INPUTNAME's separate `5 × 70` character palette: 273 authored hiragana, katakana, Latin, numeral, and symbol cells with all reserved gaps retained. +CVINIT is CONFIG's character-voice control registry. Its 37 writes form thirteen preview-voice asset +slots, a twelve-entry setting-slot-to-EBINIT-unit map, and the exact inverse unit-to-setting map used +after CNINIT's voice-family normalization. Slot 0 is the non-unit system voice; slots 1..12 join to +Lily and eleven other named characters. CONFIG uses persisted per-unit speaker-seen flags to reveal those +twelve rows, plays the corresponding preview clip, and writes the selected suppression setting. + 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 6024cd3..2846d16 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, 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 | — | +| `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, CTINIT name-entry-palette, and CVINIT character-voice 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, @@ -92,6 +92,12 @@ copies into the signed affinity matrix, paired item-tuning bonus/cost curves, an 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. +Numeric-mode CVINIT classifies all 37 writes into CONFIG's thirteen preview-voice assets, twelve +setting-slot-to-EBINIT-unit joins, and the exact inverse unit-to-suppression-setting map. Preview ids +join to shipped OGG filenames, character slots join to EBINIT names, and the schema records the +per-unit persisted speaker-seen cells that gate CONFIG row availability. Every raw coordinate remains +beside the joined setting view. + ## VM | Tool | Purpose | Run | Reads → Writes | diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index feb35b0..13d6c9f 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -53,15 +53,16 @@ - [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], **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.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], **CVINIT (13 character-voice settings)** [numeric: preview assets, slot-to-unit joins, and inverse suppression-setting map], **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.3f — Extract `CVINIT` character-voice settings.** Numeric specialization accounts for all 37 static writes as thirteen preview OGG assets, twelve CONFIG-setting-slot-to-EBINIT-unit joins, and the exact inverse unit-to-suppression-setting map. CONFIG proves preview, row-label, persisted speaker-seen unlock, and setting-edit behavior; the shared CNINIT/CVINIT voice chain proves the inverse map. - [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, 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. +- [ ] **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, CTINIT name palette, and CVINIT character-voice 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 MPINIT's structurally regular footer rows by their direct 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. ## 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 43b1a6e..d225def 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -22,6 +22,9 @@ ILINIT is a special name-mode matrix: 30 reserved condition ids by five authored levels, joined to the runtime condition-state ABI and RECOVER policy. +CVINIT is a special numeric-mode registry: thirteen voice-configuration preview +slots, twelve slot-to-unit joins, and the matching unit-to-setting inverse map. + Records are {id, name?, desc?, fields:{"0x": value}} or, for footer tables, {id, global_addr, footer_off, values:[...]}. Column addresses are raw engine globals; confirmed names come from the generated engine global registry while raw keys remain provenance. @@ -626,6 +629,13 @@ NAME_ENTRY_CHARACTER_PALETTE_ROW_NAMES = ( "symbols", ) +VOICE_CONFIG_PREVIEW_ASSET_ARRAY_BASE = 0x62CAD +VOICE_CONFIG_SLOT_UNIT_ARRAY_BASE = 0x62C8F +VOICE_CONFIG_UNIT_SETTING_ARRAY_BASE = 0x628A7 +VOICE_CONFIG_SLOT_COUNT = 13 +VOICE_CONFIG_NAMED_SLOT_COUNT = 12 +VOICE_CONFIG_SPEAKER_SEEN_ARRAY_BASE = 0x56223 + RECOVER_CURRENT_ENTITY = 0x152616 RECOVER_EFFECTIVE_STATS = 0x4E11B RECOVER_CURRENT_RESOURCES = 0x4E085 @@ -2702,6 +2712,191 @@ def extract_name_entry_palette(scr): } +def extract_voice_configuration(scr): + """Extract CVINIT's preview-voice and character-setting registry.""" + preview_by_slot: dict[int, int] = {} + unit_by_slot: dict[int, int] = {} + setting_by_unit: dict[int, int] = {} + static_write_count = 0 + classified_write_count = 0 + exit_count = 0 + + for ins in scr.instructions: + write = _static_global_write(ins) + if write is None: + if sys4load.display_label(ins.opcode) == "exit": + exit_count += 1 + continue + raise ValueError( + f"{scr.path.name}: unclassified instruction at 0x{ins.offset:x}" + ) + + static_write_count += 1 + destination, value = write + if not isinstance(value, int): + raise ValueError( + f"{scr.path.name}: non-static voice-config value " + f"at 0x{ins.offset:x}" + ) + + preview_slot = destination - VOICE_CONFIG_PREVIEW_ASSET_ARRAY_BASE + if 0 <= preview_slot < VOICE_CONFIG_SLOT_COUNT: + _store_unique(preview_by_slot, preview_slot, value, preview_slot) + classified_write_count += 1 + continue + + unit_slot = destination - VOICE_CONFIG_SLOT_UNIT_ARRAY_BASE + if 1 <= unit_slot <= VOICE_CONFIG_NAMED_SLOT_COUNT: + _store_unique(unit_by_slot, unit_slot, value, unit_slot) + classified_write_count += 1 + continue + + unit_id = destination - VOICE_CONFIG_UNIT_SETTING_ARRAY_BASE + if ( + 0 <= unit_id < CHARACTER_NAME_RECORD_SPAN + and 1 <= value <= VOICE_CONFIG_NAMED_SLOT_COUNT + ): + _store_unique(setting_by_unit, unit_id, value, unit_id) + classified_write_count += 1 + continue + + raise ValueError( + f"{scr.path.name}: unclassified voice-config write " + f"0x{destination:x} at 0x{ins.offset:x}" + ) + + expected_preview_slots = set(range(VOICE_CONFIG_SLOT_COUNT)) + expected_named_slots = set(range(1, VOICE_CONFIG_NAMED_SLOT_COUNT + 1)) + if set(preview_by_slot) != expected_preview_slots: + raise ValueError( + f"{scr.path.name}: preview slots are " + f"{sorted(preview_by_slot)}, expected 0..{VOICE_CONFIG_SLOT_COUNT - 1}" + ) + if set(unit_by_slot) != expected_named_slots: + raise ValueError( + f"{scr.path.name}: named slots are {sorted(unit_by_slot)}, " + f"expected 1..{VOICE_CONFIG_NAMED_SLOT_COUNT}" + ) + if set(setting_by_unit.values()) != expected_named_slots: + raise ValueError( + f"{scr.path.name}: inverse setting ids are " + f"{sorted(setting_by_unit.values())}, expected " + f"1..{VOICE_CONFIG_NAMED_SLOT_COUNT}" + ) + if len(setting_by_unit) != VOICE_CONFIG_NAMED_SLOT_COUNT: + raise ValueError( + f"{scr.path.name}: expected {VOICE_CONFIG_NAMED_SLOT_COUNT} " + f"unit-to-setting writes, found {len(setting_by_unit)}" + ) + for slot, unit_id in unit_by_slot.items(): + if setting_by_unit.get(unit_id) != slot: + raise ValueError( + f"{scr.path.name}: slot {slot} -> unit {unit_id} does not " + f"round-trip through the inverse map" + ) + if exit_count != 1: + raise ValueError( + f"{scr.path.name}: expected one exit, found {exit_count}" + ) + + unit_records, _ = extract_name(sys4load.load(resolve("EBINIT"))) + unit_names = {record["id"]: record["name"] for record in unit_records} + missing_unit_ids = sorted(set(unit_by_slot.values()) - set(unit_names)) + if missing_unit_ids: + raise ValueError( + f"{scr.path.name}: unknown EBINIT unit ids {missing_unit_ids}" + ) + + asset_names = callscript_names() + preview_key = f"0x{VOICE_CONFIG_PREVIEW_ASSET_ARRAY_BASE:x}" + unit_key = f"0x{VOICE_CONFIG_SLOT_UNIT_ARRAY_BASE:x}" + inverse_base = f"0x{VOICE_CONFIG_UNIT_SETTING_ARRAY_BASE:x}" + records = [] + for slot in range(VOICE_CONFIG_SLOT_COUNT): + preview_asset_id = preview_by_slot[slot] + record = { + "id": slot, + "name": ( + "system_voice" if slot == 0 else unit_names[unit_by_slot[slot]] + ), + "slot_kind": "system" if slot == 0 else "character", + "preview_voice_asset_id": preview_asset_id, + "preview_voice_asset_name": asset_names.get(preview_asset_id, ""), + "fields": {preview_key: preview_asset_id}, + "array_fields": {}, + } + if slot: + unit_id = unit_by_slot[slot] + inverse_key = f"{inverse_base}/{unit_id}" + record["fields"][unit_key] = unit_id + record["array_fields"][inverse_key] = slot + record.update({ + "unit_id": unit_id, + "unit_name": unit_names[unit_id], + "voice_suppression_setting_id": slot, + "speaker_seen_flag_address": ( + f"0x{VOICE_CONFIG_SPEAKER_SEEN_ARRAY_BASE + unit_id:x}" + ), + }) + records.append(record) + + return records, { + "schema": "character-voice-configuration", + "slot_count": VOICE_CONFIG_SLOT_COUNT, + "system_slot": 0, + "named_character_slots": list( + range(1, VOICE_CONFIG_NAMED_SLOT_COUNT + 1) + ), + "preview_asset_array_base": ( + f"0x{VOICE_CONFIG_PREVIEW_ASSET_ARRAY_BASE:x}" + ), + "slot_unit_array_base": f"0x{VOICE_CONFIG_SLOT_UNIT_ARRAY_BASE:x}", + "unit_setting_array_base": ( + f"0x{VOICE_CONFIG_UNIT_SETTING_ARRAY_BASE:x}" + ), + "speaker_seen_array_base": ( + f"0x{VOICE_CONFIG_SPEAKER_SEEN_ARRAY_BASE:x}" + ), + "array_layouts": { + inverse_base: {"length": CHARACTER_NAME_RECORD_SPAN}, + }, + "array_field_columns": [ + f"{inverse_base}/{unit_id}" for unit_id in sorted(setting_by_unit) + ], + "static_write_count": static_write_count, + "classified_static_write_count": classified_write_count, + "exit_count": exit_count, + "classified_instruction_count": classified_write_count + exit_count, + "preview_asset_join_count": sum( + bool(record["preview_voice_asset_name"]) for record in records + ), + "unit_join_count": len(unit_by_slot), + "round_trip_mapping_count": sum( + setting_by_unit[unit_id] == slot + for slot, unit_id in unit_by_slot.items() + ), + "consumer_contract": { + "script": "CONFIG.BIN", + "preview": ( + "CONFIG indexes the thirteen preview assets by voice-setting " + "slot and plays the selected clip before changing that slot's " + "suppression flag." + ), + "character_rows": ( + "CONFIG lists slots 1..12 by resolving their unit ids through " + "the shared unit display-name table. A persisted per-unit " + "speaker-seen flag controls whether each row is available." + ), + "runtime_voice_filter": ( + "Story, history, field, and battle paths normalize a unit to " + "its voice family, map that representative unit through the " + "CVINIT inverse table, and test the selected one of thirteen " + "character_voice_suppressed settings." + ), + }, + } + + @cache def gallery_thumbnail_sheet_assets() -> dict[int, int]: """Read CGMODE's enabled thumbnail-sheet assets from INIT2.""" @@ -3443,6 +3638,10 @@ def write_data_index(data_dir: Path) -> None: "pages (hiragana, katakana, Latin, numerals, and symbols), preserving all reserved", "empty slots beside the 273 authored characters.", "", + "CVINIT's dedicated character-voice schema exposes CONFIG's thirteen preview", + "clips, twelve slot-to-unit joins, and the matching unit-to-suppression-setting", + "inverse map used by story, history, field, and battle voice filters.", + "", "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`", @@ -3526,6 +3725,8 @@ def main() -> int: extractor = extract_gallery_definitions elif mode == "numeric" and name == "ALINIT": extractor = extract_alchemy_recipes + elif mode == "numeric" and name == "CVINIT": + extractor = extract_voice_configuration recs, meta = extractor(scr) if mode == "name" and name in MESSAGE_TABLES: message_name = MESSAGE_TABLES[name] diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 1253195..d92b675 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -1272,6 +1272,93 @@ def test_name_entry_palette() -> None: ) +def test_voice_configuration() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["CVINIT.BIN"]) + check( + extract_init.detect_mode(script) == "numeric", + "CVINIT remains compatible with numeric-mode auto-detection", + ) + records, meta = extract_init.extract_voice_configuration(script) + by_id = {record["id"]: record for record in records} + check( + len(records) == 13 + and meta["static_write_count"] == 37 + and meta["classified_static_write_count"] == 37 + and meta["classified_instruction_count"] == 38, + "CVINIT classifies every preview, forward-map, inverse-map, and exit instruction", + ) + check( + [ + record["preview_voice_asset_name"] + for record in records + ] == [ + "EUA0065.OGG", + "LILA1424.OGG", + "SYL0675.OGG", + "SAS0328.OGG", + "VID0345.OGG", + "EST0562.OGG", + "NEL0192.OGG", + "TIO0209.OGG", + "COL0836.OGG", + "BRI0687.OGG", + "OKT0357.OGG", + "FEM0328.OGG", + "DEI0501.OGG", + ], + "CVINIT joins all thirteen preview clips to shipped OGG assets", + ) + check( + [by_id[slot]["unit_id"] for slot in range(1, 13)] + == [2, 5, 6, 7, 12, 14, 13, 10, 8, 9, 15, 11] + and meta["unit_join_count"] == 12, + "CVINIT joins its twelve named configuration slots to EBINIT units", + ) + check( + all( + by_id[slot]["array_fields"][ + f"0x628a7/{by_id[slot]['unit_id']}" + ] == slot + for slot in range(1, 13) + ) + and meta["round_trip_mapping_count"] == 12, + "CVINIT's slot-to-unit and unit-to-setting maps round-trip exactly", + ) + check( + by_id[0]["slot_kind"] == "system" + and "unit_id" not in by_id[0] + and by_id[1]["speaker_seen_flag_address"] == "0x56225" + and by_id[12]["speaker_seen_flag_address"] == "0x5622e", + "CVINIT preserves the non-unit system slot and speaker-seen joins", + ) + + semantics = extract_init.field_semantics( + records, meta["array_layouts"] + ) + check( + len(semantics) == 14 + and semantics["0x62cad"] == "character_voice_preview_asset_ids" + and semantics["0x62c8f"] == "character_voice_setting_unit_ids" + and semantics["0x628a7/11"] + == "unit_voice_suppression_flag_ids.index_11", + "CVINIT raw arrays join to their canonical voice-setting semantics", + ) + extract_init.attach_semantic_fields(records, semantics) + check( + by_id[12]["semantic_fields"][ + "character_voice_preview_asset_ids" + ] == 11412 + and by_id[12]["semantic_fields"][ + "character_voice_setting_unit_ids" + ] == 11 + and by_id[12]["semantic_fields"][ + "unit_voice_suppression_flag_ids.index_11" + ] == 12, + "CVINIT retains raw coordinates beside its joined semantic view", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -1485,6 +1572,7 @@ if __name__ == "__main__": test_alchemy_recipes() test_affinity_definitions() test_name_entry_palette() + test_voice_configuration() test_condition_definitions() test_field_semantics() if FAILS: diff --git a/vm-map/globals.toml b/vm-map/globals.toml index efb4656..6437858 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -1403,11 +1403,46 @@ address = "0x628a7" name = "unit_voice_suppression_flag_ids" category = "data-table" type = "int[1000]" -value_domain = "index into character_voice_suppressed" -usage = "Second stage of the shared voice-eligibility chain. Story, history, field, and battle paths normalize the current unit through unit_voice_family_unit_ids, index this table by that representative unit id, and suppress voice playback when the selected character_voice_suppressed cell is nonzero." +value_domain = "0 for no mapped setting; 1..12 index character_voice_suppressed" +usage = "CVINIT's unit-to-setting inverse map for the twelve named character voice controls. Story, history, field, and battle paths normalize the current unit through unit_voice_family_unit_ids, index this table by that representative unit id, and suppress voice playback when the selected character_voice_suppressed cell is nonzero." source = "investigation" confidence = "high" -depends_on = ["0x2e49"] +depends_on = ["0x2e49", "0x62c8f"] + +[[global]] +address = "0x56223" +name = "unit_story_speaker_seen_flags" +category = "data-table" +type = "int[1000]" +value_domain = "per-EBINIT-unit {0,1}; 1 after that unit has appeared as a story speaker" +usage = "Scene scripts set and persist a unit's cell when drawing that speaker's name. CONFIG reloads the twelve CVINIT-mapped unit cells and enables each named character-voice control only after its speaker has been encountered." +source = "investigation" +confidence = "high" +depends_on = ["0x315", "0x62c8f"] + +[[global]] +address = "0x62c8f" +name = "character_voice_setting_unit_ids" +category = "data-table" +type = "int[13]" +columns = { "0" = "system_or_reserved", "1" = "lily", "2" = "sylphine", "3" = "sasune", "4" = "vidal", "5" = "estelle", "6" = "nelly", "7" = "tiofania", "8" = "colette", "9" = "bridget", "10" = "octavia", "11" = "fam", "12" = "deirdre" } +value_domain = "slot 0 unwritten; slots 1..12 contain EBINIT unit ids" +usage = "CVINIT's CONFIG-row join. CONFIG iterates setting slots 1..12, resolves each unit id through unit_story_display_names for its label, and tests the same unit's unit_story_speaker_seen_flags cell before exposing the row. CVINIT also writes the exact inverse mapping to unit_voice_suppression_flag_ids." +source = "investigation" +confidence = "high" +depends_on = ["0x315", "0x56223", "0x628a7"] + +[[global]] +address = "0x62cad" +name = "character_voice_preview_asset_ids" +category = "data-table" +type = "int[13]" +columns = { "0" = "system_voice", "1" = "lily", "2" = "sylphine", "3" = "sasune", "4" = "vidal", "5" = "estelle", "6" = "nelly", "7" = "tiofania", "8" = "colette", "9" = "bridget", "10" = "octavia", "11" = "fam", "12" = "deirdre" } +value_domain = "packed OGG asset resource id" +usage = "CVINIT's thirteen voice-setting preview clips. CONFIG indexes this array by setting slot and plays the selected clip; slot 0 is the non-unit system voice, while slots 1..12 join to character_voice_setting_unit_ids." +source = "investigation" +confidence = "high" +depends_on = ["0x2e49", "0x62c8f"] [[global]] address = "0x62cd1"