#!/usr/bin/env python3 """用 Unity 源贴图 MD5 匹配恢复 __stage__,并完成 sanxing 命名统一。""" from __future__ import annotations import hashlib import json import os import re import shutil from pathlib import Path ROOT = Path(__file__).resolve().parents[1] TEX = ROOT / "assets" / "resources" / "textures" UNITY_TEX = Path("/Users/liuyufei/tfrh/竞赛/scratch-unity-base/Assets/Texture") # 复用 unify 脚本的 RENAMES import importlib.util _spec = importlib.util.spec_from_file_location( "unify", ROOT / "tools" / "unify-theme-texture-names.py" ) _unify = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(_unify) RENAMES = _unify.RENAMES _norm_rel = _unify._norm_rel build_rename_lookup = _unify.build_rename_lookup move_pair = _unify.move_pair apply_renames = _unify.apply_renames cleanup_empty_dirs = _unify.cleanup_empty_dirs move_legacy = _unify.move_legacy SANXING_KEEP = _unify.SANXING_KEEP THEME_SHIP = _unify.THEME_SHIP build_path_replacements = _unify.build_path_replacements patch_text_files = _unify.patch_text_files patch_themes_database = _unify.patch_themes_database patch_palettes = _unify.patch_palettes UNITY_SOURCES: dict[str, list[Path]] = { "silu": [UNITY_TEX / "silu"], "numMan": [UNITY_TEX / "numMan"], "redArmy": [UNITY_TEX / "redarmy"], "chinese": [UNITY_TEX / "Panda", UNITY_TEX / "Chinese"], "snow": [TEX / "snow"], # snow 仅 Cocos,从现有目录取 "sanxing": [TEX / "sanxing"], } def md5_file(path: Path) -> str: h = hashlib.md5() with path.open("rb") as f: for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) return h.hexdigest() def build_unity_hash_index(theme: str) -> dict[str, str]: """md5 -> theme 内相对路径(不含 .png)""" index: dict[str, str] = {} for src_root in UNITY_SOURCES.get(theme, []): if not src_root.exists(): continue for png in src_root.rglob("*.png"): rel = png.relative_to(src_root).as_posix().replace(".png", "") # chinese: Panda/待机/机器人 -> skin/待机/机器人 便于 lookup if theme == "chinese" and rel.startswith("Panda/"): rel = "skin/" + rel[len("Panda/"):] digest = md5_file(png) index[digest] = rel return index def recover_stages_by_hash() -> int: lookup = build_rename_lookup() recovered = 0 for stage in sorted(TEX.rglob("__stage_*.png")): theme_key = stage.relative_to(TEX).parts[0] digest = md5_file(stage) rel = build_unity_hash_index(theme_key).get(digest) if not rel: continue new_rel = lookup.get((theme_key, Path(rel).name)) or lookup.get((theme_key, rel)) if not new_rel: continue dst = TEX / theme_key / f"{new_rel}.png" if move_pair(stage, dst, skip_if_dst_exists=True): recovered += 1 print(f" hash-recovered {theme_key}: {rel} -> {new_rel}.png") return recovered def copy_from_unity_if_missing(theme: str) -> int: """目标路径缺失时从 Unity 复制并重命名""" lookup = build_rename_lookup() count = 0 for src_root in UNITY_SOURCES.get(theme, []): if not src_root.exists() or src_root == TEX / theme: continue for png in src_root.rglob("*.png"): rel = png.relative_to(src_root).as_posix().replace(".png", "") if theme == "chinese" and rel.startswith("Panda/"): rel = "skin/" + rel[len("Panda/"):] key_name = Path(rel).name new_rel = lookup.get((theme, key_name)) or lookup.get((theme, rel)) if not new_rel: continue dst = TEX / theme / f"{new_rel}.png" if dst.exists(): continue dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(png, dst) count += 1 print(f" copied {theme}: {rel} -> {new_rel}.png") return count def remove_orphan_stages() -> None: legacy_root = TEX / "_orphan_stages" for stage in TEX.rglob("__stage_*.png"): rel = stage.relative_to(TEX) dest = legacy_root / rel dest.parent.mkdir(parents=True, exist_ok=True) move_pair(stage, dest) def main() -> None: print("=== MD5 恢复 __stage__ ===") n_hash = recover_stages_by_hash() print(f" 恢复 {n_hash} 个") print("=== 从 Unity 补全缺失文件 ===") n_copy = 0 for theme in ["silu", "numMan", "redArmy", "chinese"]: n_copy += copy_from_unity_if_missing(theme) print(f" 复制 {n_copy} 个") print("=== 常规重命名 ===") n = apply_renames() print(f" 重命名 {n} 个") print("=== 清理 ===") remove_orphan_stages() for theme in ["silu", "snow", "numMan", "redArmy", "chinese"]: cleanup_empty_dirs(TEX / theme) keep = set(SANXING_KEEP) | set(THEME_SHIP.get(theme, ())) move_legacy(theme, keep) print("=== 更新引用 ===") patch_text_files(build_path_replacements()) patch_themes_database() patch_palettes() print("完成。") if __name__ == "__main__": main()