#!/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("")` 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())