Files
cocos/tools/unify-theme-texture-names.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

454 lines
18 KiB
Python

#!/usr/bin/env python3
"""将各主题 textures 文件名统一为与 sanxing 相同的命名规范。"""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TEX = ROOT / "assets" / "resources" / "textures"
RENAMES: list[tuple[str, str, str]] = []
def add(theme: str, old: str, new: str) -> None:
old = old.replace("\\", "/")
new = new.replace("\\", "/")
if not old.endswith(".png"):
old += ".png"
if not new.endswith(".png"):
new += ".png"
RENAMES.append((theme, old, new))
# ── silu ──
for src, dst in [
("素材切图_画板 1", "bg"),
("素材切图-03", "anniu_03"),
("素材切图-05", "anniu_06"),
("1倍速", "anniu_08"),
("2倍速", "anniu_10"),
("4倍速", "anniu_12"),
("素材切图-09", "anniu_17"),
("素材切图-10", "anniu_19"),
("声音", "anniu_22"),
("声音关闭", "anniu_21"),
("素材切图-23", "kuai11"),
("Decor23", "kuai11"),
]:
add("silu", src, dst)
for i, src in enumerate(["机器人", "机器人2", "机器人3", "机器人4"], start=1):
add("silu", f"skin/跳背面/{src}", f"skin/跳背面/{i}")
# ── snow ──
add("snow", "Prop_kuai2", "Prop_kuai1")
add("snow", "nProp_kuai2", "nProp_kuai1")
# ── numMan ──
for i in range(1, 5):
add("numMan", f"skin/待机/待机{i}", f"skin/待机正面/{i}")
for i in range(1, 3):
add("numMan", f"skin/待机背面/待机{i}", f"skin/待机背面/{i}")
for i in range(1, 5):
add("numMan", f"skin/走/走{i}", f"skin/走/{i}")
add("numMan", f"skin/走背面/走背面{i}", f"skin/走背面/{i}")
add("numMan", "skin/跳/跳1", "skin/跳/1")
add("numMan", "skin/跳/跳2", "skin/跳/2")
add("numMan", "skin/跳/跳4", "skin/跳/3")
add("numMan", "skin/跳/跳5", "skin/跳/4")
add("numMan", "skin/跳背面/跳背面1", "skin/跳背面/1")
add("numMan", "skin/跳背面/跳背面2", "skin/跳背面/2")
add("numMan", "skin/跳背面/跳背面4", "skin/跳背面/3")
add("numMan", "skin/跳背面/跳背面5", "skin/跳背面/4")
add("numMan", "Prop_kuai", "Prop_kuai1")
add("numMan", "nProp_kuai", "nProp_kuai1")
# ── redArmy ──
for src, dst in [
("小游戏素材红色_03", "kuai11"),
("redarmy02", "anniu_03"),
("redarmy09", "anniu_06"),
("redarmy11", "anniu_08"),
("redarmy13", "anniu_10"),
("redarmy15", "anniu_12"),
("redarmy20", "anniu_17"),
("redarmy29", "anniu_19"),
("redarmy35", "anniu_22"),
("redarmy36", "anniu_21"),
("Prop_star", "Prop_kuai1"),
("nProp_star", "nProp_kuai1"),
("redarmyship_F", "redArmyShip_F"),
("redarmyship_B", "redArmyShip_B"),
]:
add("redArmy", src, dst)
for i in range(1, 3):
add("redArmy", f"skin/待机/小红军待机{i}", f"skin/待机正面/{i}")
add("redArmy", f"skin/待机背面/小红军待机{i}", f"skin/待机背面/{i}")
for i in range(1, 4):
add("redArmy", f"skin/走/小红军走{i}", f"skin/走/{i}")
add("redArmy", f"skin/走背面/小红军走{i}", f"skin/走背面/{i}")
add("redArmy", f"skin/跳/小红军跳{i}", f"skin/跳/{i}")
add("redArmy", f"skin/跳背面/小红军跳背面{i}", f"skin/跳背面/{i}")
# ── chinese ──
for src, dst in [
("素材切图2_画板 1", "bg"),
("素材切图2-23", "kuai11"),
("素材切图2-02", "anniu_03"),
("素材切图2-03", "anniu_06"),
("素材切图2-06", "anniu_08"),
("素材切图2-07", "anniu_10"),
("素材切图2-08", "anniu_12"),
("素材切图2-09", "anniu_17"),
("素材切图2-10", "anniu_19"),
("素材切图2-11", "anniu_22"),
("素材切图2-12", "anniu_21"),
("Prop_Dumpling", "Prop_kuai1"),
("nProp_Dumpling", "nProp_kuai1"),
("yun_F", "chineseShip_F"),
("yun_B", "chineseShip_B"),
]:
add("chinese", src, dst)
add("chinese", "skin/待机/机器人", "skin/待机正面/1")
add("chinese", "skin/待机/机器人-1", "skin/待机正面/2")
add("chinese", "skin/待机背面/机器人", "skin/待机背面/1")
add("chinese", "skin/待机背面/机器人-1", "skin/待机背面/2")
add("chinese", "skin/走/机器人", "skin/走/1")
add("chinese", "skin/走/机器人-1", "skin/走/2")
add("chinese", "skin/走/机器人-2", "skin/走/3")
add("chinese", "skin/走背面/机器人", "skin/走背面/1")
add("chinese", "skin/走背面/机器人-1", "skin/走背面/2")
add("chinese", "skin/走背面/机器人-2", "skin/走背面/3")
add("chinese", "skin/跳/机器人", "skin/跳/1")
add("chinese", "skin/跳/机器人-1", "skin/跳/2")
add("chinese", "skin/跳/机器人-2", "skin/跳/3")
add("chinese", "skin/跳背面/机器人", "skin/跳背面/1")
add("chinese", "skin/跳背面/机器人-1", "skin/跳背面/2")
add("chinese", "skin/跳背面/机器人-2", "skin/跳背面/3")
def _norm_rel(p: str) -> str:
p = p.replace("\\", "/")
return p[:-4] if p.endswith(".png") else p
def build_rename_lookup() -> dict[tuple[str, str], str]:
lookup: dict[tuple[str, str], str] = {}
for theme, old, new in RENAMES:
old_n = _norm_rel(old)
new_n = _norm_rel(new)
lookup[(theme, Path(old_n).name)] = new_n
lookup[(theme, old_n)] = new_n
return lookup
def move_pair(src: Path, dst: Path, *, skip_if_dst_exists: bool = False) -> bool:
if not src.exists():
return False
dst.parent.mkdir(parents=True, exist_ok=True)
if dst.exists():
if skip_if_dst_exists:
legacy = dst.parent / "_legacy" / "duplicates" / src.name
legacy.parent.mkdir(parents=True, exist_ok=True)
return move_pair(src, legacy)
return False
os.rename(src, dst)
meta_src = Path(str(src) + ".meta")
meta_dst = Path(str(dst) + ".meta")
if meta_src.exists():
if meta_dst.exists():
meta_dst.unlink()
os.rename(meta_src, meta_dst)
return True
def read_meta_display_name(meta_path: Path) -> str | None:
if not meta_path.exists():
return None
try:
data = json.loads(meta_path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return None
for sub in (data.get("subMetas") or {}).values():
name = sub.get("displayName")
if name:
return str(name)
return None
def recover_stages_from_meta() -> int:
lookup = build_rename_lookup()
recovered = 0
for stage in sorted(TEX.rglob("__stage_*.png")):
theme_key = stage.relative_to(TEX).parts[0]
display = read_meta_display_name(Path(str(stage) + ".meta"))
if not display:
continue
new_rel = lookup.get((theme_key, display)) or lookup.get((theme_key, _norm_rel(display)))
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" recovered {theme_key}/{display} -> {new_rel}.png")
return recovered
def apply_renames() -> int:
count = 0
for theme, old_rel, new_rel in RENAMES:
src = TEX / theme / old_rel
dst = TEX / theme / new_rel
if not src.exists():
continue
if dst.exists():
move_pair(src, src.parent / "_legacy" / "duplicates" / src.name)
continue
if move_pair(src, dst):
count += 1
return count
def cleanup_empty_dirs(theme_dir: Path) -> None:
for dirpath, dirnames, filenames in os.walk(theme_dir, topdown=False):
p = Path(dirpath)
if p == theme_dir:
continue
has_png = any(f.endswith(".png") for f in filenames)
if not has_png and not dirnames:
for f in list(p.iterdir()):
f.unlink()
meta = Path(str(p) + ".meta")
if meta.exists():
meta.unlink()
p.rmdir()
SANXING_KEEP = {
"bg", "Baseblock", "JumpBlock", "WallBlock", "kuai11",
"Prop_kuai1", "nProp_kuai1",
"anniu_03", "anniu_06", "anniu_08", "anniu_10", "anniu_12",
"anniu_17", "anniu_19", "anniu_21", "anniu_22",
}
THEME_SHIP = {
"silu": ("siluShip_F", "siluShip_B"),
"snow": ("snowShip_F", "snowShip_B"),
"numMan": ("numManShip_F", "numManShip_B"),
"redArmy": ("redArmyShip_F", "redArmyShip_B"),
"chinese": ("chineseShip_F", "chineseShip_B"),
"sanxing": ("sanxingShip_F", "sanxingShip_B"),
}
def move_legacy(theme: str, keep_names: set[str]) -> None:
theme_dir = TEX / theme
legacy = theme_dir / "_legacy"
for path in sorted(theme_dir.rglob("*.png")):
rel = path.relative_to(theme_dir).as_posix()
if rel.startswith("_legacy/") or rel.startswith("__stage_"):
continue
base = rel.replace(".png", "")
if base in keep_names:
continue
if base.startswith("skin/"):
parts = base.split("/")
if len(parts) == 3 and parts[1] in {
"待机正面", "待机背面", "", "走背面", "", "跳背面",
} and re.fullmatch(r"\d+", parts[2]):
continue
dest = legacy / rel
dest.parent.mkdir(parents=True, exist_ok=True)
move_pair(path, dest)
def build_path_replacements() -> dict[str, str]:
pairs = [
("textures/silu/素材切图_画板 1", "textures/silu/bg"),
("textures/silu/素材切图-23", "textures/silu/kuai11"),
("textures/silu/Decor23", "textures/silu/kuai11"),
("textures/silu/素材切图-03", "textures/silu/anniu_03"),
("textures/silu/素材切图-05", "textures/silu/anniu_06"),
("textures/silu/1倍速", "textures/silu/anniu_08"),
("textures/silu/2倍速", "textures/silu/anniu_10"),
("textures/silu/4倍速", "textures/silu/anniu_12"),
("textures/silu/素材切图-09", "textures/silu/anniu_17"),
("textures/silu/素材切图-10", "textures/silu/anniu_19"),
("textures/silu/声音关闭", "textures/silu/anniu_21"),
("textures/silu/声音", "textures/silu/anniu_22"),
("textures/snow/Prop_kuai2", "textures/snow/Prop_kuai1"),
("textures/snow/nProp_kuai2", "textures/snow/nProp_kuai1"),
("textures/numMan/Prop_kuai", "textures/numMan/Prop_kuai1"),
("textures/numMan/nProp_kuai", "textures/numMan/nProp_kuai1"),
("textures/numMan/skin/待机/待机1", "textures/numMan/skin/待机正面/1"),
("textures/numMan/skin/待机背面/待机1", "textures/numMan/skin/待机背面/1"),
("textures/redArmy/小游戏素材红色_03", "textures/redArmy/kuai11"),
("textures/redArmy/Prop_star", "textures/redArmy/Prop_kuai1"),
("textures/redArmy/nProp_star", "textures/redArmy/nProp_kuai1"),
("textures/redArmy/redarmyship_F", "textures/redArmy/redArmyShip_F"),
("textures/redArmy/redarmyship_B", "textures/redArmy/redArmyShip_B"),
("textures/redArmy/redarmy02", "textures/redArmy/anniu_03"),
("textures/redArmy/redarmy09", "textures/redArmy/anniu_06"),
("textures/redArmy/redarmy11", "textures/redArmy/anniu_08"),
("textures/redArmy/redarmy13", "textures/redArmy/anniu_10"),
("textures/redArmy/redarmy15", "textures/redArmy/anniu_12"),
("textures/redArmy/redarmy20", "textures/redArmy/anniu_17"),
("textures/redArmy/redarmy29", "textures/redArmy/anniu_19"),
("textures/redArmy/redarmy35", "textures/redArmy/anniu_22"),
("textures/redArmy/redarmy36", "textures/redArmy/anniu_21"),
("textures/redArmy/skin/待机/小红军待机1", "textures/redArmy/skin/待机正面/1"),
("textures/redArmy/skin/待机背面/小红军待机1", "textures/redArmy/skin/待机背面/1"),
("textures/chinese/素材切图2_画板 1", "textures/chinese/bg"),
("textures/chinese/素材切图2-23", "textures/chinese/kuai11"),
("textures/chinese/素材切图2-02", "textures/chinese/anniu_03"),
("textures/chinese/素材切图2-03", "textures/chinese/anniu_06"),
("textures/chinese/素材切图2-06", "textures/chinese/anniu_08"),
("textures/chinese/素材切图2-07", "textures/chinese/anniu_10"),
("textures/chinese/素材切图2-08", "textures/chinese/anniu_12"),
("textures/chinese/素材切图2-09", "textures/chinese/anniu_17"),
("textures/chinese/素材切图2-10", "textures/chinese/anniu_19"),
("textures/chinese/素材切图2-11", "textures/chinese/anniu_22"),
("textures/chinese/素材切图2-12", "textures/chinese/anniu_21"),
("textures/chinese/Prop_Dumpling", "textures/chinese/Prop_kuai1"),
("textures/chinese/nProp_Dumpling", "textures/chinese/nProp_kuai1"),
("textures/chinese/yun_F", "textures/chinese/chineseShip_F"),
("textures/chinese/yun_B", "textures/chinese/chineseShip_B"),
("textures/chinese/Panda/待机/机器人", "textures/chinese/skin/待机正面/1"),
("textures/chinese/Panda/待机背面/机器人", "textures/chinese/skin/待机背面/1"),
("textures/chinese/Panda/走", "textures/chinese/skin/走"),
("textures/chinese/Panda/跳", "textures/chinese/skin/跳"),
("textures/chinese/Panda/走背面", "textures/chinese/skin/走背面"),
("textures/chinese/Panda/跳背面", "textures/chinese/skin/跳背面"),
("textures/chinese/Panda/待机", "textures/chinese/skin/待机正面"),
('borderDecorKey": "素材切图-23', 'borderDecorKey": "kuai11'),
('borderDecorKey": "素材切图2-23', 'borderDecorKey": "kuai11'),
('borderDecorKey": "小游戏素材红色_03', 'borderDecorKey": "kuai11'),
]
return dict(pairs)
def patch_text_files(reps: dict[str, str]) -> int:
count = 0
for base in [ROOT / "assets", ROOT / "extensions"]:
if not base.exists():
continue
for path in base.rglob("*"):
if path.suffix not in {".ts", ".json", ".js", ".jsx"}:
continue
if "node_modules" in path.parts or "library" in path.parts:
continue
text = path.read_text(encoding="utf-8")
orig = text
for old, new in sorted(reps.items(), key=lambda x: -len(x[0])):
text = text.replace(old, new)
if text != orig:
path.write_text(text, encoding="utf-8")
count += 1
print(f" patched {path.relative_to(ROOT)}")
return count
def patch_themes_database() -> None:
db_path = ROOT / "assets" / "resources" / "theme" / "themes-database.json"
data = json.loads(db_path.read_text(encoding="utf-8"))
themes = data["themes"]
def hud(theme: str) -> dict:
return {
"navigation": f"textures/{theme}/anniu_03",
"revert": f"textures/{theme}/anniu_06",
"speed1": f"textures/{theme}/anniu_08",
"speed2": f"textures/{theme}/anniu_10",
"speed4": f"textures/{theme}/anniu_12",
"zoomIn": f"textures/{theme}/anniu_17",
"zoomOut": f"textures/{theme}/anniu_19",
"audioOn": f"textures/{theme}/anniu_22",
"audioOff": f"textures/{theme}/anniu_21",
}
folder_map = {
"silu": "silu", "sanxing": "sanxing", "snow": "snow",
"chinese": "chinese", "numMan": "numMan", "redarmy": "redArmy",
}
for key, folder in folder_map.items():
if key not in themes:
continue
t = themes[key]
ship_f, ship_b = THEME_SHIP[folder]
t["background"] = f"textures/{folder}/bg"
t["borderDecorKey"] = "kuai11"
t["entities"] = {
"playerFront": f"textures/{folder}/skin/待机正面/1",
"playerBack": f"textures/{folder}/skin/待机背面/1",
"vehicleFront": f"textures/{folder}/{ship_f}",
"vehicleBack": f"textures/{folder}/{ship_b}",
"prop": f"textures/{folder}/Prop_kuai1",
"propGround": f"textures/{folder}/nProp_kuai1",
}
t["tiles"] = {
"Baseblock": f"textures/{folder}/Baseblock",
"JumpBlock": f"textures/{folder}/JumpBlock",
"WallBlock": f"textures/{folder}/WallBlock",
"borderDecor": f"textures/{folder}/kuai11",
}
existing_hud = t.get("hud", {})
t["hud"] = {**hud(folder), **{k: v for k, v in existing_hud.items() if k not in hud(folder)}}
db_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f" updated {db_path.relative_to(ROOT)}")
def patch_palettes() -> None:
idx = ROOT / "assets" / "resources" / "map-tiles" / "palettes" / "_index.json"
if idx.exists():
data = json.loads(idx.read_text(encoding="utf-8"))
for pal in data.get("palettes", []):
theme = pal.get("theme") or pal.get("id", "")
folder = {"redarmy": "redArmy"}.get(theme, theme)
for tile in pal.get("tiles", []):
tex = tile.get("texture", "")
if any(x in tex for x in ("素材", "Decor", "小游戏", "kuai11")):
tile["texture"] = f"textures/{folder}/kuai11"
idx.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
for name, folder in [("silu", "silu"), ("chinese", "chinese"), ("redarmy", "redArmy")]:
p = ROOT / "assets" / "resources" / "map-tiles" / "palettes" / f"{name}.json"
if not p.exists():
continue
pal = json.loads(p.read_text(encoding="utf-8"))
for tile in pal.get("tiles", []):
tex = tile.get("texture", "")
if any(x in tex for x in ("素材", "Decor", "小游戏", "kuai11")):
tile["texture"] = f"textures/{folder}/kuai11"
p.write_text(json.dumps(pal, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def main() -> None:
print("=== 恢复 __stage__ 贴图 ===")
n_rec = recover_stages_from_meta()
print(f" 恢复 {n_rec} 个文件")
print("=== 重命名贴图 ===")
n = apply_renames()
print(f" 完成 {n} 项重命名")
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("=== 更新配置与代码引用 ===")
reps = build_path_replacements()
n_files = patch_text_files(reps)
patch_themes_database()
patch_palettes()
print(f" 共更新 {n_files} 个文本文件")
print("完成。")
if __name__ == "__main__":
main()