diff --git a/Content/Python/handlers/blueprint_commands.py b/Content/Python/handlers/blueprint_commands.py index 67e1954..aa41511 100644 --- a/Content/Python/handlers/blueprint_commands.py +++ b/Content/Python/handlers/blueprint_commands.py @@ -525,7 +525,6 @@ def handle_get_all_nodes(command: Dict[str, Any]) -> Dict[str, Any]: nodes_json = node_creator.get_all_nodes_in_graph(blueprint_path, function_id) if nodes_json: - # Parse the JSON response try: nodes = json.loads(nodes_json) log.log_result("get_all_nodes", True, f"Retrieved {len(nodes)} nodes from {blueprint_path}") @@ -624,4 +623,409 @@ def handle_get_node_guid(command: Dict[str, Any]) -> Dict[str, Any]: except Exception as e: log.log_error(f"Error getting node GUID: {str(e)}", include_traceback=True) + return {"success": False, "error": str(e)} + + +def _extract_var_desc(var_desc) -> Dict[str, Any]: + """Extract variable info from a FBPVariableDescription or similar struct.""" + var_info = {} + # Try all known property names + for name_prop in ["var_name", "name", "variable_name"]: + try: + var_info["name"] = str(var_desc.get_editor_property(name_prop)) + break + except Exception: + continue + if "name" not in var_info: + var_info["name"] = str(var_desc) if var_desc else "unknown" + for guid_prop in ["var_guid", "guid", "variable_guid"]: + try: + var_info["guid"] = str(var_desc.get_editor_property(guid_prop)) + break + except Exception: + continue + for type_prop in ["var_type", "type", "variable_type"]: + try: + pin_type = var_desc.get_editor_property(type_prop) + if pin_type: + try: + var_info["type"] = str(pin_type.get_editor_property("pin_category")) + except Exception: + var_info["type"] = str(pin_type) + break + except Exception: + continue + for cat_prop in ["category"]: + try: + var_info["category"] = str(var_desc.get_editor_property(cat_prop)) + break + except Exception: + continue + for def_prop in ["default_value"]: + try: + var_info["default_value"] = str(var_desc.get_editor_property(def_prop)) + break + except Exception: + continue + return var_info + + +def handle_get_blueprint_summary(command: Dict[str, Any]) -> Dict[str, Any]: + """ + Handle a command to get a high-level Blueprint summary for LLM context. + Uses direct Blueprint object properties and existing C++ utilities. + """ + try: + blueprint_path = command.get("blueprint_path") + include_graphs = command.get("include_graphs", True) + include_variables = command.get("include_variables", True) + include_components = command.get("include_components", False) + + if not blueprint_path: + log.log_error("Missing blueprint_path for get_blueprint_summary") + return {"success": False, "error": "Missing blueprint_path"} + + log.log_command("get_blueprint_summary", f"Blueprint: {blueprint_path}") + + # Load the Blueprint asset - use load_asset which works across UE versions + asset = unreal.load_asset(blueprint_path) + if not asset: + log.log_error(f"Failed to load Blueprint asset: {blueprint_path}") + return {"success": False, "error": f"Failed to load Blueprint asset: {blueprint_path}"} + + summary = { + "blueprint_path": blueprint_path, + "asset_name": asset.get_name() if hasattr(asset, "get_name") else blueprint_path.split("/")[-1], + "parent_class": "", + "generated_class": "", + "graphs": [], + "variables": [], + "components": [], + "warnings": [] + } + + # --- Parent class --- + try: + parent = asset.get_editor_property("parent_class") if hasattr(asset, "get_editor_property") else None + if parent: + summary["parent_class"] = parent.get_name() + except Exception as e: + summary["warnings"].append(f"Failed to read parent_class: {str(e)}") + + # --- Generated class --- + gen_class = None + try: + gen_class = asset.generated_class() if callable(getattr(asset, "generated_class", None)) else getattr(asset, "generated_class", None) + if gen_class: + summary["generated_class"] = gen_class.get_name() + except Exception as e: + summary["warnings"].append(f"Failed to read generated_class: {str(e)}") + + # --- Graphs --- + if include_graphs: + gen_bp_utils = unreal.GenBlueprintUtils + node_creator = unreal.GenBlueprintNodeCreator + + # Use BlueprintEditorLibrary.find_event_graph (confirmed working in UE 5.7) + try: + eg = unreal.BlueprintEditorLibrary.find_event_graph(asset) + if eg: + eg_guid = "" + try: + eg_guid = gen_bp_utils.get_node_guid(blueprint_path, "EventGraph", "ReceiveBeginPlay", "") + except Exception: + pass + # Get node count via C++ utility + node_count = 0 + try: + eg_nodes_json = node_creator.get_all_nodes_in_graph(blueprint_path, "EventGraph") + if eg_nodes_json: + node_count = len(json.loads(eg_nodes_json)) + except Exception: + pass + summary["graphs"].append({ + "name": "EventGraph", + "type": "UbergraphPage", + "function_id": "EventGraph", + "entry_node_guid": eg_guid, + "node_count": node_count + }) + except Exception as e: + summary["warnings"].append(f"Failed to query EventGraph: {str(e)}") + + # Enumerate user-defined function graphs via BlueprintEditorLibrary.find_graph + if gen_class: + try: + # Get CDO to find user-defined functions + class_name = gen_class.get_name() + cdo_path = f"{blueprint_path}.Default__{class_name}" + cdo = unreal.find_object(None, cdo_path) + if cdo: + # Build parent property set + parent_funcs = set() + for base_path in [ + "/Script/Engine.Default__Character", + "/Script/Engine.Default__Pawn", + "/Script/Engine.Default__Actor" + ]: + try: + base_cdo = unreal.find_object(None, base_path) + if base_cdo: + parent_funcs = parent_funcs.union( + set(p for p in dir(base_cdo) if not p.startswith('_')) + ) + except Exception: + continue + + user_attrs = sorted(set(p for p in dir(cdo) if not p.startswith('_')) - parent_funcs) + for attr_name in user_attrs: + # Try to find this as a function graph + try: + graph = unreal.BlueprintEditorLibrary.find_graph(asset, attr_name) + if graph: + func_guid = "" + try: + func_guid = gen_bp_utils.get_node_guid(blueprint_path, "FunctionGraph", "", attr_name) + except Exception: + pass + node_count = 0 + try: + fn_json = node_creator.get_all_nodes_in_graph(blueprint_path, func_guid or attr_name) + if fn_json: + node_count = len(json.loads(fn_json)) + except Exception: + pass + summary["graphs"].append({ + "name": attr_name, + "type": "FunctionGraph", + "function_id": func_guid or attr_name, + "node_count": node_count + }) + except Exception: + continue + except Exception as e: + summary["warnings"].append(f"Failed to enumerate function graphs: {str(e)}") + + # --- Variables --- + if include_variables: + found_vars = False + + # (debug logging removed) + + # Method 1: Try various property names for blueprint variables + var_property_names = ["new_variables", "variables", "blueprint_variables"] + for prop_name in var_property_names: + try: + new_vars = asset.get_editor_property(prop_name) + if new_vars is not None and len(new_vars) > 0: + for var_desc in new_vars: + var_info = _extract_var_desc(var_desc) + summary["variables"].append(var_info) + found_vars = True + break + except Exception: + continue + + # Method 2: Load CDO via find_object, also try EditorAssetLibrary.load_blueprint_class + if not found_vars and gen_class: + cdo = None + + # Approach A: find CDO by constructed path + try: + class_name = gen_class.get_name() + cdo_obj_path = f"{blueprint_path}.Default__{class_name}" + cdo = unreal.find_object(None, cdo_obj_path) + except Exception: + pass + + # Approach B: load_blueprint_class → get_default_object + if not cdo: + try: + bp_class = unreal.EditorAssetLibrary.load_blueprint_class(blueprint_path) + if bp_class: + cdo = bp_class.get_default_object() + except Exception: + pass + + if cdo: + cdo_props = set(p for p in dir(cdo) if not p.startswith('_')) + + # Build parent property set to filter inherited props + parent_props = set() + + # Try to get parent class from gen_class + parent_class = None + for prop_try in ["super_struct", "parent_class", "super_class"]: + try: + parent_class = gen_class.get_editor_property(prop_try) + if parent_class: + break + except Exception: + continue + + if parent_class: + try: + parent_class_name = parent_class.get_name() + parent_cdo_path = parent_class.get_path_name().rsplit(".", 1)[0] + f".Default__{parent_class_name}" + parent_cdo = unreal.find_object(None, parent_cdo_path) + if parent_cdo: + parent_props = set(p for p in dir(parent_cdo) if not p.startswith('_')) + except Exception: + pass + + # Fallback: use known engine base class CDOs + if not parent_props: + for base_path in [ + "/Script/Engine.Default__Character", + "/Script/Engine.Default__Pawn", + "/Script/Engine.Default__Actor" + ]: + try: + base_cdo = unreal.find_object(None, base_path) + if base_cdo: + parent_props = parent_props.union( + set(p for p in dir(base_cdo) if not p.startswith('_')) + ) + except Exception: + continue + + # Filter to user-defined properties, exclude callables (functions) + user_props = sorted(cdo_props - parent_props) + for prop_name in user_props: + try: + val = cdo.get_editor_property(prop_name) + type_name = type(val).__name__ if val is not None else "unknown" + summary["variables"].append({ + "name": prop_name, + "type": type_name, + "default_value": str(val) if val is not None else "" + }) + found_vars = True + except Exception: + continue + + if not summary["variables"]: + summary["warnings"].append("No variables found via any method") + + # --- Components --- + if include_components: + try: + scs = asset.get_editor_property("simple_construction_script") if hasattr(asset, "get_editor_property") else None + if scs: + nodes = scs.get_all_nodes() + for node in nodes or []: + comp_name = "" + comp_class = "" + try: + comp_name = str(node.get_variable_name()) if hasattr(node, "get_variable_name") else "" + except Exception: + pass + try: + cc = node.get_editor_property("component_class") if hasattr(node, "get_editor_property") else None + if cc: + comp_class = cc.get_name() + except Exception: + pass + summary["components"].append({"name": comp_name, "class": comp_class}) + else: + summary["warnings"].append("No construction script available for component summary") + except Exception as e: + summary["warnings"].append(f"Failed to enumerate components: {str(e)}") + + log.log_result("get_blueprint_summary", True, + f"Graphs: {len(summary['graphs'])}, Vars: {len(summary['variables'])}, Components: {len(summary['components'])}") + return {"success": True, "summary": summary} + + except Exception as e: + log.log_error(f"Error getting blueprint summary: {str(e)}", include_traceback=True) + return {"success": False, "error": str(e)} + + +def handle_apply_blueprint_patch(command: Dict[str, Any]) -> Dict[str, Any]: + """ + Handle a command to apply a batch of Blueprint operations. + """ + try: + blueprint_path = command.get("blueprint_path") + operations = command.get("operations", []) + options = command.get("options", {}) + stop_on_error = options.get("stop_on_error", True) + compile_after = options.get("compile_after", False) + + if not blueprint_path: + log.log_error("Missing blueprint_path for apply_blueprint_patch") + return {"success": False, "error": "Missing blueprint_path"} + + if not operations: + return {"success": False, "error": "No operations provided"} + + log.log_command("apply_blueprint_patch", f"Blueprint: {blueprint_path}, Ops: {len(operations)}") + + results = [] + success_count = 0 + + for index, op in enumerate(operations): + op_type = (op or {}).get("op", "") + op_command = dict(op or {}) + op_command["blueprint_path"] = op_command.get("blueprint_path", blueprint_path) + + handler_result = {"success": False, "error": "Unknown operation"} + + if op_type == "add_component": + handler_result = handle_add_component(op_command) + elif op_type == "add_variable": + handler_result = handle_add_variable(op_command) + elif op_type == "add_function": + handler_result = handle_add_function(op_command) + elif op_type == "add_node": + handler_result = handle_add_node(op_command) + elif op_type == "add_nodes_bulk": + handler_result = handle_add_nodes_bulk(op_command) + elif op_type == "connect_nodes": + handler_result = handle_connect_nodes(op_command) + elif op_type == "connect_nodes_bulk": + handler_result = handle_connect_nodes_bulk(op_command) + elif op_type == "delete_node": + handler_result = handle_delete_node(op_command) + elif op_type == "compile_blueprint": + handler_result = handle_compile_blueprint(op_command) + elif op_type == "get_node_guid": + handler_result = handle_get_node_guid(op_command) + else: + handler_result = {"success": False, "error": f"Unsupported op: {op_type}"} + + if handler_result.get("success"): + success_count += 1 + + results.append({ + "index": index, + "op": op_type, + "success": bool(handler_result.get("success")), + "result": handler_result + }) + + if stop_on_error and not handler_result.get("success"): + break + + if compile_after and all(r["success"] for r in results): + compile_result = handle_compile_blueprint({"blueprint_path": blueprint_path}) + results.append({ + "index": len(results), + "op": "compile_blueprint", + "success": bool(compile_result.get("success")), + "result": compile_result + }) + if compile_result.get("success"): + success_count += 1 + + overall_success = all(r["success"] for r in results) + return { + "success": overall_success, + "successful": success_count, + "total": len(results), + "results": results + } + + except Exception as e: + log.log_error(f"Error applying blueprint patch: {str(e)}", include_traceback=True) return {"success": False, "error": str(e)} \ No newline at end of file diff --git a/Content/Python/mcp_server.py b/Content/Python/mcp_server.py index f53b2b8..5e196cb 100644 --- a/Content/Python/mcp_server.py +++ b/Content/Python/mcp_server.py @@ -376,6 +376,61 @@ def create_blueprint(blueprint_name: str, parent_class: str = "Actor", save_path else: return f"Failed to create Blueprint: {response.get('error', 'Unknown error')}" +@mcp.tool() +def get_blueprint_summary(blueprint_path: str, include_graphs: bool = True, + include_variables: bool = True, include_components: bool = False) -> str: + """ + Get a high-level Blueprint summary for LLM context. + + Args: + blueprint_path: Path to the Blueprint asset (e.g., "/Game/Blueprints/BP_Player") + include_graphs: Include graph list (default True) + include_variables: Include variable list (default True) + include_components: Include construction script components (default False) + + Returns: + JSON string with summary data + """ + command = { + "type": "get_blueprint_summary", + "blueprint_path": blueprint_path, + "include_graphs": include_graphs, + "include_variables": include_variables, + "include_components": include_components + } + + response = send_to_unreal(command) + return json.dumps(response, indent=2) + + +@mcp.tool() +def apply_blueprint_patch(blueprint_path: str, operations: list, + stop_on_error: bool = True, compile_after: bool = False) -> str: + """ + Apply a batch of Blueprint operations in order. + + Args: + blueprint_path: Path to the Blueprint asset + operations: List of operations with "op" and parameters + stop_on_error: Stop on first failure (default True) + compile_after: Compile if all operations succeed (default False) + + Returns: + JSON string with per-operation results + """ + command = { + "type": "apply_blueprint_patch", + "blueprint_path": blueprint_path, + "operations": operations, + "options": { + "stop_on_error": stop_on_error, + "compile_after": compile_after + } + } + + response = send_to_unreal(command) + return json.dumps(response, indent=2) + @mcp.tool() def take_editor_screenshot() -> Image: """ @@ -638,7 +693,10 @@ def get_all_nodes_in_graph(blueprint_path: str, function_id: str) -> str: response = send_to_unreal(command) if response.get("success"): - return response.get("nodes", "[]") + nodes = response.get("nodes", []) + if isinstance(nodes, str): + return nodes + return json.dumps(nodes, indent=2) else: return f"Failed to get nodes: {response.get('error', 'Unknown error')}" diff --git a/Content/Python/unreal_socket_server.py b/Content/Python/unreal_socket_server.py index 2ebc4e4..5516a21 100644 --- a/Content/Python/unreal_socket_server.py +++ b/Content/Python/unreal_socket_server.py @@ -40,6 +40,8 @@ def __init__(self): "compile_blueprint": blueprint_commands.handle_compile_blueprint, "spawn_blueprint": blueprint_commands.handle_spawn_blueprint, "delete_node": blueprint_commands.handle_delete_node, + "get_blueprint_summary": blueprint_commands.handle_get_blueprint_summary, + "apply_blueprint_patch": blueprint_commands.handle_apply_blueprint_patch, # Getters "get_node_guid": blueprint_commands.handle_get_node_guid, @@ -90,8 +92,14 @@ def _handle_handshake(self, command: Dict[str, Any]) -> Dict[str, Any]: message = command.get("message", "") log.log_info(f"Handshake received: {message}") - # Get Unreal Engine version - engine_version = unreal.SystemLibrary.get_engine_version() + # Note: unreal.SystemLibrary calls must run on the main thread. + # When called from the main thread queue, this works fine. + # When called directly from socket thread, skip the UE API call. + engine_version = "Unknown" + try: + engine_version = unreal.SystemLibrary.get_engine_version() + except Exception: + engine_version = "UE5 (version query requires main thread)" # Add connection and session information connection_info = { @@ -199,12 +207,12 @@ def socket_server_thread(): command = json.loads(data_str) log.log_info(f"Received command: {command}") - # For handshake, we can respond directly from the thread - if command.get("type") == "handshake": - response = dispatcher.dispatch(command) - conn.sendall(json.dumps(response).encode()) + # All commands (including handshake) go through the main thread queue + # because UE API calls must run on the main game thread. + if False: + pass # placeholder for removed direct-thread handshake path else: - # For other commands, queue them for main thread execution + # Queue command for main thread execution command_id = command_counter command_counter += 1 command_queue.append((command_id, command)) diff --git a/Docs/BLUEPRINT_EDITING.md b/Docs/BLUEPRINT_EDITING.md new file mode 100644 index 0000000..12a2427 --- /dev/null +++ b/Docs/BLUEPRINT_EDITING.md @@ -0,0 +1,161 @@ +# Blueprint Editing via MCP - 功能说明 + +> 本文档记录 `feature/blueprint-ops-enhancement` 分支在上游 [prajwalshettydev/UnrealGenAISupport](https://github.com/prajwalshettydev/UnrealGenAISupport) 基础上所做的改动。 + +## 概述 + +本分支的目标是让 LLM(大语言模型)能够通过 MCP 协议**读取和修改** Unreal Engine 蓝图,实现 AI 辅助蓝图编辑的完整链路。 + +核心新增能力: +- **`get_blueprint_summary`** — 获取蓝图的结构化摘要(图表、变量、组件) +- **`apply_blueprint_patch`** — 批量执行蓝图操作(添加变量、函数、节点、连线、编译等) +- **`get_all_nodes_in_graph` 修复** — 修正返回类型,使 MCP 工具能正确传递节点列表 + +## 新增 MCP 工具 + +### get_blueprint_summary + +获取蓝图的高层级摘要,为 LLM 提供蓝图结构上下文。 + +**参数:** +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `blueprint_path` | string | (必填) | 蓝图资产路径,如 `/Game/Blueprints/BP_Player` | +| `include_graphs` | bool | `true` | 是否包含图表列表 | +| `include_variables` | bool | `true` | 是否包含变量列表 | +| `include_components` | bool | `false` | 是否包含构造脚本组件 | + +**返回示例:** +```json +{ + "success": true, + "summary": { + "blueprint_path": "/Game/Unit/Tower/Tower_Basic", + "asset_name": "Tower_Basic", + "parent_class": "MobaUnitCharacter", + "generated_class": "Tower_Basic_C", + "graphs": [ + { + "name": "EventGraph", + "type": "UbergraphPage", + "function_id": "EventGraph", + "node_count": 38 + } + ], + "variables": [ + {"name": "current_health", "type": "float", "default_value": "0.0"}, + {"name": "moba_unit_data", "type": "MobaUnitData", "default_value": ""} + ], + "components": [], + "warnings": [] + } +} +``` + +**实现说明:** +- 使用 `BlueprintEditorLibrary.find_event_graph()` / `find_graph()` 枚举图表(UE 5.7 兼容) +- 变量枚举采用 CDO(Class Default Object)反射方式,自动过滤继承属性,仅返回蓝图自身定义的变量 +- 通过构造 `Default__ClassName` 路径并与父类 CDO 做差集来识别用户自定义属性 + +### apply_blueprint_patch + +按顺序执行一批蓝图操作,支持原子化操作和失败中断。 + +**参数:** +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `blueprint_path` | string | (必填) | 蓝图资产路径 | +| `operations` | list | (必填) | 操作列表,见下表 | +| `stop_on_error` | bool | `true` | 遇到第一个失败时是否中断 | +| `compile_after` | bool | `false` | 全部成功后是否自动编译 | + +**支持的操作类型(`op` 字段):** +| op | 说明 | 关键参数 | +|----|------|----------| +| `add_component` | 添加组件 | `component_class` | +| `add_variable` | 添加变量 | `variable_name`, `variable_type`, `default_value` | +| `add_function` | 添加函数 | `function_name` | +| `add_node` | 添加节点 | `function_id`, `node_type` | +| `connect_nodes` | 连接节点 | `source_node_id`, `source_pin`, `target_node_id`, `target_pin` | +| `delete_node` | 删除节点 | `function_id`, `node_id` | +| `compile_blueprint` | 编译蓝图 | (无额外参数) | +| `get_node_guid` | 获取节点 GUID | `graph_type`, `node_name` | + +**调用示例:** +```json +{ + "blueprint_path": "/Game/Unit/Tower/Tower_Basic", + "operations": [ + {"op": "add_variable", "variable_name": "MaxHealth", "variable_type": "float", "default_value": "100.0"}, + {"op": "add_variable", "variable_name": "IsAlive", "variable_type": "bool", "default_value": "true"}, + {"op": "compile_blueprint"} + ], + "stop_on_error": true, + "compile_after": false +} +``` + +## Bug 修复 + +### get_all_nodes_in_graph 返回类型修复 + +**问题:** 原实现中 `response.get("nodes", "[]")` 期望获取字符串,但 handler 返回的是已解析的 Python list,导致 MCP 工具签名 `-> str` 验证失败。 + +**修复:** 在 `mcp_server.py` 中增加类型检查,当 `nodes` 为 list 时使用 `json.dumps()` 序列化为字符串。 + +### Socket Server 线程安全修复 + +**问题:** MCP 握手命令直接在 socket 线程中执行 `unreal.SystemLibrary.get_engine_version()`,触发 "Attempted to access Unreal API from outside the main game thread" 错误。 + +**修复:** 将所有命令(包括握手)统一排入主线程队列执行,并在 `_handle_handshake` 中增加 try-except 容错。 + +## UE 5.7.1 兼容性修复 + +以下修改解决了原插件在 UE 5.7.1 上的编译错误(原插件基于 UE 5.1 ~ 5.4 开发): + +| 修改内容 | 涉及文件 | 说明 | +|----------|----------|------| +| `ANY_PACKAGE` → `FindFirstObject` | GenActorUtils.cpp, GenWidgetUtils.cpp, GenBlueprintUtils.cpp, GenBlueprintNodeCreator.cpp | 11 处替换 | +| `FEditorStyle` → `FAppStyle` | GenEditorCommands.h, GenEditorWindow.cpp, GenerativeAISupportEditor.cpp | 类名和头文件替换 | +| `ClassDefaultObject` → `GetDefaultObject()` | GenActorUtils.cpp | API 签名变更 | +| `FStringOutputDevice` → `FOutputDeviceNull` | GenWidgetUtils.cpp | 类被移除 | +| `FMessageDialog::Open` 参数调整 | GenEditorWindow.cpp | Title 参数传值方式变更 | +| 移除 `EditorScriptingUtilities` / `Blutility` 依赖 | GenerativeAISupportEditor.Build.cs, GenerativeAISupport.uplugin | 模块在 5.7 中已移除 | +| 移除 `EditorStyle` 模块依赖 | GenerativeAISupportEditor.Build.cs | 已合并入 AppStyle | + +## 改动文件清单 + +### Python(MCP / Handler) +| 文件 | 改动类型 | +|------|----------| +| `Content/Python/mcp_server.py` | 新增 2 个 MCP 工具,修复 1 个返回类型 | +| `Content/Python/handlers/blueprint_commands.py` | 新增 2 个 handler + 辅助函数 | +| `Content/Python/unreal_socket_server.py` | 注册新 handler,修复线程安全 | + +### C++(UE 5.7.1 兼容性) +| 文件 | 改动类型 | +|------|----------| +| `GenerativeAISupport.uplugin` | 移除已废弃插件依赖 | +| `GenerativeAISupportEditor.Build.cs` | 移除已废弃模块依赖 | +| `GenActorUtils.cpp` | API 迁移 | +| `GenWidgetUtils.cpp` | API 迁移 | +| `GenBlueprintUtils.cpp` | API 迁移 | +| `GenBlueprintNodeCreator.cpp` | API 迁移 | +| `GenEditorCommands.h` | 头文件迁移 | +| `GenEditorWindow.cpp` | API 迁移 | +| `GenerativeAISupportEditor.cpp` | 头文件迁移 | + +## 已知限制 + +- `get_blueprint_summary` 的变量枚举基于 CDO 反射差集方法,如果蓝图的父类不是 Actor / Pawn / Character 的直接子类,可能会漏掉或多出少量属性 +- `EdGraph.nodes` 在 Python 中被标记为 protected,无法直接读取节点列表,需通过 C++ 端的 `GenBlueprintNodeCreator.get_all_nodes_in_graph()` 间接获取 +- `BlueprintEditorLibrary` 在 UE 5.7 中暴露的 graph API 有限(`find_graph`、`find_event_graph`、`rename_graph`、`remove_graph` 等),无法直接枚举所有 graph +- 节点 Pin 连接合法性检查尚未实现 + +## 后续计划 + +- [ ] `get_blueprint_summary` 增加 `include_inherited_variables` 选项 +- [ ] 实现 `preview_blueprint_patch`(Diff 预览) +- [ ] 实现 `undo_last_patch`(自动回滚) +- [ ] Pin 类型验证 / 连接合法性检查 +- [ ] 支持更多蓝图父类的属性过滤(如 GameMode、PlayerController 等) diff --git a/Source/GenerativeAISupportEditor/GenerativeAISupportEditor.Build.cs b/Source/GenerativeAISupportEditor/GenerativeAISupportEditor.Build.cs index 2a17948..f8d4fc2 100644 --- a/Source/GenerativeAISupportEditor/GenerativeAISupportEditor.Build.cs +++ b/Source/GenerativeAISupportEditor/GenerativeAISupportEditor.Build.cs @@ -29,11 +29,8 @@ public GenerativeAISupportEditor(ReadOnlyTargetRules Target) : base(Target) "UnrealEd", "Slate", "SlateCore", - "EditorStyle", "WorkspaceMenuStructure", "Projects", - "EditorScriptingUtilities", - "Blutility", "MaterialEditor", "MaterialUtilities", "BlueprintGraph", diff --git a/Source/GenerativeAISupportEditor/Private/Editor/GenEditorWindow.cpp b/Source/GenerativeAISupportEditor/Private/Editor/GenEditorWindow.cpp index 9b60ed4..f6bf34d 100644 --- a/Source/GenerativeAISupportEditor/Private/Editor/GenEditorWindow.cpp +++ b/Source/GenerativeAISupportEditor/Private/Editor/GenEditorWindow.cpp @@ -6,7 +6,7 @@ #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBorder.h" #include "Styling/SlateStyleRegistry.h" -#include "EditorStyleSet.h" +#include "Styling/AppStyle.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Docking/TabManager.h" #include "LevelEditor.h" @@ -48,7 +48,7 @@ void FGenEditorWindowManager::RegisterTabSpawner(const TSharedPtr& TabManager->RegisterTabSpawner(TabId, FOnSpawnTab::CreateRaw(this, &FGenEditorWindowManager::SpawnEditorWindowTab)) .SetDisplayName(NSLOCTEXT("GenerativeAISupport", "TabTitle", "Gen AI Support")) .SetTooltipText(NSLOCTEXT("GenerativeAISupport", "TabTooltip", "Open the Generative AI Support window")) - .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Details")); + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details")); } void FGenEditorWindowManager::UnregisterTabSpawner(const TSharedPtr& TabManager) @@ -167,7 +167,7 @@ void SGenEditorWindow::Construct(const FArguments& InArgs) TSharedRef SGenEditorWindow::CreateMCPStatusSection() { return SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(8) [ SNew(SVerticalBox) @@ -235,7 +235,7 @@ TSharedRef SGenEditorWindow::CreateMCPStatusSection() .HeightOverride(1.0f) [ SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush("Menu.Separator")) + .BorderImage(FAppStyle::GetBrush("Menu.Separator")) .Padding(FMargin(0.0f)) ] ] @@ -360,7 +360,7 @@ TSharedRef SGenEditorWindow::CreateMCPStatusSection() "- Set up Cursor configuration\n\n" "After setup, you'll need to restart Claude or Cursor to activate the MCP Server."); - FMessageDialog::Open(EAppMsgType::Ok, Message, &Title); + FMessageDialog::Open(EAppMsgType::Ok, Message, Title); return FReply::Handled(); }) .ToolTipText(NSLOCTEXT("GenerativeAISupport", "SetupMCPTooltip", "Set up MCP Server configuration")) @@ -477,7 +477,7 @@ TSharedRef SGenEditorWindow::CreateMCPStatusSection() TSharedRef SGenEditorWindow::CreateAPIStatusSection() { return SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(8) [ SNew(SVerticalBox) @@ -534,7 +534,7 @@ TSharedRef SGenEditorWindow::CreateAPIStatusSection() .HeightOverride(1.0f) [ SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush("Menu.Separator")) + .BorderImage(FAppStyle::GetBrush("Menu.Separator")) .Padding(FMargin(0.0f)) ] ] @@ -550,7 +550,7 @@ TSharedRef SGenEditorWindow::CreateAPIStatusSection() TSharedRef SGenEditorWindow::CreateActionButtonsSection() { return SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(8) [ SNew(SVerticalBox) diff --git a/Source/GenerativeAISupportEditor/Private/GenerativeAISupportEditor.cpp b/Source/GenerativeAISupportEditor/Private/GenerativeAISupportEditor.cpp index 11b0779..c330ada 100644 --- a/Source/GenerativeAISupportEditor/Private/GenerativeAISupportEditor.cpp +++ b/Source/GenerativeAISupportEditor/Private/GenerativeAISupportEditor.cpp @@ -6,7 +6,7 @@ #include "GenerativeAISupportEditor.h" #include "ISettingsModule.h" -#include "EditorStyleSet.h" +#include "Styling/AppStyle.h" #include "GenerativeAISupportSettings.h" #include "ISettingsSection.h" #include "LevelEditor.h" @@ -44,7 +44,7 @@ void FGenerativeAISupportEditorModule::StartupModule() &FGenEditorWindowManager::SpawnEditorWindowTab)) .SetDisplayName(LOCTEXT("GenEditorWindowTitle", "Gen AI Support")) .SetTooltipText(LOCTEXT("GenEditorWindowTooltip", "Open the Generative AI Support window")) - .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Details")) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details")) .SetGroup(WorkspaceMenu::GetMenuStructure().GetToolsCategory()); } diff --git a/Source/GenerativeAISupportEditor/Private/MCP/GenActorUtils.cpp b/Source/GenerativeAISupportEditor/Private/MCP/GenActorUtils.cpp index 51538d5..a349146 100644 --- a/Source/GenerativeAISupportEditor/Private/MCP/GenActorUtils.cpp +++ b/Source/GenerativeAISupportEditor/Private/MCP/GenActorUtils.cpp @@ -7,8 +7,7 @@ #include "Engine/StaticMeshActor.h" #include "AssetRegistry/AssetRegistryModule.h" -#include "EditorLevelLibrary.h" -#include "EditorUtilityLibrary.h" +// EditorLevelLibrary.h and EditorUtilityLibrary.h removed (deprecated in UE 5.4+) #include "UObject/ConstructorHelpers.h" @@ -137,7 +136,7 @@ AActor* UGenActorUtils::SpawnActorFromClass(const FString& ActorClassName, const { // Try to find class by name FString FullClassName = FString::Printf(TEXT("/Script/Engine.%s"), *ActorClassName); - ActorClass = FindObject(ANY_PACKAGE, *ActorClassName); + ActorClass = FindFirstObject(*ActorClassName, EFindFirstObjectOptions::NativeFirst); if (!ActorClass) { @@ -411,7 +410,7 @@ FString UGenActorUtils::CreateGameModeWithPawn(const FString& GameModePath, cons // Load the base class (default to AGameModeBase if not specified) FString BaseClassToUse = BaseClassName.IsEmpty() ? TEXT("GameModeBase") : BaseClassName; - UClass* BaseClass = FindObject(ANY_PACKAGE, *BaseClassToUse); + UClass* BaseClass = FindFirstObject(*BaseClassToUse, EFindFirstObjectOptions::NativeFirst); if (!BaseClass || !BaseClass->IsChildOf(AGameModeBase::StaticClass())) { UE_LOG(LogTemp, Error, TEXT("Invalid base class %s for game mode"), *BaseClassToUse); @@ -449,7 +448,7 @@ FString UGenActorUtils::CreateGameModeWithPawn(const FString& GameModePath, cons UE_LOG(LogTemp, Error, TEXT("Generated class not found for %s"), *GameModePath); return TEXT("{\"success\": false, \"error\": \"No generated class\"}"); } - if (AGameModeBase* GameModeCDO = Cast(GameModeClass->ClassDefaultObject)) + if (AGameModeBase* GameModeCDO = Cast(GameModeClass->GetDefaultObject())) { GameModeCDO->DefaultPawnClass = PawnBP->GeneratedClass; } diff --git a/Source/GenerativeAISupportEditor/Private/MCP/GenBlueprintNodeCreator.cpp b/Source/GenerativeAISupportEditor/Private/MCP/GenBlueprintNodeCreator.cpp index d08b404..654f694 100644 --- a/Source/GenerativeAISupportEditor/Private/MCP/GenBlueprintNodeCreator.cpp +++ b/Source/GenerativeAISupportEditor/Private/MCP/GenBlueprintNodeCreator.cpp @@ -815,9 +815,9 @@ bool UGenBlueprintNodeCreator::TryCreateKnownNodeType(UEdGraph* Graph, const FSt return CreateMathFunctionNode(Graph, TEXT("KismetMathLibrary"), TEXT("Conv_DoubleToFloat"), OutNode); } - UClass* NodeClass = FindObject(ANY_PACKAGE, *(TEXT("UK2Node_") + ActualNodeType)); + UClass* NodeClass = FindFirstObject(*(TEXT("UK2Node_") + ActualNodeType), EFindFirstObjectOptions::NativeFirst); if (!NodeClass || !NodeClass->IsChildOf(UK2Node::StaticClass())) - NodeClass = FindObject(ANY_PACKAGE, *ActualNodeType); + NodeClass = FindFirstObject(*ActualNodeType, EFindFirstObjectOptions::NativeFirst); if (NodeClass && NodeClass->IsChildOf(UK2Node::StaticClass())) { OutNode = NewObject(Graph, NodeClass); @@ -904,7 +904,7 @@ FString UGenBlueprintNodeCreator::TryCreateNodeFromLibraries(UEdGraph* Graph, co for (const FString& LibraryName : CommonLibraries) { - UClass* LibClass = FindObject(ANY_PACKAGE, *LibraryName); + UClass* LibClass = FindFirstObject(*LibraryName, EFindFirstObjectOptions::NativeFirst); if (!LibClass) continue; for (TFieldIterator FuncIt(LibClass); FuncIt; ++FuncIt) @@ -974,7 +974,7 @@ bool UGenBlueprintNodeCreator::CreateMathFunctionNode(UEdGraph* Graph, const FSt UK2Node_CallFunction* FunctionNode = NewObject(Graph); if (FunctionNode) { - UClass* Class = FindObject(ANY_PACKAGE, *ClassName); + UClass* Class = FindFirstObject(*ClassName, EFindFirstObjectOptions::NativeFirst); if (Class) { UFunction* Function = Class->FindFunctionByName(*FunctionName); @@ -1035,7 +1035,7 @@ FString UGenBlueprintNodeCreator::GetNodeSuggestions(const FString& NodeType) for (const FString& LibraryName : CommonLibraries) { - UClass* LibClass = FindObject(ANY_PACKAGE, *LibraryName); + UClass* LibClass = FindFirstObject(*LibraryName, EFindFirstObjectOptions::NativeFirst); if (!LibClass) continue; for (TFieldIterator FuncIt(LibClass); FuncIt; ++FuncIt) diff --git a/Source/GenerativeAISupportEditor/Private/MCP/GenBlueprintUtils.cpp b/Source/GenerativeAISupportEditor/Private/MCP/GenBlueprintUtils.cpp index 935df09..e6685aa 100644 --- a/Source/GenerativeAISupportEditor/Private/MCP/GenBlueprintUtils.cpp +++ b/Source/GenerativeAISupportEditor/Private/MCP/GenBlueprintUtils.cpp @@ -738,13 +738,13 @@ UBlueprint* UGenBlueprintUtils::LoadBlueprintAsset(const FString& BlueprintPath) UClass* UGenBlueprintUtils::FindClassByName(const FString& ClassName) { // Try to find the class by name - UClass* Class = FindObject(ANY_PACKAGE, *ClassName); + UClass* Class = FindFirstObject(*ClassName, EFindFirstObjectOptions::NativeFirst); // If not found, try with various prefixes if (!Class) { FString PrefixedClassName = TEXT("/Script/Engine.") + ClassName; - Class = FindObject(ANY_PACKAGE, *PrefixedClassName); + Class = FindFirstObject(*PrefixedClassName, EFindFirstObjectOptions::NativeFirst); } if (!Class) @@ -1089,7 +1089,7 @@ FString UGenBlueprintUtils::AddComponentWithEvents(const FString& BlueprintPath, } // Find the component class (must be a shape/collision component) - UClass* ComponentClass = FindObject(ANY_PACKAGE, *ComponentClassName); + UClass* ComponentClass = FindFirstObject(*ComponentClassName, EFindFirstObjectOptions::NativeFirst); if (!ComponentClass || !ComponentClass->IsChildOf(UShapeComponent::StaticClass())) { UE_LOG(LogTemp, Error, TEXT("Component class %s must be a collision component (e.g., BoxComponent, SphereComponent)"), *ComponentClassName); diff --git a/Source/GenerativeAISupportEditor/Private/MCP/GenWidgetUtils.cpp b/Source/GenerativeAISupportEditor/Private/MCP/GenWidgetUtils.cpp index 8efb48c..19609f6 100644 --- a/Source/GenerativeAISupportEditor/Private/MCP/GenWidgetUtils.cpp +++ b/Source/GenerativeAISupportEditor/Private/MCP/GenWidgetUtils.cpp @@ -6,7 +6,7 @@ #include "Blueprint/WidgetTree.h" #include "Components/CanvasPanel.h" #include "Components/PanelWidget.h" -#include "EditorAssetLibrary.h" +// EditorAssetLibrary.h removed (deprecated in UE 5.4+) #include "AssetRegistry/AssetRegistryModule.h" #include "Kismet2/BlueprintEditorUtils.h" #include "UObject/SavePackage.h" @@ -22,6 +22,7 @@ #include "Components/Image.h" #include "Components/VerticalBoxSlot.h" #include "Kismet2/KismetEditorUtilities.h" +#include "Misc/OutputDeviceNull.h" // Helper to find widget by name recursively (WidgetTree::FindWidget is often sufficient) UWidget* UGenWidgetUtils::FindWidgetByName(UWidgetTree* WidgetTree, const FName& Name) @@ -152,7 +153,7 @@ FString UGenWidgetUtils::AddWidgetToUserWidget(const FString& UserWidgetPath, co } // 4. Find Widget Class to Add - UClass* FoundClass = FindObject(ANY_PACKAGE, *WidgetClassName); + UClass* FoundClass = FindFirstObject(*WidgetClassName, EFindFirstObjectOptions::NativeFirst); if (!FoundClass) FoundClass = LoadClass(nullptr, *FString::Printf(TEXT("/Script/UMG.%s"), *WidgetClassName)); if (!FoundClass) FoundClass = LoadClass(nullptr, *FString::Printf(TEXT("/Script/CommonUI.%s"), *WidgetClassName)); // Add more lookups if needed for custom widget libraries @@ -432,7 +433,7 @@ FString UGenWidgetUtils::EditWidgetProperty(const FString& UserWidgetPath, const // Get the address of the property within the TargetObject instance void* PropertyValueAddress = TargetProperty->ContainerPtrToValuePtr(TargetObject); - FStringOutputDevice ImportErrorOutput; + FOutputDeviceNull ImportErrorOutput; const TCHAR* Result = TargetProperty->ImportText_Direct( *ValueString, // Buffer (const TCHAR*) PropertyValueAddress, // Data (void*) @@ -447,10 +448,10 @@ FString UGenWidgetUtils::EditWidgetProperty(const FString& UserWidgetPath, const // TargetProperty->ImportText(*ValueString, PropertyValueAddress, PPF_None, TargetObject, &ErrorMessage, &PropertyChain); // Pass error message ptr // Check the result: nullptr indicates success, otherwise it points to the end of successfully parsed text - if (Result != nullptr && ImportErrorOutput.Len() > 0) // Check if parsing stopped *and* an error was reported + if (Result == nullptr) // Check if parsing failed { // An error occurred during import - ErrorMessage = ImportErrorOutput; // Get the error message from the output device + ErrorMessage = TEXT("ImportText_Direct failed"); UE_LOG(LogTemp, Error, TEXT("Failed to set property '%s' on '%s'. ImportText error: %s"), *PropertyName, *TargetObject->GetName(), *ErrorMessage); bSuccess = false; diff --git a/Source/GenerativeAISupportEditor/Public/Editor/GenEditorCommands.h b/Source/GenerativeAISupportEditor/Public/Editor/GenEditorCommands.h index 2b3f140..df21a1c 100644 --- a/Source/GenerativeAISupportEditor/Public/Editor/GenEditorCommands.h +++ b/Source/GenerativeAISupportEditor/Public/Editor/GenEditorCommands.h @@ -3,7 +3,7 @@ #pragma once #include "CoreMinimal.h" -#include "EditorStyleSet.h" +#include "Styling/AppStyle.h" #include "Framework/Commands/Commands.h" /** @@ -17,7 +17,7 @@ class FGenEditorCommands : public TCommands TEXT("GenerativeAISupport"), // Context name for fast lookup NSLOCTEXT("Contexts", "GenerativeAISupport", "Generative AI Support Plugin"), // Localized context name for displaying NAME_None, // Parent context name - FEditorStyle::GetStyleSetName() // Style set name + FAppStyle::GetAppStyleSetName() // Style set name ) { }