most areas connected
This commit is contained in:
135
scripts/check_transitions.py
Executable file
135
scripts/check_transitions.py
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user