"""Generate the selected battlefield's ground and reusable prop design sheets. The selected overview must already be generated by the project ``gpt-image-2.5`` flow. Each request uses the main Candy home illustration as a style reference and the selected overview as a layout reference. """ from __future__ import annotations import argparse import json import subprocess import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] MANIFEST = ROOT / "Doc/Design/Scene/Battlefield/battlefield_scene_manifest.json" PROMPTS_DIR = ROOT / "Doc/Design/Scene/Battlefield" IMAGE_TOOL = ROOT / "Tools/gpt_image_generate.py" ASSET_ROOT = ROOT / "AIClient/Assets/Bundles" def load(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8-sig")) def save(path: Path, data: dict) -> None: path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def generated_overview(manifest: dict, design_id: str) -> bool: option = next((x for x in manifest.get("designOptions", []) if x.get("id") == design_id), None) generation = (option or {}).get("generation", {}) return generation.get("model") == "gpt-image-2.5" and generation.get("status") == "generated" def find_existing_prefab(component_type: str) -> str | None: """Return an authored prefab for a reusable component, if one exists. The search is intentionally limited to the game asset root. Intermediate GLB/FBX files in Tools/output are not considered reusable Unity components. """ if not component_type: return None candidates = sorted(ASSET_ROOT.rglob(f"{component_type}.prefab")) if not candidates: return None preferred = [p for p in candidates if "Scenes" in p.parts and component_type in p.parts] path = (preferred[0] if preferred else candidates[0]).resolve() return str(path.relative_to(ROOT)).replace("\\", "/") def ensure_component_entry(manifest: dict, item: dict) -> dict: component_type = item.get("type") allowed = manifest.setdefault("allowedComponentTypes", []) if component_type and component_type not in allowed: allowed.append(component_type) component = next((x for x in manifest.setdefault("components", []) if x.get("type") == item.get("type")), None) if component is None: component = { "type": item.get("type"), "label": item.get("type"), "targetTriangles": item.get("targetTriangles", 700), "generationStatus": "pending", } manifest["components"].append(component) for key in ("position", "rotation", "scale", "placements"): if key in item: component[key] = item[key] return component def main() -> int: parser = argparse.ArgumentParser(description="Generate ground and prop design sheets for a battlefield design.") parser.add_argument("--design", default="A", help="方案 ID;对应 Doc/Design/Scene/Battlefield//battlefield_component_prompts.json") parser.add_argument("--dry-run", action="store_true") parser.add_argument( "--force-new-components", action="store_true", help="即使 Assets/Bundles 中已有同名组件 Prefab,也重新生成该组件设计图", ) parser.add_argument( "--use-references", action="store_true", help="上传主界面与总览图作为参考;默认关闭,避免外传项目内部图片", ) parser.add_argument("--quality", default="auto", choices=["auto", "low", "medium", "high"]) args = parser.parse_args() manifest = load(MANIFEST) if not generated_overview(manifest, args.design) and not args.dry_run: raise SystemExit( f"方案 {args.design} 的总览图尚未由 gpt-image-2.5 生成;" f"请先运行 python Tools/generate_battlefield_designs.py --design {args.design}" ) if not generated_overview(manifest, args.design): print("warning: dry-run uses the current overview path; actual generation requires the AI-generated overview") prompt_path = PROMPTS_DIR / args.design / "battlefield_component_prompts.json" if not prompt_path.is_file(): raise SystemExit(f"缺少方案 {args.design} 的组件拆分提示词:{prompt_path}") prompt_data = load(prompt_path) style = ROOT / prompt_data["styleReference"] overview = ROOT / prompt_data["overviewReference"] output_root = PROMPTS_DIR / args.design jobs = [prompt_data["ground"], *prompt_data["components"]] # A manifest describes one selected level at a time. Remove component # entries from a previous design before adding this design's split list; # shared types are retained so their authored Prefab can be reused. job_types = {item.get("type") for item in prompt_data.get("components", [])} manifest["components"] = [ component for component in manifest.get("components", []) if component.get("type") in job_types ] reused = [] generated = [] for item in jobs: component_type = item.get("type") existing_prefab = ( find_existing_prefab(component_type) if component_type and item.get("reuseExisting", True) is not False else None ) if component_type and existing_prefab and not args.force_new_components: component = ensure_component_entry(manifest, item) component.update( { "generationStatus": "reused", "reuseExisting": True, "existingPrefab": existing_prefab, "prefab": component.get("prefab") or existing_prefab, "designImage": component.get("designImage", ""), } ) reused.append(component_type) print(f"[{component_type}] reuse {existing_prefab}; skip new design image") continue output = output_root / item["file"] output.parent.mkdir(parents=True, exist_ok=True) callback = output.with_name(output.stem + "_gpt_image_status.json") command = [ sys.executable, str(IMAGE_TOOL), "--prompt", item["prompt"], "--model", "gpt-image-2.5", "--aspect-ratio", "1:1", "--quality", args.quality, "--output", str(output), "--callback-file", str(callback), "--result-only", ] if args.use_references: command[command.index("--model"):command.index("--model")] = [ "--image", str(style), "--image", str(overview), ] print(f"[{item.get('type', 'ground')}] {output}") print(" ".join(json.dumps(part, ensure_ascii=False) for part in command)) if args.dry_run: continue try: subprocess.run(command, cwd=ROOT, check=True) except subprocess.CalledProcessError as exc: print( "component image generation failed; configure GRSAI_API_KEY or " "GPT_IMAGE_API_KEY and rerun this command", file=sys.stderr, ) return exc.returncode or 1 rel_output = str(output.relative_to(ROOT)).replace("\\", "/") rel_status = str(callback.relative_to(ROOT)).replace("\\", "/") if "type" not in item: manifest["ground"].update( { "designImage": rel_output, "designGenerationStatus": "generated", "designStatusFile": rel_status, } ) else: component = ensure_component_entry(manifest, item) component.update( { "designImage": rel_output, "generationStatus": "generated", "statusFile": rel_status, "reuseExisting": False, "existingPrefab": "", "prefab": "", "modelGenerationStatus": "pending", } ) generated.append(component_type) if not args.dry_run: manifest["componentDesignGeneration"] = { "tool": "Tools/gpt_image_generate.py", "model": "gpt-image-2.5", "styleReference": prompt_data["styleReference"] if args.use_references else "prompt-described", "overviewReference": prompt_data["overviewReference"] if args.use_references else "prompt-described", "referenceUpload": "enabled" if args.use_references else "none", "status": "generated" if generated else "reused", "generatedAtUtc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "generatedComponents": generated, "reusedComponents": reused, } save(MANIFEST, manifest) return 0 if __name__ == "__main__": raise SystemExit(main())