diff --git a/docs/global-reference.md b/docs/global-reference.md index 4e9e56e..5085e8f 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5046 globals (290 curated, 4756 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5046 globals (293 curated, 4753 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -69,6 +69,8 @@ | `0x57357` | unit_deployment_cost_adjustments | high | investigation | Persistent per-unit delta added to unit_deployment_cost_base. CALCCC increments it by class_change_deployment_cost_delta, while SETCH, SETEN, ADDEXP, and deployment scripts include it when constructing or updating runtime command cost. | | `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. | | `0x665e3` | modal_annotation_horizontal_cells | high | investigation | Per-annotation horizontal anchor input shared by MES and SBUNKI. Each renderer multiplies the cell by 21 pixels and offsets it by half the annotation's rendered width. | | `0x66647` | modal_annotation_row_offsets | high | investigation | Per-annotation vertical row offset shared by MES and SBUNKI. Renderers multiply it by 30 pixels; a negative value additionally shifts the annotation left by one 21-pixel cell. | | `0x66716` | unit_voice_asset_ids | high | investigation | EBINIT per-unit voice bank for 116 voiced characters and variants. FIELD directly selects column 0 before WARPD, column 1 when an acting unit takes chest/treasure contents, and column 3 after occupying, losing, or sealing a stage objective. BTL selects the acting unit's columns 7..9 for ordinary attacks, 10 for an ordinary critical, odd columns 11/13/15 for skill uses, and even columns 12/14/16 for critical skills; the three variants follow its six-step exchange selector. If the target survives positive damage, BTL selects target columns 19/20/21 with 60/30/10 percent weights; column 22 belongs to a target reduced to zero HP, while actor column 23 is selected for a finishing blow. SHOWGROW selects column 24 after level gain. Populated columns 4..6 and 17..18 have no reachable selector in the shipped script corpus and are retained as explicit unused authoring slots; columns 17 and 18 duplicate columns 15 and 16 in all 116 populated rows. Columns: 0=warp, 1=treasure_capture, 3=objective_interaction, 4=unused_slot_4, 5=unused_slot_5, 6=unused_slot_6, 7=normal_attack_1, 8=normal_attack_2, 9=normal_attack_3, 10=critical_normal_attack, 11=skill_use_1, 12=critical_skill_1, 13=skill_use_2, 14=critical_skill_2, 15=skill_use_3, 16=critical_skill_3, 17=unused_slot_17, 18=unused_slot_18, 19=damage_reaction_1, 20=damage_reaction_2, 21=damage_reaction_3, 22=defeated, 23=finishing_blow, 24=level_up. | @@ -221,7 +223,6 @@ | `0x1a4` | — | low | auto-shape | array | | `0x1a9` | — | low | auto-shape | array | | `0x2e2` | — | low | auto-shape | array | -| `0x315` | — | low | auto-shape | array | | `0x41f` | — | low | auto-shape | array | | `0x4f1` | — | low | auto-shape | array | | `0x5c3` | — | low | auto-shape | array | @@ -281,8 +282,6 @@ | `0x62469` | — | low | auto-shape | array | | `0x6247d` | — | low | auto-shape | array | | `0x624a3` | — | low | auto-shape | array | -| `0x624bf` | — | low | auto-shape | array | -| `0x628a7` | — | low | auto-shape | array | | `0x62c8f` | — | low | auto-shape | array | | `0x62cad` | — | low | auto-shape | array | | `0x63c71` | — | low | auto-shape | array | @@ -465,6 +464,7 @@ | address | name | conf | source | usage | |---|---|---|---|---| | `0x27e` | unit_class_titles | high | investigation | Persistent per-unit class/title string table. CALCCC writes the selected class_change_title_output into the current unit's cell; character and status presentation scripts read the resulting title. | +| `0x315` | unit_story_display_names | high | investigation | CNINIT's sparse unit-id keyed story-name table. Scene scripts and HISTORY draw the selected speaker's value, DEBUGADV uses it for its scripted speaker previews, and INPUTNAME rejects a player-entered familiar name that collides with any populated row. The table intentionally leaves Lily's form ids 2..4 empty. | | `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. | diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index af15bc2..461dd3d 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -2835,3 +2835,23 @@ table meanings. **Next:** profile CNINIT as the next unresolved high-density name-mode table. Its current generic output has 32 records but 242 address-derived fields, making it the clearest candidate for another dedicated schema driven by reader-proven row/column ownership. + +## Data-semantics sidebar: CNINIT unit names and voice families (2026-07-23) + +CNINIT's former 32-record/242-field output was an ownership error, not a complex record. It is exactly two +parallel sparse arrays with a reserved span of 1,000, both keyed by EBINIT unit id. The string array holds +story/display names used throughout scene scripts, HISTORY, and DEBUGADV; INPUTNAME also scans it when +rejecting a player-entered familiar name that collides with an existing name. The integer array maps a unit +variant to a representative voice-family unit id before the shared voice-suppression lookup. All 277 +integer ids join exactly to EBINIT, 274 carry authored names, and Lily's form ids 2..4 are the three +intentional unnamed rows. + +The dedicated schema now emits 277 records with one raw string field and one raw integer field, their +canonical semantic names, the source unit's EBINIT definition name, the representative voice-family +definition name, and an explicit alias flag. Of the 277 rows, 175 normalize to another unit id; this +captures brainwashed, concealed-name, allied, boss, EX, corrupted, and trial variants without conflating +their story label with EBINIT's authoring label. Every CNINIT instruction is structurally accounted for, +and the generated profile has collapsed from 242 pseudo-fields to the two true consumer-visible arrays. + +**Next:** audit CGINIT's large numeric output. Determine whether its apparent 1,304-field surface is a +legitimate gallery/resource matrix or another ownership artifact before assigning any new semantics. diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 6075389..a14734e 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -57,10 +57,11 @@ 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, and ILINIT condition-matrix extraction; RECOVER ABI validation; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked profiling. | run each directly | — | +| `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, and CNINIT unit-name/voice-family 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, +CNINIT's 277 EBINIT-keyed display-name/voice-family rows within parallel 1,000-cell arrays, CIINIT's 24 populated character-information profiles within four reserved 100-cell columns, MAINIT's eleven populated magic-action rows within a reserved 30-cell layout, and ILINIT's thirteen populated condition ids within a reserved 30-by-5 level layout. ILINIT classifies every authored write, adds nested diff --git a/tools/extract_init.py b/tools/extract_init.py index 4d42b1f..836478f 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -532,6 +532,10 @@ VOCABULARY_RECORD_TABLE_BASE = 0x15A2A9 VOCABULARY_RECORD_STRIDE = 3 VOCABULARY_RECORD_SPAN = 200 +CHARACTER_NAME_ARRAY_BASE = 0x315 +CHARACTER_VOICE_FAMILY_ARRAY_BASE = 0x624BF +CHARACTER_NAME_RECORD_SPAN = 1000 + CONDITION_RECORD_SPAN = 30 CONDITION_LEVEL_COUNT = 5 CONDITION_LEVEL_NAME_BASE = 0x25FA @@ -1253,6 +1257,112 @@ def extract_vocabulary(scr): } +def extract_character_names(scr): + """Extract CNINIT's unit-id keyed display-name and voice-family arrays.""" + by_id: dict[int, dict] = {} + integer_writes = 0 + string_writes = 0 + + for ins in scr.instructions: + write = _static_global_write(ins) + if write is not None: + destination, canonical_unit_id = write + record_id = destination - CHARACTER_VOICE_FAMILY_ARRAY_BASE + if not ( + 1 <= record_id < CHARACTER_NAME_RECORD_SPAN + and isinstance(canonical_unit_id, int) + ): + raise ValueError( + f"{scr.path.name}: unexpected integer write 0x{destination:x}" + ) + if record_id in by_id: + raise ValueError( + f"{scr.path.name}: duplicate unit-name row {record_id}" + ) + by_id[record_id] = { + "id": record_id, + "name": None, + "canonical_voice_unit_id": canonical_unit_id, + "string_fields": {}, + "fields": { + f"0x{CHARACTER_VOICE_FAMILY_ARRAY_BASE:x}": canonical_unit_id, + }, + } + integer_writes += 1 + continue + + if ( + ins.opcode == SET_STRING + and len(ins.args) >= 2 + and ins.args[0][0] == T_GLOBAL_STRING + ): + record_id = ins.args[0][1] - CHARACTER_NAME_ARRAY_BASE + if not (1 <= record_id < CHARACTER_NAME_RECORD_SPAN): + raise ValueError( + f"{scr.path.name}: unexpected name write 0x{ins.args[0][1]:x}" + ) + if record_id not in by_id: + raise ValueError( + f"{scr.path.name}: name without unit mapping for row {record_id}" + ) + text = scr.strings.get(ins.args[1][1], (None,))[0] + by_id[record_id]["name"] = text + by_id[record_id]["string_fields"][ + f"0x{CHARACTER_NAME_ARRAY_BASE:x}" + ] = text + string_writes += 1 + continue + + if sys4load.display_label(ins.opcode) != "exit": + raise ValueError( + f"{scr.path.name}: unexpected opcode " + f"{sys4load.display_label(ins.opcode)} at 0x{ins.offset:x}" + ) + + unit_definitions = { + record["id"]: record + for record in extract_name(sys4load.load(resolve("EBINIT")))[0] + } + unit_ids = set(by_id) + definition_ids = set(unit_definitions) + for record in by_id.values(): + unit_definition = unit_definitions.get(record["id"]) + canonical_definition = unit_definitions.get( + record["canonical_voice_unit_id"] + ) + if unit_definition: + record["unit_definition_name"] = unit_definition["name"] + if canonical_definition: + record["canonical_voice_unit_name"] = canonical_definition["name"] + record["voice_family_alias"] = ( + record["canonical_voice_unit_id"] != record["id"] + ) + + records = [by_id[record_id] for record_id in sorted(by_id)] + return records, { + "schema": "unit-display-names", + "record_span": CHARACTER_NAME_RECORD_SPAN, + "first_record_id": 1, + "name_array_base": f"0x{CHARACTER_NAME_ARRAY_BASE:x}", + "canonical_voice_unit_array_base": ( + f"0x{CHARACTER_VOICE_FAMILY_ARRAY_BASE:x}" + ), + "integer_write_count": integer_writes, + "string_write_count": string_writes, + "named_record_count": sum(record["name"] is not None for record in records), + "unnamed_record_ids": [ + record["id"] for record in records if record["name"] is None + ], + "voice_family_alias_count": sum( + record["voice_family_alias"] for record in records + ), + "unit_definition_table": "EBINIT", + "joined_unit_definition_count": len(unit_ids & definition_ids), + "cninit_ids_without_unit_definition": sorted(unit_ids - definition_ids), + "unit_definition_ids_without_cninit": sorted(definition_ids - unit_ids), + } + + def extract_recovery_protocol(scr) -> dict: """Validate and describe RECOVER's resource/condition reset ABI.""" lookups_2d = { @@ -2428,6 +2538,11 @@ def write_data_index(data_dir: Path) -> None: "policies, and icon ids. Top-level `recovery_protocol` validates RECOVER.BIN and links", "current levels, equipment/passive baselines, remaining turns, and full resource restore.", "", + "CNINIT's dedicated unit-name schema exposes 277 sparse EBINIT-keyed rows in two", + "parallel 1,000-cell arrays: story/display names and canonical voice-family unit ids.", + "Every row joins both its own EBINIT definition and the representative voice-family", + "definition; deliberately empty names and variant aliases remain explicit.", + "", "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`", @@ -2495,6 +2610,8 @@ def main() -> int: }[mode] if mode == "name" and name == "VIINIT": extractor = extract_vocabulary + elif mode == "name" and name == "CNINIT": + extractor = extract_character_names elif mode == "name" and name == "CIINIT": extractor = extract_character_profiles elif mode == "name" and name == "MAINIT": diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 1d7b587..78355ce 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -911,6 +911,65 @@ def test_message_join() -> None: ) +def test_character_names() -> None: + scripts = paths.scripts() + records, meta = extract_init.extract_character_names( + sys4load.load(scripts["CNINIT.BIN"]) + ) + by_id = {record["id"]: record for record in records} + check( + len(records) == 277 + and meta["record_span"] == 1000 + and meta["integer_write_count"] == 277 + and meta["string_write_count"] == 274, + "CNINIT extracts its complete sparse 1,000-row pair of arrays", + ) + check( + meta["joined_unit_definition_count"] == 277 + and not meta["cninit_ids_without_unit_definition"] + and not meta["unit_definition_ids_without_cninit"], + "CNINIT ids join exactly to all 277 EBINIT definitions", + ) + check( + meta["unnamed_record_ids"] == [2, 3, 4] + and all(by_id[unit_id]["name"] is None for unit_id in (2, 3, 4)), + "CNINIT preserves Lily's three deliberately unnamed form rows", + ) + check( + by_id[1]["name"] == "エミリオ" + and by_id[1]["fields"]["0x624bf"] == 1 + and by_id[1]["string_fields"]["0x315"] == "エミリオ", + "CNINIT keeps display names and voice-family ids as parallel fields", + ) + check( + meta["voice_family_alias_count"] == 175 + and by_id[21]["canonical_voice_unit_id"] == 5 + and by_id[21]["unit_definition_name"] == "シルフィーヌ(洗脳状態)" + and by_id[21]["canonical_voice_unit_name"] == "シルフィーヌ", + "CNINIT joins variant rows to their canonical voice-family definitions", + ) + check( + by_id[800]["name"] == "アセンブリア" + and by_id[800]["canonical_voice_unit_id"] == 240, + "CNINIT preserves late EX/BOSS aliases without collapsing sparse ids", + ) + + semantics = extract_init.field_semantics(records) + check( + semantics == { + "0x315": "unit_story_display_names", + "0x624bf": "unit_voice_family_unit_ids", + }, + "CNINIT's two raw arrays join to their canonical semantic names", + ) + extract_init.attach_semantic_fields(records, semantics) + check( + by_id[21]["semantic_fields"]["unit_story_display_names"] == "シルフィーヌ" + and by_id[21]["semantic_fields"]["unit_voice_family_unit_ids"] == 5, + "CNINIT records retain raw arrays beside one semantic view", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -1119,6 +1178,7 @@ if __name__ == "__main__": test_real_message_tables() test_message_infrastructure() test_message_join() + test_character_names() test_condition_definitions() test_field_semantics() if FAILS: diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 8c0331b..a81ab08 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -1376,6 +1376,39 @@ source = "investigation" confidence = "high" depends_on = ["0x52383"] +[[global]] +address = "0x315" +name = "unit_story_display_names" +category = "string-table" +type = "string[1000]" +value_domain = "CNINIT-authored speaker/display name indexed by EBINIT unit id" +usage = "CNINIT's sparse unit-id keyed story-name table. Scene scripts and HISTORY draw the selected speaker's value, DEBUGADV uses it for its scripted speaker previews, and INPUTNAME rejects a player-entered familiar name that collides with any populated row. The table intentionally leaves Lily's form ids 2..4 empty." +source = "investigation" +confidence = "high" +depends_on = ["0x624bf"] + +[[global]] +address = "0x624bf" +name = "unit_voice_family_unit_ids" +category = "data-table" +type = "int[1000]" +value_domain = "EBINIT unit id" +usage = "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." +source = "investigation" +confidence = "high" +depends_on = ["0x628a7", "0x2e49"] + +[[global]] +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." +source = "investigation" +confidence = "high" +depends_on = ["0x2e49"] + [[global]] address = "0x52383" name = "entity_condition_levels"