Decode RTINIT movement providers 7 10 and 12

This commit is contained in:
gamer147
2026-07-23 15:32:20 -04:00
parent 7b3421a961
commit 52780d92a0
12 changed files with 228 additions and 36 deletions

View File

@@ -102,6 +102,62 @@ MOVEMENT_PROVIDER_PARAMETER_SCHEMAS = {
"destination tile (or its linked type-6 stage-object exit tile)"
),
},
7: {
"behavior": "approach_injured_ally",
"parameter_fields": {
"movement_parameter_1": "maximum_target_route_steps",
"movement_parameter_2": "maximum_target_hp_percent",
},
"parameter_notes": {
"maximum_target_route_steps": (
"maximum flood-fill step distance from the acting entity; "
"shipped values are 5 or 10"
),
"maximum_target_hp_percent": (
"inclusive current-HP percentage cutoff; shipped values are "
"50, 70, or 80"
),
},
"target_selection": (
"nearest active non-self entity of the same faction whose current "
"HP percentage is at or below the cutoff; choose randomly among "
"ties, then approach a reachable tile nearest that ally"
),
"completion": (
"advance the current step's progress counter after producing a "
"valid movement destination toward the selected ally"
),
},
10: {
"behavior": "approach_healing_feather",
"parameter_fields": {
"movement_parameter_1": "resource_index",
"movement_parameter_2": "maximum_resource_percent",
},
"parameter_defaults": {
"movement_parameter_1": 0,
},
"parameter_notes": {
"resource_index": (
"0=HP, 1=SP, 2=FS; all shipped RTINIT cells are unwritten and "
"therefore use the zero/HP default"
),
"maximum_resource_percent": (
"inclusive current/max percentage cutoff; shipped values are "
"30 or 50"
),
},
"target_selection": (
"nearest active stage object of OBINIT type 15 (Healing Feather) "
"or 16 (single-use red Healing Feather), then approach a reachable "
"tile nearest that object"
),
"completion": (
"produce a movement result only when the selected resource's "
"maximum is nonzero, its current percentage is at or below the "
"cutoff, and a reachable Healing Feather exists"
),
},
11: {
"behavior": "cycle_destination_waypoints",
"parameter_fields": {
@@ -125,6 +181,22 @@ MOVEMENT_PROVIDER_PARAMETER_SCHEMAS = {
"linked type-6 stage-object exit tile)"
),
},
12: {
"behavior": "approach_destination_tile_avoiding_foreign_entities",
"parameter_fields": {
"movement_parameter_1": "destination_tile_x",
"movement_parameter_2": "destination_tile_y",
},
"routing": (
"same destination and completion logic as RTN_M005, but MVSEEK "
"mode 2 masks the doubled-coordinate terrain cells occupied by "
"active entities of another faction before its flood fill"
),
"completion": (
"advance the current step's progress counter after reaching the "
"destination tile (or its linked type-6 stage-object exit tile)"
),
},
}
UNIT_STAT_COLUMNS = (
@@ -937,19 +1009,24 @@ def _movement_provider_names(names: dict[int, str]) -> dict[int, str]:
return providers
def _join_movement_provider_semantics(step: dict) -> int:
def _join_movement_provider_semantics(step: dict) -> tuple[int, 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
return 0, 0
step["provider_behavior"] = schema["behavior"]
joined = 0
defaulted = 0
defaults = schema.get("parameter_defaults", {})
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
elif raw_field in defaults:
step[semantic_field] = defaults[raw_field]
defaulted += 1
return joined, defaulted
def extract_banked(scr):
@@ -969,6 +1046,7 @@ def extract_banked(scr):
bank_cells: dict[int, set[tuple[int, int]]] = collections.defaultdict(set)
decoded_movement_step_count = 0
decoded_movement_parameter_count = 0
decoded_movement_defaulted_parameter_count = 0
for offset, destination, value, bank_index, record_id, slot in writes:
bank_base = ROUTINE_BANK_ROOT + bank_index * ROUTINE_BANK_SPAN
@@ -1016,10 +1094,16 @@ def extract_banked(scr):
if selector is not None else {}
),
}
joined_parameter_count = _join_movement_provider_semantics(step)
(
joined_parameter_count,
defaulted_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
decoded_movement_defaulted_parameter_count += (
defaulted_parameter_count
)
movement_steps.append(step)
battle = {
@@ -1122,6 +1206,9 @@ def extract_banked(scr):
),
"decoded_movement_step_count": decoded_movement_step_count,
"decoded_movement_parameter_count": decoded_movement_parameter_count,
"decoded_movement_defaulted_parameter_count": (
decoded_movement_defaulted_parameter_count
),
"battle_provider_scripts": {
str(selector): name
for selector, name in sorted(battle_providers.items())

View File

@@ -245,6 +245,9 @@ def profile_banked(data: dict) -> dict:
"decoded_movement_parameter_count": data.get(
"decoded_movement_parameter_count", 0
),
"decoded_movement_defaulted_parameter_count": data.get(
"decoded_movement_defaulted_parameter_count", 0
),
"available_battle_provider_count": len(
data.get("battle_provider_scripts", {})
),
@@ -402,7 +405,10 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str:
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"{banked_profile['decoded_movement_parameter_count']} populated "
f"parameters + "
f"{banked_profile['decoded_movement_defaulted_parameter_count']} "
f"explicit defaults "
f"across {banked_profile['decoded_movement_provider_count']} providers",
f"- joined battle steps/used providers: "
f"{banked_profile['battle_step_count']}/"

View File

@@ -371,9 +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,
check(meta["decoded_movement_provider_count"] == 5
and meta["decoded_movement_step_count"] == 292
and meta["decoded_movement_parameter_count"] == 758
and meta["decoded_movement_defaulted_parameter_count"] == 7,
"RTINIT reports selector-specific semantic coverage")
check([
layout["bank_index"]
@@ -413,6 +414,26 @@ def test_real_routine_banks() -> None:
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")
check("provider_behavior" not in movement
and "destination_tile_x" not in movement,
"RTINIT does not leak provider-specific meanings onto undecoded selectors")
@@ -426,6 +447,11 @@ def test_real_routine_banks() -> None:
},
"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"]

View File

@@ -25,8 +25,14 @@ def test_load_and_lint():
check(entries[0x5231f]["name"] == "entity_tile_x"
and entries[0x52351]["name"] == "entity_tile_y",
"entity map-coordinate arrays are curated")
check(entries[0x4e021]["name"] == "entity_runtime_flags"
and entries[0x522ed]["name"] == "entity_faction_ids",
"entity activity and faction arrays are curated")
check(entries[0x56b20]["name"] == "entity_patrol_waypoint_indices",
"RTN_M011 waypoint state is curated")
check(entries[0xaba96]["name"] == "pathfinding_remaining_route_steps"
and entries[0xcc9f1]["name"] == "movement_search_mode",
"movement-search reachability 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)

View File

@@ -132,6 +132,7 @@ def main() -> int:
"decoded_movement_provider_count": 1,
"decoded_movement_step_count": 1,
"decoded_movement_parameter_count": 2,
"decoded_movement_defaulted_parameter_count": 1,
"movement_provider_scripts": {"1": "RTN_M001.BIN"},
"battle_provider_scripts": {"1": "RTN_B001.BIN"},
"used_movement_provider_selectors": [1],
@@ -163,6 +164,7 @@ def main() -> int:
assert banked_summary["decoded_movement_provider_count"] == 1
assert banked_summary["decoded_movement_step_count"] == 1
assert banked_summary["decoded_movement_parameter_count"] == 2
assert banked_summary["decoded_movement_defaulted_parameter_count"] == 1
messages = profile.profile_messages(fixture)
assert messages["population"] == 1