#!/usr/bin/env python3 """Regression tests for INIT table extraction. Run: py -3.11 -X utf8 tools/test_extract_init.py """ from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import extract_init import extract_message_table import paths import sys4load FAILS: list[str] = [] def check(condition: bool, message: str) -> None: print((" ok " if condition else " FAIL ") + message) if not condition: FAILS.append(message) def test_real_name_tables() -> None: expected = { "SKINIT.BIN": (300, 131), "ITINIT.BIN": (1000, 287), "EBINIT.BIN": (1000, 277), "OBINIT.BIN": (100, 46), } scripts = paths.scripts() for name, (span, count) in expected.items(): records, meta = extract_init.extract_name(sys4load.load(scripts[name])) check(meta["record_span"] == span, f"{name}: record span is {span}") check(len(records) == count, f"{name}: extracts {count} named records") check(len({record["id"] for record in records}) == count, f"{name}: record ids are unique") items, _ = extract_init.extract_name(sys4load.load(scripts["ITINIT.BIN"])) by_id = {record["id"]: record for record in items} check(by_id[1]["name"] == "銅の鍵", "ITINIT item 1 is the copper key") check(len(by_id[1]["fields"]) == 5, "ITINIT item 1 owns only its five fields") check("desc" not in by_id[1], "ITINIT item 1 has no fabricated description") check(by_id[101]["desc"] == "HP30回復", "ITINIT item 101 keeps its description") check(by_id[1]["fields"]["0x8c879"] == 10, "ITINIT columns use the runtime lookup base") check(len({key for record in items for key in record["fields"]}) == 13, "ITINIT has thirteen parallel-array fields") check(len({key for record in items for key in record.get("record_fields", {})}) == 44, "ITINIT linked row-major tables expose 44 populated columns") check(by_id[101]["record_fields"]["0xa5301/3/0"] == 30, "ITINIT item 101 stores HP recovery in row-major column zero") check(by_id[108]["record_fields"]["0x906f9/30/8"] == -5, "ITINIT item 108 preserves its paralysis-removal delta") units, _ = extract_init.extract_name(sys4load.load(scripts["EBINIT.BIN"])) unit_by_id = {record["id"]: record for record in units} check(len({ key for record in units for key in { **record.get("fields", {}), **record.get("record_fields", {}), } }) == 108, "EBINIT exposes 108 genuine populated fields") check(len({ key for record in units for key in record.get("record_fields", {}) }) == 82, "EBINIT exposes 82 genuine linked row-major columns") check( unit_by_id[456]["fields"]["0x6fb86"] == 12585 and "0x4e693/300/91" not in unit_by_id[456].get("record_fields", {}), "EBINIT unit 456 keeps its battle sprite in the parallel asset column", ) check( unit_by_id[600]["fields"]["0x7a37e"] == 80 and "0x4e693/300/35" not in unit_by_id[600].get("record_fields", {}), "EBINIT unit 600 keeps its starting level in the parallel level column", ) objects, _ = extract_init.extract_name(sys4load.load(scripts["OBINIT.BIN"])) object_by_id = {record["id"]: record for record in objects} check(object_by_id[17]["name"] == "針" and object_by_id[17]["desc"] == "HP-2", "OBINIT object 17 preserves its name and effect description") check(object_by_id[28]["name"] == "カード取得", "OBINIT object ids provide authoritative STINIT type labels") vocabulary, vocabulary_meta = extract_init.extract_vocabulary( sys4load.load(scripts["VIINIT.BIN"]) ) vocabulary_by_id = {record["id"]: record for record in vocabulary} check( vocabulary_meta["record_span"] == 200 and len(vocabulary) == 65 and vocabulary[-1]["id"] == 120, "VIINIT extracts 65 sparse glossary topics from its 200-row table", ) check( vocabulary_by_id[1]["name"] == "【迷宮】占有率" and vocabulary_by_id[1]["record_fields"]["0x15a2a9/3/0"] == 201, "VIINIT associates pre-name unlock writes with the correct topic", ) def test_static_negative_write() -> None: class Instruction: opcode = extract_init.SUB args = [(extract_init.T_GLOBAL_INT, 0x123), (extract_init.T_IMM, 0), (extract_init.T_IMM, 7)] check(extract_init._static_global_write(Instruction()) == (0x123, -7), "INIT subtraction writes preserve negative values") def test_output_name_validation() -> None: check(extract_init.normalize_outname("ITINIT.json") == "ITINIT", "INIT output names tolerate one JSON suffix") try: extract_init.normalize_outname("build/data/ITINIT.json") except ValueError: rejected = True else: rejected = False check(rejected, "INIT output names reject nested paths") def test_real_mixed_table() -> None: script = sys4load.load(extract_init.resolve("STINIT")) check(extract_init.detect_mode(script) == "mixed", "STINIT auto-detects as a mixed selector table") records, meta = extract_init.extract_mixed(script) check(len(records) == 74, "STINIT extracts all 74 sparse stage records") check(records[0]["id"] == 1 and records[-1]["id"] == 170, "STINIT preserves sparse runtime stage ids") check(meta["selector_global"] == "0x4dfbc", "STINIT records are keyed by scjump_progress_a") check(meta["array_layouts"]["0xe74b5"] == { "length": 350, "stride": 7, "rows": 50, }, "STINIT preamble recovers a consumer-confirmed row buffer") stage1 = records[0] check(list(stage1["string_fields"].values()) == [ "オークの撃破", "", "自軍拠点の制圧", "50ターン経過", ], "STINIT stage 1 preserves all four condition strings") check(stage1["fields"]["0xe7302"] == 12 and stage1["fields"]["0xe730c"] == 50 and stage1["fields"]["0xe730d"] == 0, "STINIT stage 1 preserves BGM and turn-limit scalars") check(stage1["array_fields"]["0xe7305/3"] == -2, "STINIT fixed-buffer cells preserve negative values") check(stage1["footer_arrays"]["0xe7889/3"]["values"] == [1, 1, 1], "STINIT length-prefixed footer arrays retain their destination") check(sum(len(record.get("footer_arrays", {})) for record in records) == 1396, "STINIT accounts for every footer-array copy") extract_init.attach_stage_object_placements(records) first_object = records[0]["object_placements"][0] check(first_object == { "slot": 1, "type_id": 1, "type_name": "拠点", "type_description": "▲命中・回避・防御", "tile_x": 13, "tile_y": 1, "difficulty_mask": 7, "initial_faction_id": 1, }, "STINIT joins confirmed object buffers into one placement record") stage1_slot6 = next( obj for obj in records[0]["object_placements"] if obj["slot"] == 6 ) check(stage1_slot6["item_id"] == 222 and stage1_slot6["item_quantity"] == 1, "STINIT object payloads decode by object type") stage1_slot5 = next( obj for obj in records[0]["object_placements"] if obj["slot"] == 5 ) check(stage1_slot5["reinforcement_interval_turns"] == 10 and stage1_slot5["reinforcement_spawn_limit"] == 3, "STINIT object placements expose reinforcement schedules") stage1_slot8 = next( obj for obj in records[0]["object_placements"] if obj["slot"] == 8 ) check(stage1_slot8["card_generation_list_id"] == 1, "STINIT card objects expose their generation-list id") check(stage1_slot8["type_name"] == "カード取得", "STINIT object placements join OBINIT type names") stage1_slot9 = next( obj for obj in records[0]["object_placements"] if obj["slot"] == 9 ) check(stage1_slot9["destination_tile_x"] == 13 and stage1_slot9["destination_tile_y"] == 7, "STINIT teleport payloads expose destination coordinates") stage2_slot3 = next( obj for obj in records[1]["object_placements"] if obj["slot"] == 3 ) check(stage2_slot3["required_story_flags"] == [902], "STINIT object placements join positive story prerequisites") all_objects = [ obj for record in records for obj in record["object_placements"] ] check(sum("non_triggering_faction_id" in obj for obj in all_objects) == 104, "STINIT hazard/barrier payloads expose their FIELD-proven faction gate") check({ obj["non_triggering_faction_id"] for obj in all_objects if "non_triggering_faction_id" in obj } == {1, 2, 3}, "STINIT faction gates retain all observed faction ids") state_objects = [ obj for obj in all_objects if "initial_object_state_id" in obj ] check(len(state_objects) == 78, "STINIT state-row metadata decodes every remaining initialized object state") check({ obj["type_id"] for obj in state_objects } == {11, 17, 26}, "STINIT state-row payloads remain scoped to proven object types") deployment_flags = [ obj for obj in state_objects if obj["type_id"] == 26 ] check(len(deployment_flags) == 61 and {obj["initial_object_state_id"] for obj in deployment_flags} == {2}, "STINIT deployment flags expose their initial object state") spike = next( obj for obj in state_objects if obj["type_id"] == 17 ) check(spike["initial_object_state_id"] == 2 and "non_triggering_faction_id" not in spike, "STINIT spikes expose state without inventing the excluded faction gate") ignored_objects = [ obj for obj in all_objects if "ignored_payload_fields" in obj ] check(len(ignored_objects) == 3 and {obj["type_id"] for obj in ignored_objects} == {27} and {tuple(obj["ignored_payload_fields"].items()) for obj in ignored_objects} == {(("0xe73bb", 2),)}, "STINIT preserves explicitly written but engine-ignored type-27 payloads") unknown_objects = [obj for obj in all_objects if "unknown_fields" in obj] check(not unknown_objects, "STINIT has no unresolved populated tagged object payloads") extract_init.attach_stage_enemy_spawns(records) first_spawn = records[0]["enemy_spawns"][0] check(first_spawn == { "slot": 1, "unit_id": 205, "faction_id": 2, "difficulty_mask": 7, "min_level": 1, "max_level": 10, "auto_level_scale_divisor": 1, "object_slot": 2, "movement_routine_set_ids": [1, 1, 1], "forbidden_story_flags": [11], "first_clear_only": True, }, "STINIT joins confirmed enemy buffers into one spawn record") stage1_slot2 = records[0]["enemy_spawns"][1] check(stage1_slot2["random_selection_weight"] == 1 and stage1_slot2["object_slot"] == 5, "STINIT preserves weighted object-linked enemy alternatives") check(sum(len(record["enemy_spawns"]) for record in records) == 1378, "STINIT assembles every populated enemy spawn slot") linked_deployment_spawns = [] for record in records: objects_by_slot = { obj["slot"]: obj for obj in record["object_placements"] } linked_deployment_spawns.extend( spawn for spawn in record["enemy_spawns"] if (object_slot := spawn.get("object_slot")) in objects_by_slot and objects_by_slot[object_slot]["type_id"] == 26 ) check(len(linked_deployment_spawns) == 63 and {spawn["faction_id"] for spawn in linked_deployment_spawns} == {2}, "STINIT deployment flags correlate with all linked enemy-faction spawns") check(sum( spawn.get("first_clear_only", False) for record in records for spawn in record["enemy_spawns"] ) == 485, "STINIT exposes every first-clear-only enemy gate") def test_real_class_change_rules() -> None: script = sys4load.load(extract_init.resolve("CCINIT")) check(extract_init.detect_mode(script) == "rules", "CCINIT auto-detects as a conditional rule program") records, meta = extract_init.extract_class_change_rules(script) check(len(records) == 71, "CCINIT extracts all 71 class-change rules") check(len({record["unit_id"] for record in records}) == 33, "CCINIT rules cover 33 unit definitions") check(meta["selector_global"] == "0x66715" and meta["persistent_state_table"] == "0x573bb", "CCINIT exposes its unit selector and persistent state table") check(records[0]["unit_name"] == "リリィ:少女時代" and records[0]["selected_level"] == -1 and records[0]["excluded_when_unit_equals_global"] == "0x32f0", "CCINIT preserves Lily's level-independent form rule") sylphine = records[2] check(sylphine["unit_id"] == 5 and sylphine["minimum_level"] == 50 and sylphine["class_change_slot_index"] == 0 and sylphine["title"] == "聖王女", "CCINIT decodes unit, level, slot, and awarded title") check(sylphine["deployment_cost_delta"] == 2 and sylphine["stat_bonuses"]["physical_attack"] == 3, "CCINIT decodes cost and named stat bonuses") check(sylphine["skill_awards"] == [{ "skill_slot": 3, "skill_id": 202, "skill_name": "光燐衝撃", }], "CCINIT joins awarded skill ids to SKINIT names") semantics = extract_init.field_semantics(records, meta["array_layouts"]) check(semantics["0x26b4"] == "class_change_title_output" and semantics["0xab8e9/2"] == "class_change_stat_bonuses.physical_attack" and semantics["0xab8f7/2"] == "class_change_skill_awards.skill_slot_3", "CCINIT raw outputs join to canonical global and column names") extract_init.attach_semantic_fields(records, semantics) check(sylphine["semantic_fields"]["class_change_title_output"] == "聖王女" and sylphine["semantic_fields"][ "class_change_stat_bonuses.physical_attack" ] == 3, "CCINIT rules expose a single semantic field view") check(all( record["minimum_level"] == record["selected_level"] for record in records[2:] ), "CCINIT normal promotion thresholds match their selected levels") check(sum(len(record.get("skill_awards", [])) for record in records) == 30, "CCINIT accounts for all 30 awarded skills") def test_real_scene_dispatch() -> None: script = sys4load.load(extract_init.resolve("SCINIT")) check(extract_init.detect_mode(script) == "dispatch", "SCINIT auto-detects as paired scene dispatch arrays") records, meta = extract_init.extract_dispatch(script) by_id = {record["id"]: record for record in records} check(len(records) == 1209 and meta["assignment_count"] == 2179, "SCINIT preserves all assignments and 1,209 final decision rows") check(meta["script_resource_array_base"] == "0x87a57" and meta["authored_chapter_array_base"] == "0x8a167" and meta["reserved_array_span"] == 10000, "SCINIT exposes its paired 10,000-cell array layout") check(by_id[0]["script_resource_id"] == 34 and by_id[0]["script_name"] == "SC0000.BIN" and by_id[0]["authored_chapter"] == 1, "SCINIT joins packed resource ids to scene names and chapter metadata") check(by_id[1]["assignment_count"] == 3 and [entry["authored_chapter"] for entry in by_id[1]["assignments"]] == [1, 5, 5], "SCINIT retains source-ordered overwrites rather than only the final cell") check(meta["resolved_script_count"] == 1209 and len({record["script_resource_id"] for record in records}) == 135, "every final SCINIT row resolves to one of 135 numbered scene scripts") check(meta["scjump_joined_record_count"] == 847 and meta["scjump_chapter_match_count"] == 844 and [row["decision_id"] for row in meta["scjump_chapter_mismatches"]] == [250, 1001, 1005], "SCINIT chapter tags cross-check against every live SCJUMP decision") semantics = extract_init.field_semantics(records) check(semantics["0x87a57"] == "scjump_scene_script_resource_ids" and semantics["0x8a167"] == "scjump_authored_chapters", "SCINIT's paired columns join to canonical semantic names") def test_real_routine_banks() -> None: script = sys4load.load(extract_init.resolve("RTINIT")) check(extract_init.detect_mode(script) == "banked", "RTINIT auto-detects as parallel routine-step banks") records, meta = extract_init.extract_banked(script) by_id = {record["id"]: record for record in records} check(len(records) == 172 and meta["first_record_id"] == 1 and meta["last_record_id"] == 176 and meta["missing_record_ids"] == [150, 151, 152, 153], "RTINIT preserves its sparse one-based routine-set ids") check(meta["bank_root_base"] == "0xeff78" and meta["bank_span"] == 20000 and meta["bank_count"] == 20 and meta["record_stride"] == 20 and meta["reserved_record_span"] == 1000, "RTINIT exposes twenty parallel 1000-by-20 banks") check(meta["assignment_count"] == 3336 and meta["populated_cell_count"] == 3307 and meta["overwritten_cell_count"] == 29 and meta["conflicting_overwrite_count"] == 11, "RTINIT preserves source assignments and final overwrite accounting") check(meta["movement_step_count"] == 1043 and meta["battle_step_count"] == 14 and len(meta["used_movement_provider_selectors"]) == 19 and len(meta["used_battle_provider_selectors"]) == 4 and len(meta["record_field_columns"]) == 117, "RTINIT assembles every populated movement and battle step") check(meta["decoded_movement_provider_count"] == 19 and meta["decoded_movement_step_count"] == 1043 and meta["decoded_movement_parameter_count"] == 974 and meta["decoded_movement_defaulted_parameter_count"] == 13 and meta["ignored_movement_parameter_count"] == 3, "RTINIT reports selector-specific semantic coverage") check([ layout["bank_index"] for layout in meta["bank_layouts"].values() if layout["reserved_empty"] ] == [6, 13, 14, 15, 16, 17], "RTINIT keeps all six reserved empty banks in its structural layout") movement = by_id[1]["movement_steps"][0] battle = by_id[1]["battle_steps"][0] check(movement["movement_provider_selector"] == 1 and movement["movement_activation_percent"] == 100 and movement["provider_script"] == "RTN_M001.BIN", "RTINIT joins movement selectors and activation percentages") check(battle["battle_provider_selector"] == 1 and battle["battle_activation_percent"] == 100 and battle["provider_script"] == "RTN_B001.BIN", "RTINIT joins battle selectors and activation percentages") check(by_id[2]["battle_steps"][0]["battle_parameter_1"] == 219 and by_id[2]["battle_steps"][0]["provider_script"] == "RTN_B004.BIN", "RTINIT retains provider-specific battle parameters") check(by_id[173]["movement_steps"][0]["movement_parameter_1"] == 2 and by_id[173]["movement_steps"][0]["movement_parameter_2"] == 158, "RTINIT final rows reflect source-ordered conflicting overwrites") provider_5 = by_id[5]["movement_steps"][2] check(provider_5["movement_provider_selector"] == 5 and provider_5["provider_behavior"] == "approach_destination_tile" and provider_5["destination_tile_x"] == 24 and provider_5["destination_tile_y"] == 123 and provider_5["movement_parameter_1"] == 24 and provider_5["movement_parameter_2"] == 123, "RTINIT joins RTN_M005 destination semantics without replacing raw banks") provider_11 = by_id[87]["movement_steps"][2] check(provider_11["movement_provider_selector"] == 11 and provider_11["provider_behavior"] == "cycle_destination_waypoints" and provider_11["destination_tile_x"] == 3 and provider_11["destination_tile_y"] == 499 and provider_11["waypoint_ordinal"] == 1 and provider_11["path_cost_limit_override"] == 2, "RTINIT joins all four RTN_M011 waypoint parameters") provider_7 = by_id[33]["movement_steps"][4] check(provider_7["movement_provider_selector"] == 7 and provider_7["provider_behavior"] == "approach_injured_ally" and provider_7["maximum_target_route_steps"] == 5 and provider_7["maximum_target_hp_percent"] == 80, "RTINIT joins RTN_M007 injured-ally search semantics") provider_10 = by_id[99]["movement_steps"][0] check(provider_10["movement_provider_selector"] == 10 and provider_10["provider_behavior"] == "approach_healing_feather" and provider_10["resource_index"] == 0 and provider_10["maximum_resource_percent"] == 50 and "movement_parameter_1" not in provider_10, "RTINIT projects RTN_M010's implicit HP default beside the raw banks") provider_12 = by_id[168]["movement_steps"][0] check(provider_12["movement_provider_selector"] == 12 and provider_12["provider_behavior"] == "approach_destination_tile_avoiding_foreign_entities" and provider_12["destination_tile_x"] == 11 and provider_12["destination_tile_y"] == 68, "RTINIT joins RTN_M012's route-mode destination semantics") provider_4 = by_id[5]["movement_steps"][0] check(provider_4["movement_provider_selector"] == 4 and provider_4["provider_behavior"] == "approach_stage_object_slot" and provider_4["stage_object_slot_index"] == 1, "RTINIT joins RTN_M004's stage-object slot semantics") provider_4_default = by_id[17]["movement_steps"][1] check(provider_4_default["movement_provider_selector"] == 4 and provider_4_default["stage_object_slot_index"] == 0 and "movement_parameter_1" not in provider_4_default, "RTINIT projects RTN_M004's implicit stage-object slot zero") provider_6 = by_id[6]["movement_steps"][0] check(provider_6["movement_provider_selector"] == 6 and provider_6["provider_behavior"] == "approach_nearest_enemy" and provider_6["maximum_target_route_steps"] == 5, "RTINIT joins RTN_M006's enemy-search radius") provider_15 = by_id[2]["movement_steps"][0] check(provider_15["movement_provider_selector"] == 15 and provider_15["provider_behavior"] == "approach_foreign_magic_pillar" and provider_15["maximum_target_route_steps"] == 3, "RTINIT joins RTN_M015's foreign Magic Pillar search radius") provider_1_residue = by_id[173]["movement_steps"][2] check(provider_1_residue["movement_provider_selector"] == 1 and provider_1_residue["provider_behavior"] == "advance_step_progress" and provider_1_residue["ignored_movement_parameters"] == { "movement_parameter_1": 10, "movement_parameter_2": 711, }, "RTINIT marks RTN_M001's authored but unread parameter cells") provider_8_residue = by_id[112]["movement_steps"][6] check(provider_8_residue["movement_provider_selector"] == 8 and provider_8_residue["provider_behavior"] == "approach_nearest_foreign_magic_pillar" and provider_8_residue["ignored_movement_parameters"] == {"movement_parameter_1": 1}, "RTINIT marks RTN_M008's authored but unread parameter cell") provider_13 = by_id[104]["movement_steps"][3] check(provider_13["movement_provider_selector"] == 13 and provider_13["provider_behavior"] == "approach_faction_traversable_tile" and provider_13["target_faction_filter"] == 1, "RTINIT joins RTN_M013's explicit faction filter") provider_13_default = by_id[23]["movement_steps"][1] check(provider_13_default["movement_provider_selector"] == 13 and provider_13_default["target_faction_filter"] == 0 and "movement_parameter_1" not in provider_13_default, "RTINIT projects RTN_M013's any-foreign-faction default") provider_14 = by_id[130]["movement_steps"][5] check(provider_14["movement_provider_selector"] == 14 and provider_14["provider_behavior"] == "retreat_from_nearby_enemies" and provider_14["maximum_threat_route_steps"] == 6, "RTINIT joins RTN_M014's threat-detection radius") provider_3 = by_id[3]["movement_steps"][3] check(provider_3["movement_provider_selector"] == 3 and provider_3["provider_behavior"] == "route_toward_reachable_normal_attack_target" and "movement_parameter_1" not in provider_3, "RTINIT joins RTN_M003's parameterless attack-route behavior") provider_51 = by_id[3]["movement_steps"][1] check(provider_51["movement_provider_selector"] == 51 and provider_51["provider_behavior"] == "select_effective_attack_target_and_action" and "movement_parameter_1" not in provider_51, "RTINIT joins RTN_M051's parameterless attack-selection behavior") provider_17 = by_id[3]["movement_steps"][2] check(provider_17["movement_provider_selector"] == 17 and provider_17["provider_behavior"] == "route_toward_lowest_hp_reachable_normal_attack_target" and "movement_parameter_1" not in provider_17, "RTINIT joins RTN_M017's low-HP attack-route behavior") provider_52 = by_id[3]["movement_steps"][0] check(provider_52["movement_provider_selector"] == 52 and provider_52["provider_behavior"] == "select_lowest_hp_effective_attack_target_and_action" and "movement_parameter_1" not in provider_52, "RTINIT joins RTN_M052's low-HP attack-selection behavior") provider_2 = by_id[2]["movement_steps"][1] check(provider_2["movement_provider_selector"] == 2 and provider_2["provider_behavior"] == "roam_to_random_reachable_tile" and "movement_parameter_1" not in provider_2, "RTINIT joins RTN_M002's randomized roaming behavior") provider_9 = by_id[98]["movement_steps"][1] check(provider_9["movement_provider_selector"] == 9 and provider_9["provider_behavior"] == "approach_collectible_treasure" and "movement_parameter_1" not in provider_9, "RTINIT joins RTN_M009's collectible-treasure behavior") provider_61 = by_id[33]["movement_steps"][0] check(provider_61["movement_provider_selector"] == 61 and provider_61["provider_behavior"] == "select_lowest_hp_ally_and_healing_skill" and "movement_parameter_1" not in provider_61, "RTINIT joins RTN_M061's immediate healing behavior") check(all( "provider_behavior" in step for record in records for step in record.get("movement_steps", []) ), "RTINIT joins behavior semantics onto every shipped movement step") check( meta["movement_provider_parameter_schemas"]["11"]["parameter_fields"] == { "movement_parameter_1": "destination_tile_x", "movement_parameter_2": "destination_tile_y", "movement_parameter_3": "waypoint_ordinal", "movement_parameter_4": "path_cost_limit_override", }, "RTINIT publishes the reusable RTN_M011 parameter schema", ) check( meta["movement_provider_parameter_schemas"]["10"]["parameter_defaults"] == {"movement_parameter_1": 0}, "RTINIT publishes RTN_M010's implicit resource-index default", ) semantics = extract_init.field_semantics(records) check( semantics["0xeff78/20/0"] == "movement_routine_provider_selectors.column_0" and semantics["0x125ad8/20/0"] == "battle_routine_activation_percents.column_0", "RTINIT raw banks join to canonical structural field names", ) def test_real_message_tables() -> None: scripts = paths.scripts() expected = { "ITMES.BIN": (0x8C877, 287, "fallthrough"), "SKMES.BIN": (0xA6E59, 131, "fallthrough"), "VIMES.BIN": (0x15A2A8, 65, "branch-target"), "EIMES.BIN": (0x15A759, 192, "branch-target"), } for name, (selector, count, dispatch_layout) in expected.items(): records, meta = extract_message_table.extract_messages( sys4load.load(scripts[name]) ) check(meta["selector_global"] == f"0x{selector:x}", f"{name}: discovers selector global 0x{selector:x}") check(len(records) == count, f"{name}: extracts {count} messages") check(len(records) == meta["dispatch_guard_count"], f"{name}: every dispatch guard yields a message") check(len({record['id'] for record in records}) == len(records), f"{name}: message ids are unique") check(meta["dispatch_layout"] == dispatch_layout, f"{name}: recognizes its {dispatch_layout} dispatch layout") item_messages, _ = extract_message_table.extract_messages( sys4load.load(scripts["ITMES.BIN"]) ) items = {record["id"]: record for record in item_messages} check(items[1]["title"] == "【重要:銅の鍵】     LEVEL-E", "ITMES item 1 keeps its display title") check(items[1]["description"] == " 銅の扉を開閉することが可能", "ITMES item 1 keeps its player-facing behavior") check("濃緑色" in items[32]["title"], "ITMES reconstructs furigana surface text inside a title") check(items[32]["furigana"][0]["reading"] == "のうりょくしょく", "ITMES preserves furigana readings") skill_messages, _ = extract_message_table.extract_messages( sys4load.load(scripts["SKMES.BIN"]) ) skills = {record["id"]: record for record in skill_messages} check(skills[1]["title"] == "【移動スキル:飛行】", "SKMES skill 1 keeps its display title") check(skills[1]["description"] == " 床のない地形を移動可能になる", "SKMES skill 1 keeps its player-facing behavior") vocabulary_messages, vocabulary_meta = extract_message_table.extract_messages( sys4load.load(scripts["VIMES.BIN"]) ) vocabulary = {record["id"]: record for record in vocabulary_messages} check(vocabulary[1]["title"] == "『【迷宮】占有率』" and "各勢力の占領度合" in vocabulary[1]["description"], "VIMES follows branch targets and reconstructs glossary help text") check(vocabulary_meta["message_layout"] == "title-description", "VIMES retains the title/description message layout") enemy_messages, enemy_meta = extract_message_table.extract_messages( sys4load.load(scripts["EIMES.BIN"]) ) enemies = {record["id"]: record for record in enemy_messages} check(enemies[101]["summary"] == "高い能力を秘めた隣国の姫騎士" and enemies[101]["strategy"] == "初遭遇時にはまず勝てない", "EIMES exposes its two lines as enemy summary and strategy") check(enemy_meta["message_layout"] == "enemy-commentary" and "title" not in enemies[101], "EIMES does not mislabel its first commentary line as a title") def test_message_join() -> None: scripts = paths.scripts() expected = { "IT": (287, " 銅の扉を開閉することが可能"), "SK": (131, " 床のない地形を移動可能になる"), } joined = {} for prefix, (count, _) in expected.items(): records, _ = extract_init.extract_name( sys4load.load(scripts[f"{prefix}INIT.BIN"]) ) meta = extract_init.join_messages( records, sys4load.load(scripts[f"{prefix}MES.BIN"]) ) check(meta["joined_count"] == count, f"{prefix}INIT joins all {count} {prefix}MES messages") check(not meta["init_ids_without_message"] and not meta["message_ids_without_init"], f"{prefix}INIT and {prefix}MES ids match exactly") joined[prefix] = {record["id"]: record for record in records} check(joined["IT"][1]["message"]["description"] == expected["IT"][1], "INIT/MES join uses the shared runtime id") vocabulary, _ = extract_init.extract_vocabulary( sys4load.load(scripts["VIINIT.BIN"]) ) vocabulary_meta = extract_init.join_messages( vocabulary, sys4load.load(scripts["VIMES.BIN"]) ) check( vocabulary_meta["joined_count"] == 65 and not vocabulary_meta["init_ids_without_message"] and not vocabulary_meta["message_ids_without_init"], "VIINIT and VIMES form a complete 65-topic runtime-id join", ) units, _ = extract_init.extract_name(sys4load.load(scripts["EBINIT.BIN"])) enemy_meta = extract_init.join_messages( units, sys4load.load(scripts["EIMES.BIN"]) ) unit_by_id = {record["id"]: record for record in units} check( enemy_meta["joined_count"] == 192 and len(enemy_meta["init_ids_without_message"]) == 85 and not enemy_meta["message_ids_without_init"], "EIMES joins 192 sparse enemy-commentary rows to EBINIT", ) check( unit_by_id[101]["message"]["strategy"] == "初遭遇時にはまず勝てない", "EBINIT records expose EIMES strategy text by unit id", ) def test_field_semantics() -> None: scripts = paths.scripts() items, _ = extract_init.extract_name(sys4load.load(scripts["ITINIT.BIN"])) semantics = extract_init.field_semantics(items) check(semantics["0x8c879"] == "item_sort_key", "parallel INIT fields expose canonical semantic names") check(semantics["0x9f541/14/8"] == "item_stat_modifiers.critical_chance", "row-table columns expose canonical semantic names") extract_init.attach_semantic_fields(items, semantics) check(items[0]["semantic_fields"]["item_sort_key"] == 10, "records expose a joined semantic field view") units, _ = extract_init.extract_name(sys4load.load(scripts["EBINIT.BIN"])) unit_semantics = extract_init.field_semantics(units) check( unit_semantics["0x73236/4/0"] == "unit_sally_action_unlock_requirements.contract", "SALLY unlock columns expose their action semantics", ) check( unit_semantics["0x741d6/8/6"] == "unit_sally_event_ids.sacrifice", "SALLY event columns expose their dispatched action semantics", ) check( unit_semantics["0x741d6/8/2"] == "unit_sally_event_ids.reserved_action", "the unreachable SALLY action remains explicit rather than speculative", ) check( unit_semantics["0x66716/30/19"] == "unit_voice_asset_ids.damage_reaction_1", "BTL target-owned damage voice columns expose their semantics", ) check( unit_semantics["0x66716/30/23"] == "unit_voice_asset_ids.finishing_blow", "BTL actor-owned finishing-blow voice exposes its semantics", ) check( unit_semantics["0x66716/30/17"] == "unit_voice_asset_ids.unused_slot_17", "populated but unreachable voice slots remain explicit", ) check( unit_semantics["0x7843e"] == "unit_power_tier", "EBINIT's authoring-only power tier joins by semantic name", ) check( unit_semantics["0x72296"] == "unit_boss_class", "EBINIT's signed boss class joins by semantic name", ) extract_init.attach_semantic_fields(units, unit_semantics) unit_by_id = {record["id"]: record for record in units} check( unit_by_id[5]["semantic_fields"]["unit_sally_event_ids.release"] == 1360, "EBINIT records join SALLY release events by semantic field name", ) check( unit_by_id[5]["semantic_fields"]["unit_voice_asset_ids.defeated"] == 11523, "EBINIT records join voice assets by semantic field name", ) check( [ unit_by_id[unit_id]["semantic_fields"]["unit_power_tier"] for unit_id in (2, 3, 4) ] == [2, 4, 6], "Lily's three forms preserve the correlated 2/4/6 power tiers", ) check( [ unit_by_id[unit_id]["semantic_fields"]["unit_boss_class"] for unit_id in (110, 115, 755, 756) ] == [1, -1, 4, -4], "paired story and final-boss records preserve victory-target sign", ) check( unit_by_id[456]["semantic_fields"]["unit_battle_sprite_asset_id"] == 12585 and unit_by_id[600]["semantic_fields"]["unit_starting_level"] == 80, "overlapping EBINIT writes retain their established parallel semantics", ) stages, meta = extract_init.extract_mixed( sys4load.load(extract_init.resolve("STINIT")) ) stage_semantics = extract_init.field_semantics( stages, meta["array_layouts"] ) check(stage_semantics["0xe7325/1"] == "stage_object_tile_x.index_1", "mixed buffer cells expose canonical semantic names") check( stage_semantics["0xe74b5/21"] == "stage_object_required_story_flags.row_3.required_flag_1", "mixed row buffers expose row and column semantics", ) check( stage_semantics["0xe7889/3"] == "stage_enemy_movement_routine_set_ids.row_1", "enemy footer copies expose whole-row semantics", ) extract_init.attach_semantic_fields(stages, stage_semantics) check( stages[0]["semantic_fields"][ "stage_enemy_movement_routine_set_ids.row_1" ] == [1, 1, 1], "semantic footer fields expose row values without provenance wrappers", ) if __name__ == "__main__": test_real_name_tables() test_static_negative_write() test_output_name_validation() test_real_mixed_table() test_real_class_change_rules() test_real_scene_dispatch() test_real_routine_banks() test_real_message_tables() test_message_join() test_field_semantics() if FAILS: raise SystemExit(f"{len(FAILS)} failed checks") print("all extract_init checks passed")