#!/usr/bin/env python3 """ 从 Unity Levels*.cs 导出 Cocos LevelRegistry 数据。 用法: python3 tools/export_unity_levels.py \ --input "/path/to/Levels600.cs" \ --output assets/scripts/level/levels-600.generated.ts \ --border-cache assets/scripts/level/border-cache.json """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path DIR = { "Direction.North": "Direction.North", "Direction.East": "Direction.East", "Direction.South": "Direction.South", "Direction.West": "Direction.West", } LEVEL_RE = re.compile( r"\{(\d+),new Level\(\)\{LevelID\s*=\s*\d+,spawns\s*=\s*new List\(\)\{", re.MULTILINE, ) SPAWN_RE = re.compile( r"new Spawn\(\)\{([^}]+(?:\{[^}]*\})?)\}", re.MULTILINE, ) POS_RE = re.compile(r"position\s*=\s*new Vector3Int\((-?\d+),(-?\d+),0\)") PATH_RE = re.compile(r'path\s*=\s*"?([^",\s]+)"?') PDIR_RE = re.compile(r"playerDirection\s*=\s*(Direction\.\w+)") VDIR_RE = re.compile(r"vehicleDirection\s*=\s*(Direction\.\w+)") BOUND_RE = re.compile(r"boundary\s*=\s*new Vector3Int\((\d+),(\d+),0\)") def kind_from_path(path: str) -> str: p = path.lower() if "player" in p and "multplay" not in p: return "player" if "vehicle" in p: return "vehicle" if "enemy" in p: return "enemy" if "nprop" in p: return "prop_decor" if "prop" in p: return "prop" return "prop_decor" def parse_spawns(block: str) -> list[dict]: spawns = [] for m in SPAWN_RE.finditer(block): body = m.group(1) pm = POS_RE.search(body) if not pm: continue path_m = PATH_RE.search(body) path = path_m.group(1) if path_m else "" item: dict = { "x": int(pm.group(1)), "y": int(pm.group(2)), "kind": kind_from_path(path), } pdir = PDIR_RE.search(body) vdir = VDIR_RE.search(body) if pdir: item["playerDirection"] = pdir.group(1) if vdir: item["vehicleDirection"] = vdir.group(1) spawns.append(item) return spawns def ring_border_key(bx: int, by: int) -> str: return f"{bx},{by}" def parse_file(text: str) -> dict[int, dict]: levels: dict[int, dict] = {} matches = list(LEVEL_RE.finditer(text)) for i, m in enumerate(matches): lid = int(m.group(1)) start = m.end() end = matches[i + 1].start() if i + 1 < len(matches) else len(text) chunk = text[start:end] bound = BOUND_RE.search(chunk) bx, by = (10, 10) if bound: bx, by = int(bound.group(1)), int(bound.group(2)) spawns = parse_spawns(chunk) levels[lid] = { "levelID": lid, "boundary": {"x": bx, "y": by}, "borderKey": ring_border_key(bx, by), "spawns": spawns, } return levels def spawn_to_ts(s: dict, indent: str) -> str: parts = [ f"x: {s['x']}", f"y: {s['y']}", f"kind: '{s['kind']}'", ] if "playerDirection" in s: parts.append(f"playerDirection: {s['playerDirection']}") if "vehicleDirection" in s: parts.append(f"vehicleDirection: {s['vehicleDirection']}") return indent + "{ " + ", ".join(parts) + " }" def emit_ts(levels: dict[int, dict], border_cache: dict[str, dict]) -> str: lines = [ "/* AUTO-GENERATED by tools/export_unity_levels.py — DO NOT EDIT */", "import { Direction } from '../core/Define';", "import { LevelConfig, SpawnConfig } from './LevelTypes';", "", "const BORDER_CACHE: Record> = " + json.dumps(border_cache, separators=(',', ':')) + ";", "", "function withBorder(key: string, cfg: Omit): LevelConfig {", " return { ...cfg, border: BORDER_CACHE[key] };", "}", "", "export const LEVELS_600: Record = {", ] for lid in sorted(levels.keys()): L = levels[lid] lines.append(f" {lid}: withBorder('{L['borderKey']}', {{") lines.append(f" levelID: {lid},") lines.append(f" boundary: {{ x: {L['boundary']['x']}, y: {L['boundary']['y']} }},") lines.append(" spawns: [") for s in L["spawns"]: lines.append(spawn_to_ts(s, " ") + ",") lines.append(" ],") lines.append(" }),") lines.append("};") lines.append("") return "\n".join(lines) def build_border_cache(levels: dict[int, dict]) -> dict[str, dict]: cache: dict[str, dict] = {} for L in levels.values(): key = L["borderKey"] if key in cache: continue bx, by = L["boundary"]["x"], L["boundary"]["y"] border: dict[str, bool] = {} for x in range(-bx, bx + 1): for y in range(-by, by + 1): if abs(x) == bx or abs(y) == by: border[f"{x},{y}"] = True cache[key] = border return cache def main(): ap = argparse.ArgumentParser() ap.add_argument("--input", required=True, help="Unity Levels*.cs path") ap.add_argument("--output", required=True, help="Output .ts path") ap.add_argument("--border-cache", default="", help="Optional border cache json") args = ap.parse_args() text = Path(args.input).read_text(encoding="utf-8") levels = parse_file(text) if not levels: print("No levels parsed", file=sys.stderr) sys.exit(1) border_cache = build_border_cache(levels) out = Path(args.output) out.parent.mkdir(parents=True, exist_ok=True) out.write_text(emit_ts(levels, border_cache), encoding="utf-8") if args.border_cache: Path(args.border_cache).write_text(json.dumps(border_cache, indent=2), encoding="utf-8") print(f"Exported {len(levels)} levels -> {out}") if __name__ == "__main__": main()