납품 인박스 반영 도구 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