#!/usr/bin/env python3 """ 从 Unity Assets/Texture 导入 PNG 到 Cocos assets/resources/textures/ 用法: python3 tools/import_unity_textures.py \\ --unity-root "/path/to/主站" \\ [--themes silu,snow,default] """ from __future__ import annotations import argparse import json import shutil import uuid from pathlib import Path # 与 GameController UIStyleNames 对应(Unity 目录名 → Cocos 子目录) THEME_MAP = { "default": ".", # Assets/Texture 根目录单层 png "silu": "silu", "chinese": "Chinese", "redArmy": "redarmy", "numMan": "numMan", "snow": "snow", "sanxing": "sanxing", } # 角色 + 砖块核心文件名(各主题有则拷贝,无则跳过) CORE_NAMES = { "Baseblock.png", "JumpBlock.png", "WallBlock.png", "player_F.png", "player_B.png", "ship_F.png", "ship_B.png", "Ship_F.png", "Prop.png", "nProp.png", "kuai11.png", "素材切图-23.png", "素材切图2-23.png", "Prop_kuai1.png", "Prop_kuai2.png", "Prop_kuai.png", "nProp_kuai1.png", "nProp_kuai2.png", "nProp_kuai.png", "Prop_Dumpling.png", "Prop_Mooncake.png", "Prop_Ricedumpling.png", "Prop_Zhongzi.png", "Prop_star.png", "nProp_star.png", "nProp_Dumpling.png", "nProp_Mooncake.png", "nProp_Ricedumpling.png", "nProp_Zhongzi.png", } # 丝路:对齐 sanxing/snow 命名(skin/待机正面、*Ship_F、Prop_kuai1) SILU_CANONICAL_COPIES: list[tuple[str, str]] = [ ("ship_F.png", "siluShip_F.png"), ("ship_B.png", "siluShip_B.png"), ("Prop.png", "Prop_kuai1.png"), ("nProp.png", "nProp_kuai1.png"), ] def ensure_directory_meta(meta_path: Path) -> None: if meta_path.is_file(): return meta_path.write_text( json.dumps( { "ver": "1.2.0", "importer": "directory", "imported": True, "uuid": str(uuid.uuid4()), "files": [], "subMetas": {}, "userData": {}, }, indent=2, ), encoding="utf-8", ) def ensure_image_meta(meta_path: Path, display_name: str) -> None: if meta_path.is_file(): return base_uuid = str(uuid.uuid4()) meta_path.write_text( json.dumps( { "ver": "1.0.27", "importer": "image", "imported": False, "uuid": base_uuid, "files": [".json", ".png"], "subMetas": { "6c48a": { "importer": "texture", "uuid": f"{base_uuid}@6c48a", "displayName": display_name, "id": "6c48a", "name": "texture", "userData": { "imageUuidOrDatabaseUri": base_uuid, "isUuid": True, "visible": False, }, "ver": "1.0.22", "imported": False, "files": [".json"], "subMetas": {}, } }, "userData": {"type": "texture", "redirect": f"{base_uuid}@6c48a"}, }, indent=2, ), encoding="utf-8", ) def ensure_png_meta(png_path: Path) -> None: ensure_image_meta(png_path.with_suffix(".png.meta"), png_path.stem) def first_existing_file(*candidates: Path) -> Path | None: for path in candidates: if path.is_file(): return path return None def copy_skin_tree(src_skin: Path, dst_skin: Path) -> None: """将 player/skin 整树复制到主题根 skin(对齐 sanxing 目录结构)。""" if not src_skin.is_dir(): return dst_skin.mkdir(parents=True, exist_ok=True) ensure_directory_meta(dst_skin.parent / f"{dst_skin.name}.meta") for src in sorted(src_skin.rglob("*")): rel = src.relative_to(src_skin) dst = dst_skin / rel if src.is_dir(): dst.mkdir(parents=True, exist_ok=True) ensure_directory_meta(dst.parent / f"{dst.name}.meta") continue if src.suffix.lower() != ".png": continue dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst) ensure_png_meta(dst) def normalize_silu_assets(theme_dir: Path) -> None: """拷贝丝路实体贴图为标准文件名(保留 Unity 原名,额外写规范别名)。""" skin_root = theme_dir / "skin" player_skin = theme_dir / "player" / "skin" if player_skin.is_dir(): copy_skin_tree(player_skin, skin_root) skin_front = skin_root / "待机正面" skin_back = skin_root / "待机背面" skin_front.mkdir(parents=True, exist_ok=True) skin_back.mkdir(parents=True, exist_ok=True) ensure_directory_meta(skin_root.parent / f"{skin_root.name}.meta") ensure_directory_meta(skin_front.parent / f"{skin_front.name}.meta") ensure_directory_meta(skin_back.parent / f"{skin_back.name}.meta") front_src = first_existing_file( skin_front / "1.png", player_skin / "待机正面" / "1.png", theme_dir / "player" / "idel-正" / "1.png", theme_dir / "player_F.png", ) if front_src and front_src != skin_front / "1.png": shutil.copy2(front_src, skin_front / "1.png") if (skin_front / "1.png").is_file(): ensure_png_meta(skin_front / "1.png") back_src = first_existing_file( skin_back / "1.png", player_skin / "待机背面" / "1.png", theme_dir / "player" / "idel-反" / "1.png", theme_dir / "player_B.png", ) if back_src and back_src != skin_back / "1.png": shutil.copy2(back_src, skin_back / "1.png") if (skin_back / "1.png").is_file(): ensure_png_meta(skin_back / "1.png") for src_name, dst_name in SILU_CANONICAL_COPIES: src = theme_dir / src_name dst = theme_dir / dst_name if src.is_file(): shutil.copy2(src, dst) ensure_png_meta(dst) def normalize_nprop_filenames(theme_dir: Path) -> None: """Unity numMan 等主题 nProp 文件名可能带尾空格,统一为无空格文件名。""" for png in theme_dir.rglob("nProp*.png"): stem = png.stem if stem != stem.rstrip(): fixed = png.with_name(f"{stem.rstrip()}.png") if fixed != png: if fixed.is_file(): png.unlink() else: png.rename(fixed) meta = png.with_suffix(".png.meta") fixed_meta = fixed.with_suffix(".png.meta") if meta.is_file() and not fixed_meta.is_file(): meta.rename(fixed_meta) # HUD 按钮(UIMain 右侧,与 themes-database.json hud 字段对应) HUD_GLOBS = ( "anniu_*.png", "redarmy*.png", "素材切图-*.png", "素材切图2-*.png", "1倍速.png", "2倍速.png", "4倍速.png", "声音.png", "声音关闭.png", "导航图标.png", "重置.png", "放大图标.png", "缩小图标.png", ) def copy_hud_assets(unity_tex: Path, out_root: Path, style_key: str, rel: str) -> int: """拷贝 Unity UI 按钮贴图(原先 rglob 会跳过 anniu_/倍速/声音)""" src = unity_tex if rel == "." else unity_tex / rel if not src.is_dir(): return 0 dst = out_root / style_key dst.mkdir(parents=True, exist_ok=True) count = 0 seen: set[str] = set() for pattern in HUD_GLOBS: for png in src.glob(pattern): if not png.is_file(): continue key = str(png.resolve()) if key in seen: continue seen.add(key) target = dst / png.name shutil.copy2(png, target) count += 1 return count def copy_theme(unity_tex: Path, out_root: Path, style_key: str, rel: str) -> int: src = unity_tex if rel == "." else unity_tex / rel dst = out_root / style_key dst.mkdir(parents=True, exist_ok=True) count = 0 if rel == ".": for png in src.glob("*.png"): shutil.copy2(png, dst / png.name) if png.name == "Ship_F.png": shutil.copy2(png, dst / "ship_F.png") count += 1 return count if not src.is_dir(): print(f" skip missing theme dir: {src}") return 0 for png in src.rglob("*.png"): rel_path = png.relative_to(src) # 跳过 HUD 专用碎图(由 copy_hud_assets 扁平拷贝到主题根目录) if png.name.startswith("anniu_") or "倍速" in png.name or png.name.startswith("声音"): continue if png.name.startswith("素材切图") or png.name.startswith("redarmy"): continue target = dst / rel_path target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(png, target) count += 1 # silu 装饰砖块别名(烘焙脚本用 Decor23) decor = dst / "素材切图-23.png" if decor.is_file(): shutil.copy2(decor, dst / "Decor23.png") normalize_nprop_filenames(dst) if style_key == "silu": normalize_silu_assets(dst) return count def copy_prop(unity_tex: Path, out_root: Path) -> int: src = unity_tex / "Prop" if not src.is_dir(): return 0 dst = out_root / "prop" dst.mkdir(parents=True, exist_ok=True) n = 0 for png in src.glob("*.png"): name = png.name.replace("载具 正面", "vehicle_F").replace("载具 背面", "vehicle_B") shutil.copy2(png, dst / name) n += 1 return n def main(): ap = argparse.ArgumentParser() ap.add_argument("--unity-root", required=True) ap.add_argument("--out", default="assets/resources/textures") ap.add_argument("--themes", default=",".join(THEME_MAP.keys())) args = ap.parse_args() project = Path(__file__).resolve().parent.parent unity_tex = Path(args.unity_root) / "Assets" / "Texture" out_root = project / args.out if not unity_tex.is_dir(): raise SystemExit(f"Unity Texture 不存在: {unity_tex}") themes = [t.strip() for t in args.themes.split(",") if t.strip()] total = 0 for key in themes: rel = THEME_MAP.get(key) if rel is None: print(f"unknown theme: {key}") continue n = copy_theme(unity_tex, out_root, key, rel) hn = copy_hud_assets(unity_tex, out_root, key, rel) print(f" {key}: {n} png (+ {hn} hud)") total += n + hn pn = copy_prop(unity_tex, out_root) print(f" prop: {pn} png") total += pn print(f"Imported {total} files -> {out_root}") print("请在 Cocos Creator 中刷新 assets/resources/textures,然后运行:") print(" python3 tools/fix_tile_texture_metas.py") if __name__ == "__main__": main()