Files
cocos/tools/export_cocos_level_db.py
刘宇飞 d393302388 Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 15:30:58 +08:00

199 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
从 Cocos 工程导出 levels-database.json权威数据源
地图 / 主题level-prefabs/Level{id}.prefab 上的 LevelMapDatagroundJson / borderJson / theme
实体 spawn / boundary保留 assets/level-data/levels-database.json 中已有条目(关卡编辑器维护)
Unity 主站仅作 ID 对照参考,不参与本导出。
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from level_id import LEVEL_ID_BASE, level_db_path, normalize_level_id, prefab_resource_path
LEVEL_MAP_TYPE = "d4e5fanuMlNDh8qO0xdbn+K"
PREFAB_NAME_RE = re.compile(r"^Level(\d+)\.prefab$", re.I)
def parse_prefab_map_data(prefab_path: Path) -> dict | None:
try:
objs = json.loads(prefab_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
print(f"warn: skip {prefab_path.name}: {e}", file=sys.stderr)
return None
if not isinstance(objs, list):
return None
for obj in objs:
if not isinstance(obj, dict):
continue
if obj.get("__type__") != LEVEL_MAP_TYPE and "groundJson" not in obj:
continue
if "groundJson" not in obj and "levelID" not in obj:
continue
level_id = int(obj.get("levelID") or 0)
ground = {}
border = {}
try:
ground = json.loads(obj.get("groundJson") or "{}")
except json.JSONDecodeError:
pass
try:
border = json.loads(obj.get("borderJson") or "{}")
except json.JSONDecodeError:
pass
theme = str(obj.get("theme") or "").strip() or None
return {
"levelID": level_id,
"ground": ground if isinstance(ground, dict) else {},
"border": border if isinstance(border, dict) else {},
"theme": theme,
}
return None
def load_existing_db(path: Path) -> dict:
if not path.is_file():
return {"levels": {}}
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {"levels": {}}
def merge_entry(level_id: int, map_data: dict | None, prev: dict | None) -> dict:
ext_id = normalize_level_id(level_id)
out: dict = {
"levelID": ext_id,
"boundary": (prev or {}).get("boundary") or {"x": 20, "y": 20},
"spawns": list((prev or {}).get("spawns") or []),
"cocosPrefab": prefab_resource_path(ext_id),
}
if prev and prev.get("unityPrefab"):
out["unityPrefab"] = prev["unityPrefab"]
if map_data:
if map_data.get("ground"):
out["ground"] = map_data["ground"]
if map_data.get("border"):
out["border"] = map_data["border"]
if map_data.get("theme"):
out["theme"] = map_data["theme"]
if map_data.get("levelID") and map_data["levelID"] > 0:
out["levelID"] = normalize_level_id(map_data["levelID"])
elif prev:
for k in ("ground", "border", "theme", "entityTextures"):
if prev.get(k) is not None:
out[k] = prev[k]
return out
def export_db(project: Path, output: Path, prefab_dir: Path, level_id: int | None = None) -> dict:
existing = load_existing_db(output)
prev_levels = existing.get("levels") or {}
levels: dict[str, dict] = dict(prev_levels)
prefab_files = sorted(prefab_dir.glob("Level*.prefab"), key=lambda p: p.name)
if level_id is not None:
target = normalize_level_id(level_id)
prefab_files = [
p for p in prefab_files
if (m := PREFAB_NAME_RE.match(p.name)) and int(m.group(1)) == target
]
if not prefab_files and level_id is None:
print(f"error: no prefabs in {prefab_dir}", file=sys.stderr)
sys.exit(1)
if level_id is not None and not prefab_files:
print(f"error: prefab Level{normalize_level_id(level_id)} not found in {prefab_dir}", file=sys.stderr)
sys.exit(1)
for pf in prefab_files:
m = PREFAB_NAME_RE.match(pf.name)
if not m:
continue
lid = int(m.group(1))
map_data = parse_prefab_map_data(pf)
prev = prev_levels.get(str(lid)) or prev_levels.get(str(normalize_level_id(lid)))
levels[str(normalize_level_id(lid))] = merge_entry(lid, map_data, prev)
if level_id is None:
# 保留仅有 DB 条目、尚无预制体的关卡(编辑器先写 spawns
for key, prev in prev_levels.items():
if key not in levels:
try:
lid = int(key)
except ValueError:
continue
levels[key] = merge_entry(lid, None, prev)
prev_stats = existing.get("stats") or {}
total_with_maps = sum(
1 for v in levels.values() if (v.get("ground") or v.get("border"))
)
return {
"version": 2,
"generatedAt": datetime.now(timezone.utc).isoformat(),
"source": "Cocos level-prefabs LevelMapData + levels-database spawns",
"levelIdBase": LEVEL_ID_BASE,
"stats": {
"total": len(levels),
"withPrefabTilemap": total_with_maps,
"withBoundaryRing": prev_stats.get("withBoundaryRing", 0),
},
"levels": dict(sorted(levels.items(), key=lambda kv: int(kv[0]))),
}
def resolve_prefab_dir(project: Path) -> Path:
"""迁移后预制体在 bundle-level-prefabs/level-prefabs否则仍在 resources 下。"""
bundle = project / "assets/bundle-level-prefabs/level-prefabs"
legacy = project / "assets/resources/level-prefabs"
if bundle.is_dir():
return bundle
return legacy
def main():
ap = argparse.ArgumentParser(description="从 Cocos 预制体导出 levels-database.json")
ap.add_argument(
"--project",
default=str(Path(__file__).resolve().parent.parent),
help="Cocos 工程根目录",
)
ap.add_argument(
"--output",
default="",
help="输出 JSON默认 assets/level-data/levels-database.json",
)
ap.add_argument(
"--prefab-dir",
default="",
help="关卡预制体目录(默认自动检测 bundle-level-prefabs 或 resources",
)
ap.add_argument(
"--level-id",
type=int,
default=None,
help="仅导出/合并指定关卡(增量更新 levels-database.json",
)
args = ap.parse_args()
project = Path(args.project)
output = Path(args.output) if args.output else level_db_path(project)
prefab_dir = Path(args.prefab_dir) if args.prefab_dir else resolve_prefab_dir(project)
data = export_db(project, output, prefab_dir, args.level_id)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Exported {data['stats']['total']} levels -> {output}")
print(f" with prefab tilemap: {data['stats']['withPrefabTilemap']}")
if __name__ == "__main__":
main()