- Implemented `formats.py` to define data structures and functions for handling map data, including reading and decoding shape and map items. - Created `png.py` for generating PNG images from shape frames and pixel data. - Developed `sorting.py` to manage the sorting and rendering order of map items based on their properties and spatial relationships. - Introduced `render_all_maps.py` to facilitate the rendering of all maps for specified games, including command-line argument parsing and subprocess management for rendering tasks.
121 lines
4.3 KiB
Python
121 lines
4.3 KiB
Python
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("<H", data, 0x54)[0]
|
|
|
|
|
|
def has_world_rect(extra_args: list[str]) -> 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())
|