Files
OpenMaidEngine/tools/test_extract_init.py
2026-07-23 17:39:08 -04:00

813 lines
38 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"] == "-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")