"""UE5 蓝图结构提取脚本。 对 index.json 中发现的蓝图资产提取: - 是否为 Actor 类型蓝图 - 组件层次结构(类型、名称、相对变换) - StaticMeshComponent 引用的模型路径 - 触发区域组件(BoxComponent / SphereComponent) - 非 Actor 类型资产列表(Curve、Sound、Struct 等) - AI 占位字段(interaction_type_tags, mechanism_tags, description) 输出: output/blueprints/.json,每个子目录一个文件 用法: 交互模式: exec(open(r"...\analyze_blueprints.py").read()) 命令行: 通过 run_all.py 调用 """ import sys from pathlib import Path _SCRIPT_DIR = Path(__file__).resolve().parent if str(_SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(_SCRIPT_DIR)) from _common import ( HAS_UNREAL, get_scan_root, get_output_dir, write_json, read_json, wait_for_editor, progress_log, iso_now, safe_subfolder_name, ) TAG = "[BlueprintAnalysis]" def _round3(v): """将浮点数或列表保留 3 位小数。""" if isinstance(v, (list, tuple)): return [round(x, 3) if isinstance(x, float) else x for x in v] return round(v, 3) if isinstance(v, float) else v # ============================================================ # 自动 Tag 生成 # ============================================================ import re _BP_NAME_RULES = [ (r"Door", ["door", "interactive"]), (r"Button|PushButton|FloorButton|Lever|Valve", ["button", "interactive"]), (r"Lamp|CeilingLight|RoadLamp|RoadLight|FlamingTorch|BlockingLights", ["lighting"]), (r"Decal|Zone|Circle|Octagon|Shape|Stripe|Arrow|Square", ["decal", "marker"]), (r"Procedural|BarrierWall|StairPlane|SpeedBump|Ladder", ["procedural"]), (r"Tooltip|DisplayPanel|CameraPreview", ["ui", "display"]), (r"Human|Scale", ["reference"]), (r"Spline", ["spline", "utility"]), ] def _generate_blueprint_tags(name: str) -> list: """根据蓝图名生成 tags。""" tags = [] for pattern, rule_tags in _BP_NAME_RULES: if re.search(pattern, name): for t in rule_tags: if t not in tags: tags.append(t) if not tags: tags = ["unclassified"] return tags # 已知的非 Actor 蓝图类型 NON_ACTOR_TYPES = { "CurveVector", "CurveLinearColor", "CurveFloat", "SoundCue", "SoundWave", "SoundClass", "UserDefinedStruct", "UserDefinedEnum", "NiagaraSystem", "NiagaraEmitter", "NiagaraModule", "Texture2D", "TextureCube", "DataTable", "StringTable", "AnimSequence", "AnimMontage", "BlendSpace", "WidgetBlueprint", } def _is_actor_blueprint(obj) -> bool: """判断加载的对象是否为 Actor 类型的蓝图。 纯 API 检测,多层回退: 1. 尝试 obj.generated_class() → 检查父类链 2. 尝试 obj.get_editor_property("generated_class") → 检查父类链 3. 尝试 obj.get_class() 的父类链(对 Blueprint 资产通常到不了 Actor) """ import unreal # 方法 1 & 2: 通过 generated_class 属性获取生成的类 gen_class = None for getter in [ lambda: obj.generated_class(), lambda: obj.get_editor_property("generated_class"), lambda: getattr(obj, "generated_class", None), ]: try: gen_class = getter() if gen_class: break except Exception: continue if gen_class: # 检查生成类的父类链是否到 Actor if _check_class_is_actor(gen_class): return True # 方法 3: 直接加载 _C 后缀的生成类 try: obj_path = obj.get_path_name() # /Game/.../B_AutoDoor.B_AutoDoor → /Game/.../B_AutoDoor.B_AutoDoor_C generated_path = obj_path.rsplit(".", 1) if len(generated_path) == 2: gen_path = generated_path[0] + "." + generated_path[1] + "_C" gen_asset = unreal.load_asset(gen_path) if gen_asset and _check_class_is_actor(gen_asset): return True except Exception: pass # 方法 4: 对 obj.get_class() 做父类链检查(通常失败但值得尝试) try: obj_class = obj.get_class() if _check_class_is_actor(obj_class): return True except Exception: pass return False def _check_class_is_actor(ue_class) -> bool: """检查 UE 类的父类链中是否包含 Actor。""" try: current = ue_class visited = set() while current: name = current.get_name() if name in visited: break visited.add(name) if name == "Actor": return True if name in ("Object", "Interface", "None", "Object"): break current = current.get_super_class() except Exception: pass return False def _extract_component_info(comp) -> dict: """从组件提取信息。""" import unreal comp_info = { "type": comp.get_class().get_name(), "name": "", "relative_location": None, "relative_rotation": None, "relative_scale": None, } try: comp_info["name"] = comp.get_name() except Exception: pass try: rel_loc = comp.get_editor_property("relative_location") if rel_loc: comp_info["relative_location"] = _round3([rel_loc.x, rel_loc.y, rel_loc.z]) except Exception: pass try: rel_rot = comp.get_editor_property("relative_rotation") if rel_rot: comp_info["relative_rotation"] = _round3([rel_rot.pitch, rel_rot.yaw, rel_rot.roll]) except Exception: pass try: rel_scale = comp.get_editor_property("relative_scale3d") or comp.get_editor_property("relative_scale") if rel_scale: comp_info["relative_scale"] = _round3([rel_scale.x, rel_scale.y, rel_scale.z]) except Exception: pass # StaticMeshComponent: 提取引用的模型 if "StaticMeshComponent" in comp_info["type"] or "StaticMesh" in comp_info["type"]: try: sm = comp.get_static_mesh() if sm: comp_info["static_mesh_path"] = sm.get_path_name() except Exception: pass # 尝试另一种方式 if "static_mesh_path" not in comp_info: try: sm = comp.get_editor_property("static_mesh") if sm: comp_info["static_mesh_path"] = sm.get_path_name() except Exception: pass # SkeletalMeshComponent: 提取引用的骨骼模型 if "SkeletalMeshComponent" in comp_info["type"] or "SkeletalMesh" in comp_info["type"]: try: skm = comp.get_editor_property("skeletal_mesh") if skm: comp_info["skeletal_mesh_path"] = skm.get_path_name() except Exception: pass return comp_info def _analyze_single_blueprint(asset_info: dict) -> dict: """分析单个蓝图资产。""" import unreal ue_path = asset_info["ue_path"] name = asset_info["name"] ue_class_type = asset_info.get("ue_class_type", "Unknown") result = { "ue_path": ue_path, "ue_class_type": ue_class_type, "is_actor_blueprint": False, "components": [], "analysis_notes": [], # AI Pass 2 占位 "interaction_type_tags": [], "mechanism_tags": [], "description": "", } try: obj = unreal.load_asset(ue_path) except Exception as e: result["analysis_notes"].append(f"加载失败: {e}") return result if obj is None: result["analysis_notes"].append("加载返回 None") result["style_tags"] = _generate_blueprint_tags(name) return result # 检查是否为已知非 Actor 类型 actual_class = type(obj).__name__ if actual_class in NON_ACTOR_TYPES: result["analysis_notes"].append(f"非 Actor 类型: {actual_class}") result["style_tags"] = _generate_blueprint_tags(name) return result # 尝试判断是否为 Actor 蓝图 is_actor = _is_actor_blueprint(obj) result["is_actor_blueprint"] = is_actor if not is_actor: result["analysis_notes"].append(f"非 Actor 蓝图: {actual_class}") result["style_tags"] = _generate_blueprint_tags(name) return result # ---- 提取组件 ---- try: # 优先通过 generated_class 获取 CDO bp_class = None for getter in [ lambda: obj.generated_class(), lambda: obj.get_editor_property("generated_class"), lambda: obj.get_class(), ]: try: bp_class = getter() if bp_class: break except Exception: continue if bp_class: cdo = bp_class.get_default_object() if cdo: components = _extract_all_components(cdo) result["components"] = components result["component_count"] = len(components) else: result["analysis_notes"].append("CDO 获取返回 None") else: result["analysis_notes"].append("无法获取 Blueprint 类") except Exception as e: result["analysis_notes"].append(f"组件提取失败: {e}") # ---- 自动 Tags ---- result["style_tags"] = _generate_blueprint_tags(name) # ---- 基于 Tags 推断 interaction_type_tags ---- interaction_tags = [] if result["components"]: comp_types = [c.get("type", "") for c in result["components"]] comp_names = [c.get("name", "") for c in result["components"]] has_trigger = any("Box" in t or "Sphere" in t or "Capsule" in t or "Trigger" in n for t, n in zip(comp_types, comp_names)) has_timeline = any("Timeline" in n for n in comp_names) has_smc = any("StaticMesh" in t for t in comp_types) if has_trigger and "door" in str(result["style_tags"]): interaction_tags.extend(["proximity_trigger", "auto_door"]) elif has_trigger and "button" in str(result["style_tags"]): interaction_tags.extend(["press_interact", "button_trigger"]) elif has_trigger: interaction_tags.append("proximity_trigger") if has_timeline: interaction_tags.append("animated") if has_smc and not has_trigger and "interactive" not in str(result["style_tags"]): interaction_tags.append("static_prop") result["interaction_type_tags"] = interaction_tags return result def _extract_all_components(cdo) -> list: """从 CDO 提取所有组件信息。""" import unreal components = [] try: # 方法 1: get_all_components (某些版本可用) all_comps = cdo.get_all_components() if all_comps: for comp in all_comps: comp_info = _extract_component_info(comp) components.append(comp_info) return components except Exception: pass try: # 方法 2: 通过 BlueprintCreatedComponents 或实例化组件 # 尝试获取组件列表 comp_prop = cdo.get_editor_property("blueprint_created_components") if comp_prop: for item in comp_prop: try: comp = item.get_editor_property("component") if comp: comp_info = _extract_component_info(comp) components.append(comp_info) except Exception: pass except Exception: pass try: # 方法 3: 通过 InstanceComponents inst_comps = cdo.get_editor_property("instance_components") if inst_comps: for comp in inst_comps: comp_info = _extract_component_info(comp) components.append(comp_info) except Exception: pass # 去重(某些方法可能重复获取) seen = set() unique = [] for c in components: key = (c.get("type", ""), c.get("name", "")) if key not in seen: seen.add(key) unique.append(c) return unique # ============================================================ # 主流程 # ============================================================ def run(index_data: dict = None) -> dict: """执行蓝图分析。""" if not HAS_UNREAL: print(f"{TAG} 错误: unreal 模块不可用") return {"status": "error", "reason": "no unreal"} output_dir = get_output_dir() # 加载 index.json if index_data is None: index_path = output_dir / "index.json" if not index_path.exists(): print(f"{TAG} 错误: index.json 不存在,请先运行 discover") return {"status": "error", "reason": "no index.json"} index_data = read_json(index_path) blueprints_by_subfolder = index_data.get("blueprints", {}) if not blueprints_by_subfolder: print(f"{TAG} 无蓝图资产需要分析") return {"status": "ok", "analyzed": 0} # 创建输出目录 blueprints_dir = output_dir / "blueprints" blueprints_dir.mkdir(parents=True, exist_ok=True) total_all = sum(len(assets) for assets in blueprints_by_subfolder.values()) print(f"{TAG} 共 {total_all} 个蓝图资产,分布在 {len(blueprints_by_subfolder)} 个子目录") analyzed = 0 skipped_non_actor = 0 for subfolder, assets in blueprints_by_subfolder.items(): safe_name = safe_subfolder_name(subfolder) output_file = blueprints_dir / f"{safe_name}.json" # 断点续跑 if output_file.exists(): try: existing = read_json(output_file) if existing.get("completed"): print(f"{TAG} 跳过已完成的: {safe_name}") analyzed += len(existing.get("actor_blueprints", {})) skipped_non_actor += len(existing.get("skipped_non_actor", [])) continue except Exception: pass print(f"{TAG} 分析子目录: {subfolder} ({len(assets)} 个资产)") actor_blueprints = {} non_actor = [] sub_analyzed = 0 for i, asset_info in enumerate(assets): progress_log(i, len(assets), f"{asset_info['name']}") result = _analyze_single_blueprint(asset_info) if result.get("is_actor_blueprint"): actor_blueprints[asset_info["name"]] = result sub_analyzed += 1 else: non_actor.append({ "name": asset_info["name"], "ue_class_type": result.get("ue_class_type", asset_info.get("ue_class_type", "Unknown")), "note": result.get("analysis_notes", []), }) # 写入分组文件 group_data = { "subfolder": subfolder, "source_path": f"{get_scan_root()}/Blueprints/{subfolder}" if subfolder else f"{get_scan_root()}/Blueprints", "actor_blueprint_count": len(actor_blueprints), "non_actor_count": len(non_actor), "completed": True, "actor_blueprints": actor_blueprints, "skipped_non_actor": non_actor, } write_json(group_data, output_file) analyzed += sub_analyzed skipped_non_actor += len(non_actor) print(f"{TAG} ✅ {safe_name}: {sub_analyzed} Actor 蓝图, {len(non_actor)} 非 Actor → {output_file}") print(f"{TAG} 全部完成: {analyzed} Actor 蓝图, {skipped_non_actor} 非 Actor / {total_all} 总计") return { "status": "ok", "total": total_all, "actor_blueprints": analyzed, "skipped_non_actor": skipped_non_actor, } # ---- 入口 ---- if __name__ == "__main__": if not wait_for_editor(): sys.exit(1) result = run() if result.get("status") != "ok": sys.exit(1)