Close RTINIT movement parameter semantics

This commit is contained in:
gamer147
2026-07-23 15:58:38 -04:00
parent c55c5ada4c
commit 80575fdd11
12 changed files with 222 additions and 38 deletions

View File

@@ -91,6 +91,24 @@ ROUTINE_BANK_ROLES = (
)
MOVEMENT_PROVIDER_PARAMETER_SCHEMAS = {
1: {
"behavior": "advance_step_progress",
"parameter_fields": {},
"ignored_parameter_fields": {
"movement_parameter_1": (
"authored once (routine set 173, slot 2, value 10), but "
"RTN_M001 never reads movement parameter bank 2"
),
"movement_parameter_2": (
"authored once in the same step (value 711), but RTN_M001 "
"never reads movement parameter bank 3"
),
},
"completion": (
"unconditionally advance the current step's progress counter and "
"produce movement result state 1 without selecting a destination"
),
},
4: {
"behavior": "approach_stage_object_slot",
"parameter_fields": {
@@ -172,6 +190,26 @@ MOVEMENT_PROVIDER_PARAMETER_SCHEMAS = {
"valid movement destination toward the selected ally"
),
},
8: {
"behavior": "approach_nearest_foreign_magic_pillar",
"parameter_fields": {},
"ignored_parameter_fields": {
"movement_parameter_1": (
"authored once (routine set 112, slot 6, value 1), but "
"RTN_M008 never reads movement parameter bank 2"
),
},
"target_selection": (
"nearest reachable active stage object of OBINIT type 2, 3, or 4 "
"(small, medium, or large Magic Pillar) whose runtime state/faction "
"differs from the acting entity; unlike RTN_M015, no configured "
"route-radius gate is applied"
),
"completion": (
"produce a movement result when a reachable foreign-controlled "
"Magic Pillar exists"
),
},
10: {
"behavior": "approach_healing_feather",
"parameter_fields": {
@@ -241,6 +279,55 @@ MOVEMENT_PROVIDER_PARAMETER_SCHEMAS = {
"destination tile (or its linked type-6 stage-object exit tile)"
),
},
13: {
"behavior": "approach_faction_traversable_tile",
"parameter_fields": {
"movement_parameter_1": "target_faction_filter",
},
"parameter_defaults": {
"movement_parameter_1": 0,
},
"parameter_notes": {
"target_faction_filter": (
"zero means any faction other than the acting entity's faction; "
"a nonzero value selects exactly that faction id. Only one "
"shipped step explicitly writes value 1; three use default zero"
),
},
"target_selection": (
"when the current tile is not traversable by the selected faction "
"set, choose the nearest reachable tile whose "
"tile_faction_traversal_masks value includes that set, then "
"approach it"
),
"completion": (
"advance the current step's progress counter after producing a "
"valid movement destination into the selected faction's traversable "
"territory"
),
},
14: {
"behavior": "retreat_from_nearby_enemies",
"parameter_fields": {
"movement_parameter_1": "maximum_threat_route_steps",
},
"parameter_notes": {
"maximum_threat_route_steps": (
"inclusive route-step radius used to collect active foreign-"
"faction threats; shipped values are 3 or 6"
),
},
"target_selection": (
"sum route-proximity scores from every active foreign-faction "
"entity within the threat radius, exclude occupied tiles, and "
"choose a reachable tile with the lowest positive aggregate score "
"(farthest from the collected threats), randomizing ties"
),
"completion": (
"advance the current step's progress counter after producing a "
"valid retreat destination"
),
},
15: {
"behavior": "approach_foreign_magic_pillar",
"parameter_fields": {
@@ -1074,12 +1161,12 @@ def _movement_provider_names(names: dict[int, str]) -> dict[int, str]:
return providers
def _join_movement_provider_semantics(step: dict) -> tuple[int, int]:
def _join_movement_provider_semantics(step: dict) -> tuple[int, 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, 0
return 0, 0, 0
step["provider_behavior"] = schema["behavior"]
joined = 0
defaulted = 0
@@ -1091,7 +1178,14 @@ def _join_movement_provider_semantics(step: dict) -> tuple[int, int]:
elif raw_field in defaults:
step[semantic_field] = defaults[raw_field]
defaulted += 1
return joined, defaulted
ignored_fields = {
raw_field: step[raw_field]
for raw_field in schema.get("ignored_parameter_fields", {})
if raw_field in step
}
if ignored_fields:
step["ignored_movement_parameters"] = ignored_fields
return joined, defaulted, len(ignored_fields)
def extract_banked(scr):
@@ -1112,6 +1206,7 @@ def extract_banked(scr):
decoded_movement_step_count = 0
decoded_movement_parameter_count = 0
decoded_movement_defaulted_parameter_count = 0
ignored_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
@@ -1162,6 +1257,7 @@ def extract_banked(scr):
(
joined_parameter_count,
defaulted_parameter_count,
ignored_parameter_count,
) = _join_movement_provider_semantics(step)
if selector in MOVEMENT_PROVIDER_PARAMETER_SCHEMAS:
decoded_movement_step_count += 1
@@ -1169,6 +1265,7 @@ def extract_banked(scr):
decoded_movement_defaulted_parameter_count += (
defaulted_parameter_count
)
ignored_movement_parameter_count += ignored_parameter_count
movement_steps.append(step)
battle = {
@@ -1274,6 +1371,7 @@ def extract_banked(scr):
"decoded_movement_defaulted_parameter_count": (
decoded_movement_defaulted_parameter_count
),
"ignored_movement_parameter_count": ignored_movement_parameter_count,
"battle_provider_scripts": {
str(selector): name
for selector, name in sorted(battle_providers.items())

View File

@@ -248,6 +248,9 @@ def profile_banked(data: dict) -> dict:
"decoded_movement_defaulted_parameter_count": data.get(
"decoded_movement_defaulted_parameter_count", 0
),
"ignored_movement_parameter_count": data.get(
"ignored_movement_parameter_count", 0
),
"available_battle_provider_count": len(
data.get("battle_provider_scripts", {})
),
@@ -408,7 +411,9 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str:
f"{banked_profile['decoded_movement_parameter_count']} populated "
f"parameters + "
f"{banked_profile['decoded_movement_defaulted_parameter_count']} "
f"explicit defaults "
f"explicit defaults + "
f"{banked_profile['ignored_movement_parameter_count']} "
f"authored-but-unread parameters "
f"across {banked_profile['decoded_movement_provider_count']} providers",
f"- joined battle steps/used providers: "
f"{banked_profile['battle_step_count']}/"

View File

@@ -371,10 +371,11 @@ 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"] == 8
and meta["decoded_movement_step_count"] == 508
and meta["decoded_movement_parameter_count"] == 971
and meta["decoded_movement_defaulted_parameter_count"] == 10,
check(meta["decoded_movement_provider_count"] == 12
and meta["decoded_movement_step_count"] == 632
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"]
@@ -454,8 +455,41 @@ def test_real_routine_banks() -> None:
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")
check("provider_behavior" not in movement
and "destination_tile_x" not in movement,
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")
undecoded_movement = by_id[2]["movement_steps"][1]
check(undecoded_movement["movement_provider_selector"] == 2
and "provider_behavior" not in undecoded_movement
and "destination_tile_x" not in undecoded_movement,
"RTINIT does not leak provider-specific meanings onto undecoded selectors")
check(
meta["movement_provider_parameter_schemas"]["11"]["parameter_fields"]

View File

@@ -36,6 +36,8 @@ def test_load_and_lint():
check(entries[0xbf6fe]["name"] == "pathfinding_filtered_route_scores"
and entries[0xcc9f4]["name"] == "offensive_action_scope_masks",
"movement target filtering and offensive scope are curated")
check(entries[0x20543]["name"] == "tile_faction_traversal_masks",
"faction-specific tile traversal masks are curated")
check(entries[0xaba64]["name"] == "stage_object_runtime_flags",
"stage-object targeting flags are curated")
check(entries[0xb240e]["name"] == "pathfinding_movement_costs",

View File

@@ -133,6 +133,7 @@ def main() -> int:
"decoded_movement_step_count": 1,
"decoded_movement_parameter_count": 2,
"decoded_movement_defaulted_parameter_count": 1,
"ignored_movement_parameter_count": 1,
"movement_provider_scripts": {"1": "RTN_M001.BIN"},
"battle_provider_scripts": {"1": "RTN_B001.BIN"},
"used_movement_provider_selectors": [1],
@@ -165,6 +166,7 @@ def main() -> int:
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
assert banked_summary["ignored_movement_parameter_count"] == 1
messages = profile.profile_messages(fixture)
assert messages["population"] == 1