Files
AIGame/Tools/install_character_bundles.py
T

197 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""将角色 XPbr / 烘焙产物安装到 Bundles/Characters。
人物:Characters/Human/{ActorName}/
不传 --kind 时落到 Characters/{ActorName}/(兼容 MaleExplorer)。
优先识别 XPbr 产物(推荐):
{ActorName}.fbx
{ActorName}_base.png # RGB albedo, A=AO/透明
{ActorName}_mix.png # RG sphere-normal, B=metallic, A=roughness
{ActorName}.mat # Test/XPbr
{ActorName}.prefab # Pipeline SetupCharacter 生成
概念图留在 Doc/Design/Character/,不要拷进 Bundles。
兼容旧五通道烘焙目录(_color/_normal/_ao/_metallic/_roughness)。
用法:
python Tools/install_character_bundles.py -a CaoCao --kind Human -b Tools/output/caocao_game/caocao_xpbr -c Doc/Design/Character/CaoCao/caocao.png
"""
from __future__ import annotations
import argparse
import json
import shutil
import sys
from pathlib import Path
def _repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def _pick_first(files: list[Path], predicate) -> Path | None:
for path in files:
if path.is_file() and predicate(path.name.lower()):
return path
return None
def _safe_seg(name: str, label: str) -> str:
text = (name or "").strip()
if not text or any(ch in text for ch in r'\/:*?"<>|'):
raise ValueError(f"非法 {label}: {name!r}")
return text
def install(
*,
actor: str,
bake_dir: Path,
kind: str | None = None,
concept: Path | None = None,
dry_run: bool = False,
) -> dict:
actor = _safe_seg(actor, "ActorName")
kind = _safe_seg(kind, "Kind") if kind else None
bake_dir = bake_dir.expanduser().resolve()
if not bake_dir.is_dir():
raise FileNotFoundError(f"资源目录不存在: {bake_dir}")
dest = _repo_root() / "AIClient" / "Assets" / "Bundles" / "Characters"
if kind:
dest = dest / kind
dest = dest / actor
files = list(bake_dir.iterdir())
sources: dict[str, Path] = {}
fbx = _pick_first(files, lambda n: n.endswith(".fbx"))
if fbx:
sources["fbx"] = fbx
# XPbr 优先
base = _pick_first(files, lambda n: n.endswith("_base.png") or n.endswith("_basemap.png"))
mix = _pick_first(files, lambda n: n.endswith("_mix.png") or n.endswith("_mixmap.png"))
mat = _pick_first(files, lambda n: n.endswith(".mat"))
if base:
sources["base"] = base
if mix:
sources["mix"] = mix
if mat:
sources["mat"] = mat
# 旧五通道兜底
if "base" not in sources:
color = _pick_first(files, lambda n: "_color" in n and n.endswith(".png"))
if color:
sources["color"] = color
for key, token in (
("normal", "_normal"),
("ao", "_ao"),
("metallic", "_metallic"),
("roughness", "_roughness"),
):
hit = _pick_first(files, lambda n, t=token: t in n and n.endswith(".png"))
if hit:
sources[key] = hit
if "fbx" not in sources:
raise FileNotFoundError(f"目录缺少 FBX: {bake_dir}")
if "base" not in sources and "color" not in sources:
raise FileNotFoundError(
f"目录缺少 XPbr _base.png 或旧版 _color.png: {bake_dir}"
)
name_map = {
"fbx": f"{actor}.fbx",
"base": f"{actor}_base.png",
"mix": f"{actor}_mix.png",
"mat": f"{actor}.mat",
"color": f"{actor}_color.png",
"normal": f"{actor}_normal.png",
"ao": f"{actor}_ao.png",
"metallic": f"{actor}_metallic.png",
"roughness": f"{actor}_roughness.png",
}
copied: list[str] = []
if not dry_run:
dest.mkdir(parents=True, exist_ok=True)
for key, src in sources.items():
target = dest / name_map[key]
if not dry_run:
shutil.copy2(src, target)
# 同步 .meta(GUID 复用,避免破坏引用)
meta = Path(str(src) + ".meta")
if meta.is_file():
shutil.copy2(meta, Path(str(target) + ".meta"))
copied.append(str(target.relative_to(_repo_root())).replace("\\", "/"))
if concept is not None:
concept = concept.expanduser().resolve()
if not concept.is_file():
raise FileNotFoundError(f"概念图不存在: {concept}")
mode = "xpbr" if "base" in sources else "legacy_pbr"
result = {
"ok": True,
"actor": actor,
"kind": kind,
"mode": mode,
"dest": str(dest.relative_to(_repo_root())).replace("\\", "/"),
"files": copied,
"hint": (
"Unity Pipeline: unity command run_script --file AgentScripts/SetupCharacter.cs "
f"--entry SetupCharacter.Run --args {json.dumps([kind or '', actor])}"
";并登记 Doc/ResDesc_Model.md"
),
}
if concept is not None:
result["design"] = str(concept.relative_to(_repo_root())).replace("\\", "/")
return result
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description="安装角色资源到 Bundles/Characters/{Actor}")
p.add_argument("--actor", "-a", required=True, help="角色目录名 / 寻址名,如 CaoCao")
p.add_argument(
"--kind",
"-k",
default=None,
help="人物用 Human,落到 Characters/{Kind}/{Actor};省略则为 Characters/{Actor}",
)
p.add_argument(
"--bake-dir",
"-b",
required=True,
help="model_lowpoly_bake 的 *_xpbr 目录,或旧五通道 bake 子目录",
)
p.add_argument(
"--concept",
"-c",
default=None,
help="设计原稿路径(仅校验存在,不拷进 Bundles;应放在 Doc/Design/)",
)
p.add_argument("--dry-run", action="store_true")
args = p.parse_args(argv)
try:
result = install(
actor=args.actor,
bake_dir=Path(args.bake_dir),
kind=args.kind,
concept=Path(args.concept) if args.concept else None,
dry_run=args.dry_run,
)
except Exception as exc: # noqa: BLE001
print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False), flush=True)
return 1
print(json.dumps(result, ensure_ascii=False, indent=2), flush=True)
return 0
if __name__ == "__main__":
sys.exit(main())