#!/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 from export_unity_prefab_maps import parse_level_prefab 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 "prop" in p: return "prop" return "prop_decor" def prop_placement_from_path(path: str) -> str | None: if "nprop" in path.lower(): return "ground" if "prop" in path.lower(): return "block" return None 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), } placement = prop_placement_from_path(path) if placement and item["kind"] == "prop": item["propPlacement"] = placement 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']}") if s.get("propPlacement"): parts.append(f"propPlacement: '{s['propPlacement']}'") return indent + "{ " + ", ".join(parts) + " }" def emit_ts(levels: dict[int, dict], export_const: str = "LEVELS_600") -> str: lines = [ "/* AUTO-GENERATED by tools/export_unity_levels.py — DO NOT EDIT */", "/* spawns 来自 Levels*.cs;ground/border 来自 Assets/Prefabs/Level/LevelN.prefab Tilemap */", "import { Direction } from '../core/Define';", "import { LevelConfig } from './LevelTypes';", "", f"export const {export_const}: Record = {{", ] for lid in sorted(levels.keys()): L = levels[lid] lines.append(f" {lid}: {{") lines.append(f" levelID: {lid},") lines.append( f" boundary: {{ x: {L['boundary']['x']}, y: {L['boundary']['y']} }}," ) if L.get("ground"): lines.append( " ground: " + json.dumps(L["ground"], ensure_ascii=False, separators=(",", ": ")) + "," ) if L.get("border"): lines.append( " border: " + json.dumps(L["border"], ensure_ascii=False, separators=(",", ": ")) + "," ) 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") ap.add_argument("--limit", type=int, default=0, help="Max levels to export (0 = all)") ap.add_argument("--export-const", default="LEVELS_600", help="TS export const name") ap.add_argument( "--prefab-dir", default="", help="Unity Assets/Prefabs/Level 目录,合并 LevelN.prefab 的 Tilemap", ) args = ap.parse_args() text = Path(args.input).read_text(encoding="utf-8") levels = parse_file(text) if args.limit > 0: sorted_ids = sorted(levels.keys())[: args.limit] levels = {k: levels[k] for k in sorted_ids} if not levels: print("No levels parsed", file=sys.stderr) sys.exit(1) prefab_dir = Path(args.prefab_dir) if args.prefab_dir else None merged_prefab = 0 if prefab_dir: for lid, L in levels.items(): maps = parse_level_prefab(prefab_dir / f"Level{lid}.prefab") if maps: L["ground"] = maps.get("ground", {}) L["border"] = maps.get("border", {}) merged_prefab += 1 else: ring = build_border_cache({lid: L})[L["borderKey"]] L["border"] = ring print(f"Merged Tilemap from {merged_prefab} prefabs") else: border_cache = build_border_cache(levels) for L in levels.values(): L["border"] = border_cache[L["borderKey"]] out = Path(args.output) out.parent.mkdir(parents=True, exist_ok=True) out.write_text(emit_ts(levels, args.export_const), encoding="utf-8") if args.border_cache: cache = {str(lid): L.get("border", {}) for lid, L in levels.items()} Path(args.border_cache).write_text(json.dumps(cache, indent=2), encoding="utf-8") print(f"Exported {len(levels)} levels -> {out}") if __name__ == "__main__": main()