from __future__ import annotations import argparse import os import struct import subprocess import sys from pathlib import Path if __package__ in (None, ""): sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from tools.crusader_map.formats import collect_render_items, load_globs, load_map_items, load_typeflags def get_map_count(fixed_dat: Path) -> int: data = fixed_dat.read_bytes() return struct.unpack_from(" bool: return "--world-rect" in extra_args def render_game(repo_root: Path, python_exe: str, game: str, start: int | None, end: int | None, extra_args: list[str]) -> int: out_dir = repo_root / "out" / game out_dir.mkdir(parents=True, exist_ok=True) if game == "regret": static_dir = repo_root / "STATIC_REGRET" else: static_dir = repo_root / "STATIC" fixed_dat = static_dir / "FIXED.DAT" if not fixed_dat.exists(): print(f"Missing {fixed_dat}", file=sys.stderr) return 1 map_count = get_map_count(fixed_dat) shape_infos = load_typeflags(static_dir / "TYPEFLAG.DAT") globs = load_globs(static_dir / "GLOB.FLX") batch_max_items = int(os.environ.get("BATCH_MAX_ITEMS", "0")) world_rect_requested = has_world_rect(extra_args) start_index = 0 if start is None else max(0, start) end_index = map_count - 1 if end is None else min(end, map_count - 1) if start_index > end_index: print(f"Invalid map range {start_index}..{end_index} for {game} ({map_count} maps)", file=sys.stderr) return 1 print(f"Rendering {game} maps {start_index}..{end_index} into {out_dir}") failed = False script_path = repo_root / "tools" / "render_crusader_map.py" for map_index in range(start_index, end_index + 1): print(f"[{game}] Rendering map {map_index}...") output_png = out_dir / f"map-{map_index}.png" output_json = out_dir / f"map-{map_index}.json" base_items = load_map_items(fixed_dat, map_index) render_items = collect_render_items( base_items, shape_infos, globs, include_editor=True, expand_globs=True, world_rect=None, include_roofs=False, include_hidden_markers=True, ) if not render_items: print(f"[{game}] Skipping empty map {map_index}.") output_png.unlink(missing_ok=True) output_json.unlink(missing_ok=True) continue if batch_max_items > 0 and not world_rect_requested and len(render_items) > batch_max_items: print( f"[{game}] Skipping map {map_index}: {len(render_items)} render items exceed batch threshold {batch_max_items}. " "Set BATCH_MAX_ITEMS=0 to disable or use RENDER_ARGS=--world-rect ... for bounded runs.", file=sys.stderr, ) output_png.unlink(missing_ok=True) output_json.unlink(missing_ok=True) continue command = [ python_exe, str(script_path), "--game", game, "--map", str(map_index), "--output", str(output_png), "--metadata", str(output_json), *extra_args, ] result = subprocess.run(command, cwd=repo_root) if result.returncode != 0: print(f"[{game}] Map {map_index} failed.", file=sys.stderr) failed = True return 1 if failed else 0 def main() -> int: parser = argparse.ArgumentParser(description="Render every Crusader fixed map for one or both games.") parser.add_argument("--game", choices=("remorse", "regret", "all"), required=True) parser.add_argument("--start", type=int, help="Optional starting map index.") parser.add_argument("--end", type=int, help="Optional ending map index.") args, extra_args = parser.parse_known_args() repo_root = Path(__file__).resolve().parents[1] python_exe = os.environ.get("PYTHON_EXE") or sys.executable games = [args.game] if args.game != "all" else ["remorse", "regret"] exit_code = 0 for game in games: exit_code |= render_game(repo_root, python_exe, game, args.start, args.end, extra_args) return exit_code if __name__ == "__main__": raise SystemExit(main())