#!/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()