Decode CVINIT voice configuration
This commit is contained in:
@@ -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<col_base>": 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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user