Decode RTINIT movement providers 5 and 11

This commit is contained in:
gamer147
2026-07-23 14:25:47 -04:00
parent eefca87ad8
commit 46d4d1e88f
12 changed files with 257 additions and 29 deletions

View File

@@ -90,6 +90,43 @@ ROUTINE_BANK_ROLES = (
"battle_forbidden_story_flag_id",
)
MOVEMENT_PROVIDER_PARAMETER_SCHEMAS = {
5: {
"behavior": "approach_destination_tile",
"parameter_fields": {
"movement_parameter_1": "destination_tile_x",
"movement_parameter_2": "destination_tile_y",
},
"completion": (
"advance the current step's progress counter after reaching the "
"destination tile (or its linked type-6 stage-object exit tile)"
),
},
11: {
"behavior": "cycle_destination_waypoints",
"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",
},
"parameter_notes": {
"waypoint_ordinal": (
"one-based; only the ordinal matching the entity's current "
"zero-based waypoint index executes"
),
"path_cost_limit_override": (
"optional; zero/absent falls back to the entity's current FS"
),
},
"completion": (
"advance the entity's waypoint index modulo the largest authored "
"waypoint ordinal after reaching the destination tile (or its "
"linked type-6 stage-object exit tile)"
),
},
}
UNIT_STAT_COLUMNS = (
"accuracy", "evasion", "physical_attack", "physical_defense",
"magic_attack", "magic_defense", "speed", "luck", "critical_chance",
@@ -900,6 +937,21 @@ def _movement_provider_names(names: dict[int, str]) -> dict[int, str]:
return providers
def _join_movement_provider_semantics(step: dict) -> int:
"""Add selector-specific RTN_M semantics while retaining every raw bank."""
selector = step.get("movement_provider_selector")
schema = MOVEMENT_PROVIDER_PARAMETER_SCHEMAS.get(selector)
if schema is None:
return 0
step["provider_behavior"] = schema["behavior"]
joined = 0
for raw_field, semantic_field in schema["parameter_fields"].items():
if raw_field in step:
step[semantic_field] = step[raw_field]
joined += 1
return joined
def extract_banked(scr):
"""Extract RTINIT's sparse routine sets across twenty parallel step banks."""
writes = _routine_bank_writes(scr)
@@ -915,6 +967,8 @@ def extract_banked(scr):
records_by_id: dict[int, dict] = {}
cell_assignments: dict[tuple[int, int, int], list[int]] = collections.defaultdict(list)
bank_cells: dict[int, set[tuple[int, int]]] = collections.defaultdict(set)
decoded_movement_step_count = 0
decoded_movement_parameter_count = 0
for offset, destination, value, bank_index, record_id, slot in writes:
bank_base = ROUTINE_BANK_ROOT + bank_index * ROUTINE_BANK_SPAN
@@ -954,14 +1008,19 @@ def extract_banked(scr):
}
if movement:
selector = movement.get("movement_provider_selector")
movement_steps.append({
step = {
"slot": slot,
**movement,
**(
{"provider_script": movement_providers.get(selector, "")}
if selector is not None else {}
),
})
}
joined_parameter_count = _join_movement_provider_semantics(step)
if selector in MOVEMENT_PROVIDER_PARAMETER_SCHEMAS:
decoded_movement_step_count += 1
decoded_movement_parameter_count += joined_parameter_count
movement_steps.append(step)
battle = {
ROUTINE_BANK_ROLES[bank]: final_by_bank_slot[(bank, slot)]
@@ -1049,6 +1108,20 @@ def extract_banked(scr):
str(selector): name
for selector, name in sorted(movement_providers.items())
},
"movement_provider_parameter_schemas": {
str(selector): {
"provider_script": movement_providers.get(selector, ""),
**schema,
}
for selector, schema in sorted(
MOVEMENT_PROVIDER_PARAMETER_SCHEMAS.items()
)
},
"decoded_movement_provider_count": len(
MOVEMENT_PROVIDER_PARAMETER_SCHEMAS
),
"decoded_movement_step_count": decoded_movement_step_count,
"decoded_movement_parameter_count": decoded_movement_parameter_count,
"battle_provider_scripts": {
str(selector): name
for selector, name in sorted(battle_providers.items())

View File

@@ -236,6 +236,15 @@ def profile_banked(data: dict) -> dict:
"available_movement_provider_count": len(
data.get("movement_provider_scripts", {})
),
"decoded_movement_provider_count": data.get(
"decoded_movement_provider_count", 0
),
"decoded_movement_step_count": data.get(
"decoded_movement_step_count", 0
),
"decoded_movement_parameter_count": data.get(
"decoded_movement_parameter_count", 0
),
"available_battle_provider_count": len(
data.get("battle_provider_scripts", {})
),
@@ -391,6 +400,10 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str:
f"{banked_profile['movement_step_count']}/"
f"{banked_profile['movement_provider_count']} "
f"({banked_profile['available_movement_provider_count']} dispatchable)",
f"- selector-specific movement semantics: "
f"{banked_profile['decoded_movement_step_count']} steps, "
f"{banked_profile['decoded_movement_parameter_count']} parameters "
f"across {banked_profile['decoded_movement_provider_count']} providers",
f"- joined battle steps/used providers: "
f"{banked_profile['battle_step_count']}/"
f"{banked_profile['battle_provider_count']} "

View File

@@ -371,6 +371,10 @@ def test_real_routine_banks() -> None:
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"] == 2
and meta["decoded_movement_step_count"] == 252
and meta["decoded_movement_parameter_count"] == 685,
"RTINIT reports selector-specific semantic coverage")
check([
layout["bank_index"]
for layout in meta["bank_layouts"].values()
@@ -393,6 +397,35 @@ def test_real_routine_banks() -> None:
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")
check("provider_behavior" not in movement
and "destination_tile_x" not in movement,
"RTINIT does not leak provider-specific meanings onto undecoded selectors")
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",
)
semantics = extract_init.field_semantics(records)
check(
semantics["0xeff78/20/0"]

View File

@@ -16,6 +16,19 @@ def test_load_and_lint():
check(entries[0xa57]["category"] == "story-flag", "0xa57 is category story-flag")
check(entries[0x3234]["category"] == "story-flag", "0x3234 is category story-flag")
check({0xa57, 0xa58, 0xa59} <= set(entries), "Lily form flags A/B/C all present")
check(entries[0x4e085]["columns"] == {
"0": "current_hp", "1": "current_sp", "2": "current_fs"
}, "entity current-resource columns are curated")
check(entries[0x4e11b]["columns"]["10"] == "movement"
and entries[0x4e11b]["columns"]["13"] == "max_fs",
"entity effective-stat columns are curated")
check(entries[0x5231f]["name"] == "entity_tile_x"
and entries[0x52351]["name"] == "entity_tile_y",
"entity map-coordinate arrays are curated")
check(entries[0x56b20]["name"] == "entity_patrol_waypoint_indices",
"RTN_M011 waypoint state is curated")
check(entries[0xb240e]["name"] == "pathfinding_movement_costs",
"movement-cost work grid is curated")
# lint clean against a permissive address universe (curated addrs are self-consistent)
errors, warnings = G.lint(entries, set(entries))
check(errors == [], f"globals.toml lints clean (errors={errors})")

View File

@@ -129,6 +129,9 @@ def main() -> int:
"conflicting_overwrite_count": 1,
"movement_step_count": 2,
"battle_step_count": 1,
"decoded_movement_provider_count": 1,
"decoded_movement_step_count": 1,
"decoded_movement_parameter_count": 2,
"movement_provider_scripts": {"1": "RTN_M001.BIN"},
"battle_provider_scripts": {"1": "RTN_B001.BIN"},
"used_movement_provider_selectors": [1],
@@ -157,6 +160,9 @@ def main() -> int:
assert banked_summary["reserved_bank_count"] == 1
assert banked_summary["movement_step_count"] == 2
assert banked_summary["battle_provider_count"] == 1
assert banked_summary["decoded_movement_provider_count"] == 1
assert banked_summary["decoded_movement_step_count"] == 1
assert banked_summary["decoded_movement_parameter_count"] == 2
messages = profile.profile_messages(fixture)
assert messages["population"] == 1