#!/usr/bin/env python3 """从 Unity Level{N}.prefab 解析 Tilemap(Ground / Border)格子数据。 贴图以 Tilemap 的 m_TileSpriteArray 为准(和 Unity 编辑器里看到的砖一致), 不按关卡主题改名、不换层、不把 JumpBlock 收成 Baseblock。 """ from __future__ import annotations import re from functools import lru_cache from pathlib import Path GROUND_TILES = frozenset({"Baseblock", "JumpBlock"}) BORDER_DECOR = frozenset( { "WallBlock", "kuai11", "Decor23", "素材切图-23", "素材切图2-23", "小游戏素材红色_03", } ) UNITY_FOLDER_TO_COCOS = { "Chinese": "chinese", "chinese": "chinese", "RedArmy": "redArmy", "redArmy": "redArmy", "redarmy": "redArmy", "numMan": "numMan", "sanxing": "sanxing", "snow": "snow", "silu": "silu", "SILU": "silu", } def tile_basename(name: str | None) -> str: if not name: return "" return name.rsplit("/", 1)[-1] def parse_tilemap_layer(text: str, layer_name: str) -> dict[str, int]: """返回格子 → 该 Tilemap 本地 m_TileIndex。""" cells = parse_tilemap_cells(text, layer_name) return {k: v[0] for k, v in cells.items()} def parse_tilemap_cells(text: str, layer_name: str) -> dict[str, tuple[int, int]]: """格子 → (m_TileIndex, m_TileSpriteIndex)。""" part = _tilemap_part(text, layer_name) if not part: return {} tiles: dict[str, tuple[int, int]] = {} for m in re.finditer( r"first: \{x: (-?\d+), y: (-?\d+), z: 0\}[\s\S]*?m_TileIndex: (\d+)[\s\S]*?m_TileSpriteIndex: (\d+)", part, ): x, y = int(m.group(1)), int(m.group(2)) tiles[f"{x},{y}"] = (int(m.group(3)), int(m.group(4))) return tiles def _tilemap_part(text: str, layer_name: str) -> str | None: parts = re.split(r"(?=--- !u!1 &)", text) for part in parts: if f"m_Name: {layer_name}" not in part or "Tilemap:" not in part: continue return part return None def _guid_array(tilemap_part: str, header: str, stop_tokens: tuple[str, ...]) -> list[str | None]: assets: list[str | None] = [] in_arr = False for line in tilemap_part.splitlines(): if header in line: in_arr = True continue if in_arr and any(tok in line for tok in stop_tokens): break if not in_arr: continue if "guid:" in line: m = re.search(r"guid: ([a-f0-9]+)", line) assets.append(m.group(1) if m else None) elif "m_Data: {fileID: 0}" in line: assets.append(None) return assets def _tile_asset_guids(tilemap_part: str) -> list[str | None]: """按 m_TileAssetArray 顺序解析 guid(空槽为 None)。""" return _guid_array( tilemap_part, "m_TileAssetArray:", ("m_TileSpriteArray", "m_AnimationFrameRate"), ) def _tile_sprite_guids(tilemap_part: str) -> list[str | None]: return _guid_array( tilemap_part, "m_TileSpriteArray:", ("m_TileMatrixArray", "m_TileColorArray", "m_AnimationFrameRate"), ) def _cocos_folder(unity_folder: str) -> str: if unity_folder in UNITY_FOLDER_TO_COCOS: return UNITY_FOLDER_TO_COCOS[unity_folder] return unity_folder[:1].lower() + unity_folder[1:] if unity_folder else "" @lru_cache(maxsize=1) def _guid_to_tile_name(unity_texture_root: str) -> dict[str, str]: """guid → Cocos 贴图引用,如 numMan/Baseblock。""" root = Path(unity_texture_root) out: dict[str, str] = {} if not root.is_dir(): return out for meta in root.rglob("*.meta"): name = meta.name if not (name.endswith(".png.meta") or name.endswith(".asset.meta")): continue try: text = meta.read_text(encoding="utf-8", errors="ignore") except OSError: continue m = re.search(r"^guid: ([a-f0-9]+)", text, re.M) if not m: continue stem = name.replace(".png.meta", "").replace(".asset.meta", "") try: rel = meta.relative_to(root) except ValueError: out[m.group(1)] = stem continue if len(rel.parts) >= 2: out[m.group(1)] = f"{_cocos_folder(rel.parts[0])}/{stem}" else: out[m.group(1)] = stem return out def tile_asset_names(text: str, layer_name: str, unity_texture_root: Path | None = None) -> list[str | None]: part = _tilemap_part(text, layer_name) if not part: return [] guids = _tile_asset_guids(part) if unity_texture_root is None: return [None] * len(guids) mapping = _guid_to_tile_name(str(unity_texture_root)) return [mapping.get(g) if g else None for g in guids] def tile_sprite_names(text: str, layer_name: str, unity_texture_root: Path | None = None) -> list[str | None]: part = _tilemap_part(text, layer_name) if not part: return [] guids = _tile_sprite_guids(part) if unity_texture_root is None: return [None] * len(guids) mapping = _guid_to_tile_name(str(unity_texture_root)) return [mapping.get(g) if g else None for g in guids] def normalize_ground_tile(name: str | None) -> str: return "JumpBlock" if tile_basename(name) == "JumpBlock" else "Baseblock" def normalize_border_tile(name: str | None) -> str: base = tile_basename(name) if not name or base in GROUND_TILES: return "WallBlock" if base in BORDER_DECOR: return name return name def _resolve_name(assets: list[str | None], tile_index: int) -> str | None: if 0 <= tile_index < len(assets): return assets[tile_index] return None def parse_level_prefab(prefab_path: Path, unity_texture_root: Path | None = None) -> dict: """按 Unity 原层导入:只保留有 Tile Asset 的格子,空槽不补砖。""" if not prefab_path.is_file(): return {} text = prefab_path.read_text(encoding="utf-8") g_cells = parse_tilemap_cells(text, "Ground") b_cells = parse_tilemap_cells(text, "Border") if not g_cells and not b_cells: return {} if unity_texture_root is None: unity_texture_root = prefab_path.resolve().parents[2] / "Texture" g_assets = tile_asset_names(text, "Ground", unity_texture_root) b_assets = tile_asset_names(text, "Border", unity_texture_root) ground: dict[str, str] = {} border: dict[str, str] = {} for key, (ti, _si) in g_cells.items(): asset = _resolve_name(g_assets, ti) if not asset: continue base = tile_basename(asset) ground[key] = "JumpBlock" if base == "JumpBlock" else base if base in GROUND_TILES else "Baseblock" for key, (ti, _si) in b_cells.items(): asset = _resolve_name(b_assets, ti) if not asset: # Unity 空槽不渲染,禁止补 WallBlock(否则空隙里会多出砖) continue base = tile_basename(asset) if base in GROUND_TILES: continue border[key] = base if base else "WallBlock" return {"ground": ground, "border": border}