#!/usr/bin/env python3 """Validates transition nodes in KQ4 room scenes.""" import re import sys from pathlib import Path from dataclasses import dataclass from typing import Optional @dataclass class Transition: exit_node_name: str target_uid: str appear_at_node: str label: str source_room: str @dataclass class SceneInfo: uid: str path: Path node_names: set[str] def find_uid_files(root: Path) -> dict[str, Path]: """Build a mapping of UID -> file path from all .uid files.""" uid_map = {} for uid_file in root.rglob("*.uid"): uid_content = uid_file.read_text().strip() if uid_content.startswith("uid://"): resource_path = uid_file.with_suffix("") if resource_path.exists(): uid_map[uid_content] = resource_path else: uid_map[uid_content] = uid_file.with_suffix("") return uid_map def parse_scene_file(tscn_path: Path) -> tuple[Optional[str], set[str], list[Transition]]: """Parse a .tscn file to extract UID, node names, and transitions.""" content = tscn_path.read_text() scene_uid = None uid_match = re.search(r'\[gd_scene[^\]]*uid="([^"]+)"', content) if uid_match: scene_uid = uid_match.group(1) node_names = set(re.findall(r'^\[node name="([^"]+)"', content, re.MULTILINE)) transitions = [] room_name = tscn_path.stem transition_pattern = re.compile( r'\[node name="([^"]+)"[^\]]*instance=ExtResource\([^\)]+\)\]\s*\n' r'((?:[^\[]+\n)*)', re.MULTILINE ) for match in transition_pattern.finditer(content): node_name = match.group(1) body = match.group(2) target_match = re.search(r'^target = "([^"]+)"', body, re.MULTILINE) appear_match = re.search(r'^appear_at_node = "([^"]+)"', body, re.MULTILINE) label_match = re.search(r'^label = "([^"]+)"', body, re.MULTILINE) if target_match and appear_match: transitions.append(Transition( exit_node_name=node_name, target_uid=target_match.group(1), appear_at_node=appear_match.group(1), label=label_match.group(1) if label_match else node_name, source_room=room_name )) return scene_uid, node_names, transitions def main(): root = Path(__file__).parent.parent scenes_dir = root / "scenes" uid_map = find_uid_files(root) scene_files = list(scenes_dir.glob("kq4_*/kq4_*.tscn")) scene_files = [f for f in scene_files if "placeholder_template" not in str(f)] scenes_by_uid: dict[str, SceneInfo] = {} all_transitions: list[Transition] = [] for scene_file in scene_files: scene_uid, node_names, transitions = parse_scene_file(scene_file) if scene_uid: scenes_by_uid[scene_uid] = SceneInfo( uid=scene_uid, path=scene_file, node_names=node_names ) all_transitions.extend(transitions) errors = 0 for t in all_transitions: source_room = t.source_room target_scene = scenes_by_uid.get(t.target_uid) if target_scene is None: print(f"ERROR: {source_room} exit '{t.exit_node_name}' -> target UID '{t.target_uid}' NOT FOUND") errors += 1 continue target_room_name = target_scene.path.stem if t.appear_at_node not in target_scene.node_names: print(f"ERROR: {source_room} exit '{t.exit_node_name}' -> {target_room_name} node '{t.appear_at_node}' NOT FOUND") errors += 1 continue print(f"OK: {source_room} exit '{t.exit_node_name}' -> {target_room_name} (node: {t.appear_at_node})") print(f"\n{'='*60}") print(f"Checked {len(all_transitions)} transitions across {len(scene_files)} rooms") if errors: print(f"FOUND {errors} ERROR(S)") sys.exit(1) else: print("All transitions valid!") sys.exit(0) if __name__ == "__main__": main()