#!/usr/bin/env python3 """ 从 Unity 全量导出关卡到单一 JSON 数据库(方便后期增删查改)。 数据来源: - Assets/Scripts/Core/Levels*.cs → spawns / boundary / levelPath - Assets/Prefabs/Level/LevelN.prefab → Ground / Border Tilemap 用法: python3 tools/export_all_levels.py \\ --unity-root "/path/to/主站" \\ --output assets/level-data/levels-database.json """ from __future__ import annotations import argparse import json import re import sys from datetime import datetime, timezone from pathlib import Path from export_unity_levels import ( LEVEL_RE, BOUND_RE, SPAWN_RE, POS_RE, PATH_RE, PDIR_RE, VDIR_RE, kind_from_path, parse_spawns, build_border_cache, ) from export_unity_prefab_maps import parse_level_prefab from level_id import LEVEL_ID_BASE, normalize_level_id, prefab_resource_path LEVEL_PATH_RE = re.compile( r'levelPath\s*=\s*"?([^",\s]+Level\d+\.prefab)"?', re.I ) def parse_level_block(chunk: str, lid: int) -> dict: bound = BOUND_RE.search(chunk) bx, by = (10, 10) if bound: bx, by = int(bound.group(1)), int(bound.group(2)) path_m = LEVEL_PATH_RE.search(chunk) level_path = path_m.group(1) if path_m else f"Assets/Prefabs/Level/Level{lid}.prefab" ext_id = normalize_level_id(lid) return { "levelID": ext_id, "boundary": {"x": bx, "y": by}, "spawns": parse_spawns(chunk), "unityPrefab": level_path.replace("\\", "/"), "cocosPrefab": prefab_resource_path(ext_id), } def parse_levels_cs_file(path: Path) -> dict[int, dict]: text = path.read_text(encoding="utf-8") 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) levels[lid] = parse_level_block(text[start:end], lid) return levels def parse_all_levels_cs(core_dir: Path) -> dict[int, dict]: merged: dict[int, dict] = {} for cs in sorted(core_dir.glob("Levels*.cs")): part = parse_levels_cs_file(cs) for lid, L in part.items(): if lid in merged: print(f"warn: duplicate level id {lid} in {cs.name}, keep first", file=sys.stderr) continue merged[lid] = L return merged def prefab_path_for_level(L: dict, prefab_dir: Path) -> Path | None: rel = L.get("unityPrefab", "") if rel: name = Path(rel).name p = prefab_dir / name if p.is_file(): return p p = prefab_dir / f"Level{L['levelID']}.prefab" return p if p.is_file() else None def merge_prefab_maps(L: dict, prefab_dir: Path) -> None: p = prefab_path_for_level(L, prefab_dir) if not p: bx, by = L["boundary"]["x"], L["boundary"]["y"] ring = build_border_cache({L["levelID"]: {**L, "borderKey": f"{bx},{by}"}}) L["border"] = ring[f"{bx},{by}"] L["_mapSource"] = "boundary_ring" return maps = parse_level_prefab(p) if maps.get("ground"): L["ground"] = maps["ground"] if maps.get("border"): L["border"] = maps["border"] L["_mapSource"] = "unity_prefab" def strip_internal(L: dict) -> dict: out = { "levelID": L["levelID"], "boundary": L["boundary"], "spawns": L["spawns"], } if L.get("unityPrefab"): out["unityPrefab"] = L["unityPrefab"] if L.get("cocosPrefab"): out["cocosPrefab"] = L["cocosPrefab"] if L.get("ground"): out["ground"] = L["ground"] if L.get("border"): out["border"] = L["border"] return out def main(): ap = argparse.ArgumentParser() ap.add_argument("--unity-root", required=True, help="Unity 项目根目录(含 Assets)") ap.add_argument("--output", required=True, help="输出 levels-database.json") ap.add_argument("--limit", type=int, default=0, help="仅导出前 N 个关卡(调试用)") ap.add_argument("--skip-prefab-maps", action="store_true", help="不解析 Unity prefab 瓦片(快,地图由 Cocos 预制体承担)") args = ap.parse_args() unity = Path(args.unity_root) core = unity / "Assets/Scripts/Core" prefab_dir = unity / "Assets/Prefabs/Level" if not core.is_dir(): print(f"Core dir not found: {core}", file=sys.stderr) sys.exit(1) print("Parsing Levels*.cs …") levels = parse_all_levels_cs(core) ids = sorted(levels.keys()) if args.limit > 0: ids = ids[: args.limit] levels = {k: levels[k] for k in ids} print(f"Level definitions: {len(levels)}") from_prefab = 0 from_ring = 0 if args.skip_prefab_maps: print("Skipping prefab Tilemaps (--skip-prefab-maps)") else: print("Merging prefab Tilemaps …") for i, lid in enumerate(ids): if (i + 1) % 200 == 0: print(f" … {i + 1}/{len(ids)}") merge_prefab_maps(levels[lid], prefab_dir) if levels[lid].get("_mapSource") == "unity_prefab": from_prefab += 1 else: from_ring += 1 payload = { "version": 1, "generatedAt": datetime.now(timezone.utc).isoformat(), "source": "Unity Levels*.cs + Assets/Prefabs/Level/*.prefab", "stats": { "total": len(levels), "withPrefabTilemap": from_prefab, "withBoundaryRing": from_ring, }, "levelIdBase": LEVEL_ID_BASE, "levels": {str(levels[lid]["levelID"]): strip_internal(levels[lid]) for lid in ids}, } out = Path(args.output) out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") print(f"Wrote {out} ({out.stat().st_size // 1024} KB)") print(f" prefab tilemap: {from_prefab}, fallback ring: {from_ring}") if __name__ == "__main__": main()