226 lines
7.0 KiB
Python
226 lines
7.0 KiB
Python
#!/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}
|