Files
cocos/tools/export_unity_levels.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

241 lines
7.7 KiB
Python
Raw 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
"""
从 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<Spawn>\(\)\{",
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*.csground/border 来自 Assets/Prefabs/Level/LevelN.prefab Tilemap */",
"import { Direction } from '../core/Define';",
"import { LevelConfig } from './LevelTypes';",
"",
f"export const {export_const}: Record<number, LevelConfig> = {{",
]
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()