납품 인박스 반영 도구 Tools/apply_delivery.py — 제작 세션이 묶음 완료 시 deliveries/<batch>.json 1장만 쓰고, 반영 세션은 list/apply --all 로 배경 복사·md5 검증·poseset 교체(PD 2026-11-05 「완전히 완료된 경우 리소스만 전달」)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b9e9f22914
commit
46f65e942f
|
|
@ -0,0 +1,148 @@
|
|||
"""납품 인박스 → 게임 반영 (파일 작업만 · Unity 쪽은 끝에 출력하는 명령으로).
|
||||
|
||||
PD 2026-11-05: 「매번 수신하지 않아도, 요청한 작업이 완전히 완료된 경우 해당 리소스만 전달받는 체계」.
|
||||
제작 세션은 묶음이 **완전히** 끝났을 때만 아래 json 한 장을 인박스에 쓴다(중간 보고 없음 · 채팅 메시지 없음).
|
||||
|
||||
인박스 = C:\\ArtRnD\\samguk_game\\deliveries\\<YYYYMMDD_HHMM>_<batch>.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", # <root>/<art>/<clip>.png
|
||||
"arts": ["dz_infantry", "dz_spear", …],
|
||||
"clips": ["front_idle", "front_ready", "front_action"], # back_* 가 있으면 함께
|
||||
"md5_list": "C:/ArtRnD/samguk_game/poses/md5.txt" # 선택 · <root> 기준 상대 경로 · md5sum 형식
|
||||
}
|
||||
|
||||
실행: <ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py list
|
||||
<ComfyUI venv python> -I -B -X utf8 Tools/apply_delivery.py apply <json 경로> (반영 뒤 .applied.json 으로 이름 바꿈)
|
||||
<ComfyUI venv python> -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 형식(<md5> <상대경로>) 목록 검증 → (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 <noreply@anthropic.com>)")
|
||||
|
||||
|
||||
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)
|
||||
Loading…
Reference in New Issue