From 46f65e942fd81850e9ce4de33598d2547a594d41 Mon Sep 17 00:00:00 2001 From: swrring Date: Thu, 5 Nov 2026 01:39:51 +0900 Subject: [PATCH] =?UTF-8?q?=EB=82=A9=ED=92=88=20=EC=9D=B8=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=20=EB=B0=98=EC=98=81=20=EB=8F=84=EA=B5=AC=20Tools/app?= =?UTF-8?q?ly=5Fdelivery.py=20=E2=80=94=20=EC=A0=9C=EC=9E=91=20=EC=84=B8?= =?UTF-8?q?=EC=85=98=EC=9D=B4=20=EB=AC=B6=EC=9D=8C=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EC=8B=9C=20deliveries/.json=201=EC=9E=A5=EB=A7=8C=20?= =?UTF-8?q?=EC=93=B0=EA=B3=A0,=20=EB=B0=98=EC=98=81=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=EC=9D=80=20list/apply=20--all=20=EB=A1=9C=20=EB=B0=B0=EA=B2=BD?= =?UTF-8?q?=20=EB=B3=B5=EC=82=AC=C2=B7md5=20=EA=B2=80=EC=A6=9D=C2=B7posese?= =?UTF-8?q?t=20=EA=B5=90=EC=B2=B4(PD=202026-11-05=20=E3=80=8C=EC=99=84?= =?UTF-8?q?=EC=A0=84=ED=9E=88=20=EC=99=84=EB=A3=8C=EB=90=9C=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EB=A6=AC=EC=86=8C=EC=8A=A4=EB=A7=8C=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=E3=80=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- Tools/apply_delivery.py | 148 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 Tools/apply_delivery.py diff --git a/Tools/apply_delivery.py b/Tools/apply_delivery.py new file mode 100644 index 00000000..93d0a4a6 --- /dev/null +++ b/Tools/apply_delivery.py @@ -0,0 +1,148 @@ +"""납품 인박스 → 게임 반영 (파일 작업만 · 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)