Files
aetherbound-guild/tools/audio/wire_entry_shell.py
T

156 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""M76-C wiring pass: connect the runtime audio service to the entry shell.
Adds (once, idempotently):
* service creation + helpers in `_ready`/`_apply_runtime_settings`;
* a `_audio_enter(page, family)` call as the first statement of each page
`show_*` function (page_open cue + music family switch);
* a `_audio_sfx("<event>")` call at the first statement of each semantic
player-commit action function.
"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SHELL = ROOT / "runtime/entry/entry_shell.gd"
PAGES: dict[str, tuple[str, str]] = {
"show_boot_diagnostics": ("boot", "title"),
"show_first_launch": ("setup", "title"),
"show_title": ("title", "title"),
"show_initial_company": ("initial_company", "title"),
"show_guild_handoff": ("guild_handoff", "guild"),
"show_guild_anchor": ("guild", "guild"),
"show_roster": ("roster", "guild"),
"show_recruit_detail": ("recruit_detail", "guild"),
"show_contract_choice": ("contract_choice", "prep"),
"show_market_handoff": ("market_handoff", "market"),
"show_market": ("market", "market"),
"show_recruit_review": ("recruit_review", "market"),
"show_equipment_review": ("equipment_review", "market"),
"show_party_line": ("party_line", "prep"),
"show_readiness_review": ("readiness_review", "prep"),
"show_risk_board": ("risk_board", "run"),
"show_risk_commitment": ("risk_commitment", "run"),
"show_battle_opening": ("battle_opening", "run"),
"show_battle": ("battle", "run"),
"show_battle_result": ("battle_result", ""),
"show_battle_knockout_review": ("battle_knockout_review", "result_loss"),
"show_battle_reward_choice": ("battle_reward_choice", "result_win"),
"show_battle_recovery": ("battle_recovery", "result_loss"),
"show_settings_from_guild": ("settings", ""),
"show_settings_placeholder": ("settings", ""),
"show_settings": ("settings", ""),
"show_credits": ("credits", ""),
"show_unavailable_reason": ("unavailable", ""),
}
ACTIONS: dict[str, str] = {
"commit_dismissal": "dismiss_commit",
"commit_contract_return": "run_end_voluntary",
"confirm_contract_choice": "confirm_commit",
"save_party_order": "reorder_commit",
"reset_party_order": "reorder_cancel",
"select_party_member": "unit_select",
"select_contract_difficulty": "difficulty_change_review",
"accept_readiness": "ready_check_pass",
"select_risk_option": "risk_option_focus",
"toggle_risk_commitment_confirmation": "confirm_arm",
"begin_battle": "battle_start_commit",
"commit_battle_reward": "reward_accept",
"commit_battle_recovery": "recovery_option_open",
"purchase_selected_equipment": "equip_commit",
"hire_selected_recruit": "recruit_hire_commit",
"select_battle_reward": "reward_option_focus",
"open_recruit_review": "recruit_hire_review",
"open_equipment_review": "equip_review",
"open_dismissal_review": "dismiss_arm",
"open_contract_return_review": "run_end_voluntary",
"continue_from_battle_result": "run_continue",
"select_market_offer": "offer_reveal",
}
HELPERS = '''
func _ensure_audio_service() -> void:
if audio_service != null and is_instance_valid(audio_service):
return
audio_service = AudioServiceScript.new()
audio_service.name = "AudioService"
add_child(audio_service)
func _audio_enter(page_id: String, family := "") -> void:
_ensure_audio_service()
if audio_service == null:
return
audio_page_id = page_id
if not family.is_empty():
audio_service.set_music(family)
audio_service.play_sfx("page_open")
func _audio_sfx(event_name: String) -> void:
_ensure_audio_service()
if audio_service != null:
audio_service.play_sfx(event_name)
'''
def insert_first_statement(text: str, function: str, statement: str, indent: str = "\t") -> tuple[str, bool]:
pattern = re.compile(rf"^func {re.escape(function)}\(([^\n]*)\)[^\n]*:\n", re.M)
match = pattern.search(text)
if match is None:
return text, False
insert_at = match.end()
if text[insert_at:].startswith(f"{indent}{statement}"):
return text, False
return text[:insert_at] + f"{indent}{statement}\n" + text[insert_at:], True
def main() -> int:
text = SHELL.read_text()
changed: list[str] = []
if "func _audio_enter(" not in text:
anchor = "func start_boot() -> void:"
if anchor not in text:
raise SystemExit("anchor for helpers not found")
text = text.replace(anchor, HELPERS.lstrip("\n") + anchor, 1)
changed.append("helpers")
if "var audio_service" not in text:
marker = "func _ready() -> void:\n"
text = text.replace(marker, "var audio_service: ABGAudioService\nvar audio_page_id := \"\"\n\n\n" + marker, 1)
changed.append("service vars")
if "AudioServiceScript" not in text:
preload_line = 'const AudioServiceScript = preload("res://presentation/audio_service.gd")\n'
# place after the first const block line
first_const = re.search(r"^const [A-Za-z_]+ = .*\n", text, re.M)
if first_const is None:
raise SystemExit("no const anchor")
text = text[:first_const.end()] + preload_line + text[first_const.end():]
changed.append("preload")
if "audio_service.apply_settings(settings)" not in text:
old = "\tvar master := AudioServer.get_bus_index(&\"Master\")"
if old in text:
text = text.replace(old, "\t_ensure_audio_service()\n\tif audio_service != null:\n\t\taudio_service.apply_settings(settings)\n" + old, 1)
changed.append("apply settings")
for function, (page_id, family) in PAGES.items():
statement = f'_audio_enter("{page_id}", "{family}")'
text, did = insert_first_statement(text, function, statement)
if did:
changed.append(f"page:{page_id}")
for function, event in ACTIONS.items():
statement = f'_audio_sfx("{event}")'
text, did = insert_first_statement(text, function, statement)
if did:
changed.append(f"action:{function}")
SHELL.write_text(text)
print(f"wired {len(changed)} sites: {', '.join(changed[:6])}{' ...' if len(changed) > 6 else ''}")
return 0
if __name__ == "__main__":
raise SystemExit(main())