"""납품 인박스 → 게임 반영 (파일 작업만 · Unity 쪽은 끝에 출력하는 명령으로). PD 2026-11-05: 「매번 수신하지 않아도, 요청한 작업이 완전히 완료된 경우 해당 리소스만 전달받는 체계」. 제작 세션은 묶음이 **완전히** 끝났을 때만 아래 json 한 장을 인박스에 쓴다(중간 보고 없음 · 채팅 메시지 없음). 인박스 = C:\\ArtRnD\\samguk_game\\deliveries\\_.json { "batch": "bg" | "hgj" | "heroes_back" | "dz" | "cc" | "wei" | …, "kind": "bg" | "poses", "qa": "PASS", "session": "<만든 세션 이름>", # kind == "bg" "file": "C:/ArtRnD/samguk_game/bg/battlefield_01.png", "md5": "<파일 md5>", # kind == "poses" "root": "C:/ArtRnD/samguk_game/poses", # //.png "arts": ["dz_infantry", "dz_spear", …], "clips": ["front_idle", "front_ready", "front_action"], # back_* 가 있으면 함께 "md5_list": "C:/ArtRnD/samguk_game/poses/md5.txt" # 선택 · 기준 상대 경로 · md5sum 형식 } 실행: -I -B -X utf8 Tools/apply_delivery.py list -I -B -X utf8 Tools/apply_delivery.py apply (반영 뒤 .applied.json 으로 이름 바꿈) -I -B -X utf8 Tools/apply_delivery.py apply --all (미반영 전부) 반영 뒤 Unity: 메뉴 재임포트 → 비동기 테스트 → 커밋 (끝에 출력). """ import argparse import hashlib import json import os import shutil import subprocess import sys PROJECT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) INBOX = r"C:\ArtRnD\samguk_game\deliveries" EXPORTER = r"C:\Ai\NerdNavisAi\.claude\skills\skeletal-anim-pipeline\files\scripts\rig_frames_export.py" UNITS = os.path.join(PROJECT, "Assets", "Resources", "Units") BOARD = os.path.join(PROJECT, "Assets", "Resources", "Board", "battlefield_01.png") def md5_of(path): h = hashlib.md5() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def pending(): os.makedirs(INBOX, exist_ok=True) return sorted(f for f in os.listdir(INBOX) if f.endswith(".json") and not f.endswith(".applied.json")) def check_md5_list(root, md5_list): """md5sum 형식( <상대경로>) 목록 검증 → (ok, bad).""" ok, bad = 0, [] for line in open(md5_list, encoding="utf-8"): line = line.strip() if not line: continue digest, rel = line.split(None, 1) rel = rel.lstrip("*").replace("/", os.sep) p = os.path.join(root, rel) if not os.path.exists(p) or md5_of(p) != digest.lower(): bad.append(rel) else: ok += 1 return ok, bad def apply_bg(d): src = d["file"] if d.get("md5") and md5_of(src) != d["md5"].lower(): sys.exit("배경 md5 불일치: " + src) os.makedirs(os.path.dirname(BOARD), exist_ok=True) shutil.copyfile(src, BOARD) print("bg ->", BOARD, "md5", md5_of(BOARD)) def apply_poses(d): root = d["root"] if d.get("md5_list"): ok, bad = check_md5_list(root, d["md5_list"]) if bad: sys.exit("md5 불일치 %d: %s" % (len(bad), ", ".join(bad[:5]))) print("md5 OK", ok) clips = d.get("clips") or ["front_idle", "front_ready", "front_action"] for art in d["arts"]: src = os.path.join(root, art) have = [c for c in clips if os.path.exists(os.path.join(src, c + ".png"))] if not have: sys.exit("포즈 없음: " + src) out = os.path.join(UNITS, art) if os.path.isdir(out): shutil.rmtree(out) # 구판(리그 프레임·정지 그림) 제거 — git 에 있음 r = subprocess.run([sys.executable, "-I", "-B", "-X", "utf8", EXPORTER, "poseset", src, "--out", out, "--clips", ",".join(have)], capture_output=True, text=True, encoding="utf-8") if r.returncode != 0: sys.exit("poseset 실패 " + art + "\n" + r.stdout + r.stderr) print(r.stdout.strip().splitlines()[-1]) def cmd_list(a): files = pending() print("미반영 납품:", len(files)) for f in files: d = json.load(open(os.path.join(INBOX, f), encoding="utf-8")) print(" ", f, "|", d.get("kind"), d.get("batch"), "|", d.get("qa"), "|", d.get("session", "")) def cmd_apply(a): targets = pending() if a.all else [os.path.basename(a.json)] if not targets: print("반영할 납품 없음") return for f in targets: p = os.path.join(INBOX, f) d = json.load(open(p, encoding="utf-8")) if d.get("qa", "PASS") != "PASS": print("건너뜀(QA 미통과):", f) continue print("==", f, d.get("kind"), d.get("batch")) if d["kind"] == "bg": apply_bg(d) elif d["kind"] == "poses": apply_poses(d) else: sys.exit("모르는 kind: " + str(d.get("kind"))) os.replace(p, p[:-5] + ".applied.json") print("\n다음(Unity · PD 에디터 Play 중이면 먼저 editor.playmode stop):") print(" unity command --project-path . editor invoke_menu --path 'TKS/Reimport Unit Art (유닛 아트 재임포트)' --result-only") print(" unity command --project-path . test run --mode playmode --async-tests --timeout 600 --result-only ; unity command --project-path . test status --result-only") print(" git add -A && git commit (끝줄 Co-Authored-By: Claude Fable 5.1 )") if __name__ == "__main__": p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sp = p.add_subparsers(dest="cmd", required=True) l = sp.add_parser("list") l.set_defaults(fn=cmd_list) ap = sp.add_parser("apply") ap.add_argument("json", nargs="?", default="") ap.add_argument("--all", action="store_true") ap.set_defaults(fn=cmd_apply) a = p.parse_args() if a.cmd == "apply" and not a.all and not a.json: sys.exit("json 경로 또는 --all") a.fn(a)