@createlex/createlexgenai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -0
- package/bin/createlex.js +5 -0
- package/package.json +45 -0
- package/python/activity_tracker.py +280 -0
- package/python/fastmcp.py +768 -0
- package/python/mcp_server_stdio.py +4720 -0
- package/python/requirements.txt +7 -0
- package/python/subscription_validator.py +199 -0
- package/python/ue_native_handler.py +573 -0
- package/python/ui_slice_host.py +637 -0
- package/src/cli.js +109 -0
- package/src/commands/config.js +56 -0
- package/src/commands/connect.js +100 -0
- package/src/commands/exec.js +148 -0
- package/src/commands/login.js +111 -0
- package/src/commands/logout.js +17 -0
- package/src/commands/serve.js +237 -0
- package/src/commands/setup.js +65 -0
- package/src/commands/status.js +126 -0
- package/src/commands/tools.js +133 -0
- package/src/core/auth-manager.js +147 -0
- package/src/core/config-store.js +81 -0
- package/src/core/discovery.js +71 -0
- package/src/core/ide-configurator.js +189 -0
- package/src/core/remote-execution.js +228 -0
- package/src/core/subscription.js +176 -0
- package/src/core/unreal-connection.js +318 -0
- package/src/core/web-remote-control.js +243 -0
- package/src/utils/logger.js +66 -0
- package/src/utils/python-manager.js +142 -0
|
@@ -0,0 +1,4720 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from fastmcp import FastMCP
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, List, Dict, Optional
|
|
11
|
+
|
|
12
|
+
_CURRENT_DIR = Path(__file__).resolve().parent
|
|
13
|
+
if str(_CURRENT_DIR) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(_CURRENT_DIR))
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from ui_slice_host import auto_slice_ui_mockup_host, has_pillow as host_slice_has_pillow
|
|
18
|
+
except Exception as host_slice_import_error:
|
|
19
|
+
auto_slice_ui_mockup_host = None
|
|
20
|
+
|
|
21
|
+
def host_slice_has_pillow():
|
|
22
|
+
return False, str(host_slice_import_error)
|
|
23
|
+
|
|
24
|
+
# THIS FILE WILL RUN OUTSIDE THE UNREAL ENGINE SCOPE,
|
|
25
|
+
# DO NOT IMPORT UNREAL MODULES HERE OR EXECUTE IT IN THE UNREAL ENGINE PYTHON INTERPRETER
|
|
26
|
+
|
|
27
|
+
# Global context cache for Blueprint analysis and iterative refinement
|
|
28
|
+
_blueprint_context_cache = {
|
|
29
|
+
"cached_context": None,
|
|
30
|
+
"cache_timestamp": 0,
|
|
31
|
+
"cache_blueprint_path": None,
|
|
32
|
+
"cache_duration": 300, # 5 minutes cache duration
|
|
33
|
+
"generation_history": [], # Track generation attempts for iterative refinement
|
|
34
|
+
"last_generated_function": None, # Store last generated function for refinement
|
|
35
|
+
"validation_results": None # Store validation results for iteration
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Create a PID file to let the Unreal plugin know this process is running
|
|
39
|
+
def write_pid_file():
|
|
40
|
+
try:
|
|
41
|
+
pid = os.getpid()
|
|
42
|
+
pid_dir = os.path.join(os.path.expanduser("~"), ".unrealgenai")
|
|
43
|
+
os.makedirs(pid_dir, exist_ok=True)
|
|
44
|
+
pid_path = os.path.join(pid_dir, "mcp_server.pid")
|
|
45
|
+
with open(pid_path, "w") as f:
|
|
46
|
+
f.write(f"{pid}\n8080") # Store PID and port
|
|
47
|
+
|
|
48
|
+
# Register to delete the PID file on exit
|
|
49
|
+
import atexit
|
|
50
|
+
def cleanup_pid_file():
|
|
51
|
+
try:
|
|
52
|
+
if os.path.exists(pid_path):
|
|
53
|
+
os.remove(pid_path)
|
|
54
|
+
except:
|
|
55
|
+
pass
|
|
56
|
+
atexit.register(cleanup_pid_file)
|
|
57
|
+
return pid_path
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"Failed to write PID file: {e}", file=sys.stderr)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
# Write PID file on startup
|
|
63
|
+
pid_file = write_pid_file()
|
|
64
|
+
if pid_file:
|
|
65
|
+
print(f"MCP Server started with PID file at: {pid_file}", file=sys.stderr)
|
|
66
|
+
|
|
67
|
+
# Create an MCP server
|
|
68
|
+
mcp = FastMCP("UnrealHandshake")
|
|
69
|
+
|
|
70
|
+
# Context caching and validation utilities
|
|
71
|
+
def get_cached_blueprint_context(force_refresh=False):
|
|
72
|
+
"""Get cached Blueprint context or fetch fresh if needed"""
|
|
73
|
+
global _blueprint_context_cache
|
|
74
|
+
|
|
75
|
+
current_time = time.time()
|
|
76
|
+
cache = _blueprint_context_cache
|
|
77
|
+
|
|
78
|
+
# Check if cache is valid and not forced to refresh
|
|
79
|
+
if (not force_refresh and
|
|
80
|
+
cache["cached_context"] and
|
|
81
|
+
(current_time - cache["cache_timestamp"]) < cache["cache_duration"]):
|
|
82
|
+
print(f"Using cached context (age: {current_time - cache['cache_timestamp']:.1f}s)")
|
|
83
|
+
return cache["cached_context"]
|
|
84
|
+
|
|
85
|
+
# Fetch fresh context
|
|
86
|
+
try:
|
|
87
|
+
context_cmd = {
|
|
88
|
+
"type": "get_blueprint_context",
|
|
89
|
+
"include_editor_state": True,
|
|
90
|
+
"include_graph_info": True,
|
|
91
|
+
"include_selected_nodes": True,
|
|
92
|
+
"include_open_editors": False
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
context_response = send_to_unreal(context_cmd)
|
|
96
|
+
|
|
97
|
+
if context_response.get("success"):
|
|
98
|
+
# Cache the context
|
|
99
|
+
cache["cached_context"] = context_response
|
|
100
|
+
cache["cache_timestamp"] = current_time
|
|
101
|
+
cache["cache_blueprint_path"] = context_response.get("active_blueprint", {}).get("path", "")
|
|
102
|
+
|
|
103
|
+
blueprint_name = context_response.get("active_blueprint", {}).get("name", "Unknown")
|
|
104
|
+
node_count = len(context_response.get("all_nodes", {}).get("nodes", []))
|
|
105
|
+
print(f"Context refreshed - Blueprint: {blueprint_name} ({node_count} nodes)")
|
|
106
|
+
|
|
107
|
+
return context_response
|
|
108
|
+
else:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"Error fetching Blueprint context: {e}")
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def validate_generated_function(implementation_result):
|
|
116
|
+
"""Validate the generated function and identify issues"""
|
|
117
|
+
validation_result = {
|
|
118
|
+
"is_valid": True,
|
|
119
|
+
"issues": [],
|
|
120
|
+
"suggestions": [],
|
|
121
|
+
"needs_refinement": False
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if not implementation_result.get("success"):
|
|
125
|
+
validation_result["is_valid"] = False
|
|
126
|
+
validation_result["issues"].append("Function creation failed")
|
|
127
|
+
validation_result["needs_refinement"] = True
|
|
128
|
+
|
|
129
|
+
errors = implementation_result.get("errors", [])
|
|
130
|
+
if errors:
|
|
131
|
+
validation_result["is_valid"] = False
|
|
132
|
+
validation_result["issues"].extend(errors)
|
|
133
|
+
validation_result["needs_refinement"] = True
|
|
134
|
+
|
|
135
|
+
# Analyze error types and suggest fixes
|
|
136
|
+
for error in errors:
|
|
137
|
+
if "node connection failed" in error.lower():
|
|
138
|
+
validation_result["suggestions"].append("Check node pin compatibility and availability")
|
|
139
|
+
elif "failed to add" in error.lower() and "node" in error.lower():
|
|
140
|
+
validation_result["suggestions"].append("Verify node type exists in current Unreal Engine version")
|
|
141
|
+
elif "compilation" in error.lower():
|
|
142
|
+
validation_result["suggestions"].append("Check Blueprint syntax and node logic")
|
|
143
|
+
|
|
144
|
+
# Check implementation completeness
|
|
145
|
+
created_nodes = implementation_result.get("created_nodes", [])
|
|
146
|
+
implementation_steps = implementation_result.get("implementation_steps", [])
|
|
147
|
+
|
|
148
|
+
if len(created_nodes) < 2:
|
|
149
|
+
validation_result["suggestions"].append("Function seems too simple, consider adding more logic nodes")
|
|
150
|
+
|
|
151
|
+
successful_steps = [step for step in implementation_steps if step.get("success")]
|
|
152
|
+
if len(successful_steps) < len(implementation_steps) * 0.8: # Less than 80% success rate
|
|
153
|
+
validation_result["needs_refinement"] = True
|
|
154
|
+
validation_result["suggestions"].append("Multiple implementation steps failed, function needs rework")
|
|
155
|
+
|
|
156
|
+
return validation_result
|
|
157
|
+
|
|
158
|
+
def refine_function_based_on_issues(original_generation, validation_result, context):
|
|
159
|
+
"""Refine function generation based on validation issues and updated context"""
|
|
160
|
+
|
|
161
|
+
# Extract original specification
|
|
162
|
+
original_spec = original_generation.get("function_specification", {})
|
|
163
|
+
original_description = original_generation.get("function_description", "")
|
|
164
|
+
|
|
165
|
+
# Create refined specification
|
|
166
|
+
refined_spec = original_spec.copy()
|
|
167
|
+
refined_description = original_description
|
|
168
|
+
|
|
169
|
+
# Apply refinements based on issues
|
|
170
|
+
issues = validation_result.get("issues", [])
|
|
171
|
+
suggestions = validation_result.get("suggestions", [])
|
|
172
|
+
|
|
173
|
+
# Analyze what currently exists in the Blueprint to avoid duplication
|
|
174
|
+
existing_functions = []
|
|
175
|
+
existing_node_types = set()
|
|
176
|
+
|
|
177
|
+
if context and context.get("all_nodes", {}).get("nodes"):
|
|
178
|
+
for node in context["all_nodes"]["nodes"]:
|
|
179
|
+
node_class = node.get("class_name", "")
|
|
180
|
+
node_title = node.get("title", "")
|
|
181
|
+
existing_node_types.add(node_class)
|
|
182
|
+
|
|
183
|
+
# Look for function calls that might conflict
|
|
184
|
+
if "Function" in node_class or "Call Function" in node_title:
|
|
185
|
+
existing_functions.append(node_title)
|
|
186
|
+
|
|
187
|
+
# Check if our function might conflict with existing ones
|
|
188
|
+
proposed_name = refined_spec.get("function_name", "")
|
|
189
|
+
if any(proposed_name.lower() in func.lower() for func in existing_functions):
|
|
190
|
+
# Append iteration number to avoid conflict
|
|
191
|
+
iteration_num = original_generation.get("refinement_iteration", 0) + 1
|
|
192
|
+
refined_spec["function_name"] = f"{proposed_name}V{iteration_num}"
|
|
193
|
+
print(f"Function name conflict detected, renamed to: {refined_spec['function_name']}")
|
|
194
|
+
|
|
195
|
+
# Use existing node patterns from the Blueprint for better compatibility
|
|
196
|
+
commonly_used_nodes = []
|
|
197
|
+
if context and context.get("all_nodes", {}).get("nodes"):
|
|
198
|
+
node_count_map = {}
|
|
199
|
+
for node in context["all_nodes"]["nodes"]:
|
|
200
|
+
node_class = node.get("class_name", "")
|
|
201
|
+
if node_class.startswith("K2Node_"):
|
|
202
|
+
simple_name = node_class.replace("K2Node_", "").replace("_", " ")
|
|
203
|
+
node_count_map[simple_name] = node_count_map.get(simple_name, 0) + 1
|
|
204
|
+
|
|
205
|
+
# Get the top 3 most used node types
|
|
206
|
+
commonly_used_nodes = [node for node, count in sorted(node_count_map.items(), key=lambda x: x[1], reverse=True)[:3]]
|
|
207
|
+
print(f"Most commonly used nodes in Blueprint: {commonly_used_nodes}")
|
|
208
|
+
|
|
209
|
+
# Refine node suggestions based on errors
|
|
210
|
+
if any("node connection failed" in issue.lower() for issue in issues):
|
|
211
|
+
# Simplify connections - use more basic nodes
|
|
212
|
+
refined_spec["suggested_nodes"] = [
|
|
213
|
+
node for node in refined_spec.get("suggested_nodes", [])
|
|
214
|
+
if node in ["Branch", "Print String", "Set", "Get", "Add", "Multiply", "Greater", "Less"]
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
# Add basic execution flow nodes
|
|
218
|
+
if "Branch" not in refined_spec["suggested_nodes"]:
|
|
219
|
+
refined_spec["suggested_nodes"].insert(0, "Branch")
|
|
220
|
+
if "Print String" not in refined_spec["suggested_nodes"]:
|
|
221
|
+
refined_spec["suggested_nodes"].append("Print String")
|
|
222
|
+
|
|
223
|
+
if any("failed to add" in issue.lower() and "node" in issue.lower() for issue in issues):
|
|
224
|
+
# Replace complex nodes with basic equivalents
|
|
225
|
+
node_replacements = {
|
|
226
|
+
"Launch Character": "Set Actor Location",
|
|
227
|
+
"Apply Damage": "Set Health",
|
|
228
|
+
"Spawn Actor from Class": "Create Object",
|
|
229
|
+
"Play Animation": "Set Animation Mode",
|
|
230
|
+
"Set Timer by Function Name": "Delay"
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
new_nodes = []
|
|
234
|
+
for node in refined_spec.get("suggested_nodes", []):
|
|
235
|
+
if node in node_replacements:
|
|
236
|
+
new_nodes.append(node_replacements[node])
|
|
237
|
+
else:
|
|
238
|
+
new_nodes.append(node)
|
|
239
|
+
refined_spec["suggested_nodes"] = new_nodes
|
|
240
|
+
|
|
241
|
+
# Prioritize commonly used nodes from the existing Blueprint
|
|
242
|
+
if commonly_used_nodes:
|
|
243
|
+
# Replace some suggested nodes with commonly used ones for better compatibility
|
|
244
|
+
final_nodes = []
|
|
245
|
+
|
|
246
|
+
# Start with commonly used nodes that make sense
|
|
247
|
+
for common_node in commonly_used_nodes:
|
|
248
|
+
if common_node not in ["Event", "Comment"] and len(final_nodes) < 3: # Skip event nodes and comments
|
|
249
|
+
final_nodes.append(common_node)
|
|
250
|
+
|
|
251
|
+
# Add some of the original suggestions
|
|
252
|
+
for suggested_node in refined_spec.get("suggested_nodes", []):
|
|
253
|
+
if suggested_node not in final_nodes and len(final_nodes) < 6:
|
|
254
|
+
final_nodes.append(suggested_node)
|
|
255
|
+
|
|
256
|
+
# Ensure we have basic nodes for functionality
|
|
257
|
+
essential_nodes = ["Branch", "Print String"]
|
|
258
|
+
for essential in essential_nodes:
|
|
259
|
+
if essential not in final_nodes and len(final_nodes) < 6:
|
|
260
|
+
final_nodes.append(essential)
|
|
261
|
+
|
|
262
|
+
refined_spec["suggested_nodes"] = final_nodes
|
|
263
|
+
print(f"Refined node suggestions (using common Blueprint patterns): {final_nodes}")
|
|
264
|
+
|
|
265
|
+
# Limit complexity for better success rate
|
|
266
|
+
if len(refined_spec.get("suggested_nodes", [])) > 6:
|
|
267
|
+
refined_spec["suggested_nodes"] = refined_spec["suggested_nodes"][:6]
|
|
268
|
+
|
|
269
|
+
# Create new generation result with refinements
|
|
270
|
+
refined_generation = original_generation.copy()
|
|
271
|
+
refined_generation["function_specification"] = refined_spec
|
|
272
|
+
refined_generation["refinement_iteration"] = original_generation.get("refinement_iteration", 0) + 1
|
|
273
|
+
refined_generation["refinement_reason"] = f"Issues found: {', '.join(issues[:3])}" # First 3 issues
|
|
274
|
+
|
|
275
|
+
# Update implementation plan
|
|
276
|
+
refined_plan = create_implementation_plan(refined_spec, original_generation.get("context_analysis", {}))
|
|
277
|
+
refined_generation["implementation_plan"] = refined_plan
|
|
278
|
+
|
|
279
|
+
return refined_generation
|
|
280
|
+
|
|
281
|
+
# Function to send a message to Unreal Engine via socket
|
|
282
|
+
# Falls back to Python Remote Execution or Web Remote Control if plugin socket unavailable
|
|
283
|
+
def send_to_unreal(command):
|
|
284
|
+
# Get Unreal Engine port from environment variable, default to 9878 (Unreal Engine's socket server)
|
|
285
|
+
unreal_port = int(os.environ.get('UNREAL_PORT', '9878'))
|
|
286
|
+
|
|
287
|
+
# Try 1: Plugin socket (fastest, full feature support)
|
|
288
|
+
try:
|
|
289
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
290
|
+
s.settimeout(5)
|
|
291
|
+
s.connect(('localhost', unreal_port))
|
|
292
|
+
|
|
293
|
+
json_str = json.dumps(command)
|
|
294
|
+
s.sendall(json_str.encode('utf-8'))
|
|
295
|
+
|
|
296
|
+
buffer_size = 8192
|
|
297
|
+
response_data = b""
|
|
298
|
+
|
|
299
|
+
while True:
|
|
300
|
+
chunk = s.recv(buffer_size)
|
|
301
|
+
if not chunk:
|
|
302
|
+
break
|
|
303
|
+
response_data += chunk
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
json.loads(response_data.decode('utf-8'))
|
|
307
|
+
break
|
|
308
|
+
except json.JSONDecodeError:
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
if response_data:
|
|
312
|
+
return json.loads(response_data.decode('utf-8'))
|
|
313
|
+
else:
|
|
314
|
+
return {"success": False, "error": "No response received"}
|
|
315
|
+
except ConnectionRefusedError:
|
|
316
|
+
print(f"Plugin socket unavailable (port {unreal_port}), trying fallback backends...", file=sys.stderr)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
# For non-connection errors on the plugin socket, still try fallback
|
|
319
|
+
if "Connection refused" in str(e) or "No connection" in str(e) or "WinError 10061" in str(e):
|
|
320
|
+
print(f"Plugin socket unavailable, trying fallback backends...", file=sys.stderr)
|
|
321
|
+
else:
|
|
322
|
+
print(f"Error sending to Unreal via plugin: {e}", file=sys.stderr)
|
|
323
|
+
return {"success": False, "error": str(e)}
|
|
324
|
+
|
|
325
|
+
# Try 2: Generate native Python and execute via Remote Execution or Web Remote Control
|
|
326
|
+
return _send_via_fallback(command)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _send_via_fallback(command):
|
|
330
|
+
"""Execute a command via UE's built-in Remote Execution or Web Remote Control."""
|
|
331
|
+
try:
|
|
332
|
+
from ue_native_handler import build_handler_script
|
|
333
|
+
except ImportError:
|
|
334
|
+
return {"success": False, "error": "Fallback handler not available. Install the CreatelexGenAI plugin or ensure ue_native_handler.py is in the Python path."}
|
|
335
|
+
|
|
336
|
+
script = build_handler_script(command)
|
|
337
|
+
|
|
338
|
+
# Try Web Remote Control API first (HTTP, simpler)
|
|
339
|
+
result = _try_web_remote_control(script)
|
|
340
|
+
if result is not None:
|
|
341
|
+
return result
|
|
342
|
+
|
|
343
|
+
# Try Python Remote Execution (UDP multicast)
|
|
344
|
+
result = _try_remote_execution(script)
|
|
345
|
+
if result is not None:
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
"success": False,
|
|
350
|
+
"error": "No connection to Unreal Engine. The plugin socket, Web Remote Control, and Python Remote Execution are all unavailable. Start UE with at least one enabled."
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _try_web_remote_control(script):
|
|
355
|
+
"""Try to execute a Python script via UE's Web Remote Control HTTP API."""
|
|
356
|
+
import urllib.request
|
|
357
|
+
import urllib.error
|
|
358
|
+
|
|
359
|
+
port = int(os.environ.get('WEB_REMOTE_PORT', '30010'))
|
|
360
|
+
url = f"http://127.0.0.1:{port}/remote/object/call"
|
|
361
|
+
|
|
362
|
+
payload = json.dumps({
|
|
363
|
+
"objectPath": "/Script/PythonScriptPlugin.Default__PythonScriptLibrary",
|
|
364
|
+
"functionName": "ExecutePythonCommandEx",
|
|
365
|
+
"parameters": {
|
|
366
|
+
"PythonCommand": script,
|
|
367
|
+
"ExecutionMode": "ExecuteStatement",
|
|
368
|
+
"FileExecutionScope": "Public"
|
|
369
|
+
}
|
|
370
|
+
}).encode('utf-8')
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="PUT")
|
|
374
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
375
|
+
data = json.loads(resp.read().decode('utf-8'))
|
|
376
|
+
# Try to extract the printed JSON result from output
|
|
377
|
+
output = str(data.get("ReturnValue", data.get("result", "")))
|
|
378
|
+
if output:
|
|
379
|
+
try:
|
|
380
|
+
return json.loads(output)
|
|
381
|
+
except json.JSONDecodeError:
|
|
382
|
+
pass
|
|
383
|
+
return data
|
|
384
|
+
except urllib.error.URLError:
|
|
385
|
+
return None
|
|
386
|
+
except Exception as e:
|
|
387
|
+
print(f"Web Remote Control failed: {e}", file=sys.stderr)
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _try_remote_execution(script):
|
|
392
|
+
"""Try to execute a Python script via UE's built-in Remote Execution protocol (UDP/TCP)."""
|
|
393
|
+
import threading
|
|
394
|
+
import queue
|
|
395
|
+
|
|
396
|
+
MULTICAST_GROUP = '239.0.0.1'
|
|
397
|
+
MULTICAST_PORT = 6766
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
import uuid
|
|
401
|
+
client_id = str(uuid.uuid4())
|
|
402
|
+
except ImportError:
|
|
403
|
+
import hashlib
|
|
404
|
+
client_id = hashlib.md5(str(time.time()).encode()).hexdigest()
|
|
405
|
+
|
|
406
|
+
result_queue = queue.Queue()
|
|
407
|
+
|
|
408
|
+
# Step 1: Start a TCP server to receive UE's connection
|
|
409
|
+
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
410
|
+
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
411
|
+
tcp_server.settimeout(10)
|
|
412
|
+
tcp_server.bind(('127.0.0.1', 0))
|
|
413
|
+
tcp_port = tcp_server.getsockname()[1]
|
|
414
|
+
tcp_server.listen(1)
|
|
415
|
+
|
|
416
|
+
def handle_connection():
|
|
417
|
+
try:
|
|
418
|
+
conn, addr = tcp_server.accept()
|
|
419
|
+
conn.settimeout(30)
|
|
420
|
+
|
|
421
|
+
# Send the command
|
|
422
|
+
cmd_msg = json.dumps({
|
|
423
|
+
"magic": "ue_py",
|
|
424
|
+
"version": 1,
|
|
425
|
+
"type": "command",
|
|
426
|
+
"source": client_id,
|
|
427
|
+
"command": script,
|
|
428
|
+
"unattended": False,
|
|
429
|
+
"exec_mode": "ExecuteStatement"
|
|
430
|
+
})
|
|
431
|
+
conn.sendall(cmd_msg.encode('utf-8'))
|
|
432
|
+
|
|
433
|
+
# Receive response
|
|
434
|
+
response_data = b""
|
|
435
|
+
while True:
|
|
436
|
+
try:
|
|
437
|
+
chunk = conn.recv(8192)
|
|
438
|
+
if not chunk:
|
|
439
|
+
break
|
|
440
|
+
response_data += chunk
|
|
441
|
+
try:
|
|
442
|
+
json.loads(response_data.decode('utf-8'))
|
|
443
|
+
break
|
|
444
|
+
except json.JSONDecodeError:
|
|
445
|
+
continue
|
|
446
|
+
except socket.timeout:
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
conn.close()
|
|
450
|
+
|
|
451
|
+
if response_data:
|
|
452
|
+
resp = json.loads(response_data.decode('utf-8'))
|
|
453
|
+
# Extract output from Remote Execution response
|
|
454
|
+
output_log = resp.get("output_log", [])
|
|
455
|
+
output_text = ""
|
|
456
|
+
for entry in output_log:
|
|
457
|
+
if isinstance(entry, dict):
|
|
458
|
+
output_text += entry.get("message", "") + "\n"
|
|
459
|
+
else:
|
|
460
|
+
output_text += str(entry) + "\n"
|
|
461
|
+
|
|
462
|
+
# Try to parse the output as JSON (our handler prints JSON)
|
|
463
|
+
for line in output_text.strip().split("\n"):
|
|
464
|
+
line = line.strip()
|
|
465
|
+
if line.startswith("{"):
|
|
466
|
+
try:
|
|
467
|
+
result_queue.put(json.loads(line))
|
|
468
|
+
return
|
|
469
|
+
except json.JSONDecodeError:
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
result_queue.put({"success": resp.get("success", False), "output": output_text.strip()})
|
|
473
|
+
else:
|
|
474
|
+
result_queue.put(None)
|
|
475
|
+
except Exception as e:
|
|
476
|
+
result_queue.put(None)
|
|
477
|
+
finally:
|
|
478
|
+
tcp_server.close()
|
|
479
|
+
|
|
480
|
+
# Start TCP handler in background thread
|
|
481
|
+
tcp_thread = threading.Thread(target=handle_connection, daemon=True)
|
|
482
|
+
tcp_thread.start()
|
|
483
|
+
|
|
484
|
+
# Step 2: Send open_connection via UDP multicast
|
|
485
|
+
try:
|
|
486
|
+
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
487
|
+
udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
488
|
+
udp_sock.settimeout(2)
|
|
489
|
+
|
|
490
|
+
open_msg = json.dumps({
|
|
491
|
+
"magic": "ue_py",
|
|
492
|
+
"version": 1,
|
|
493
|
+
"type": "open_connection",
|
|
494
|
+
"source": client_id,
|
|
495
|
+
"dest": "",
|
|
496
|
+
"command_ip": "127.0.0.1",
|
|
497
|
+
"command_port": tcp_port
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
udp_sock.sendto(open_msg.encode('utf-8'), (MULTICAST_GROUP, MULTICAST_PORT))
|
|
501
|
+
udp_sock.close()
|
|
502
|
+
except Exception as e:
|
|
503
|
+
tcp_server.close()
|
|
504
|
+
print(f"Remote Execution UDP failed: {e}", file=sys.stderr)
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
# Step 3: Wait for result
|
|
508
|
+
tcp_thread.join(timeout=15)
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
result = result_queue.get_nowait()
|
|
512
|
+
return result
|
|
513
|
+
except queue.Empty:
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
def _ensure_dict_response(response):
|
|
517
|
+
"""Normalize Unreal responses to dictionaries for cross-version compatibility."""
|
|
518
|
+
if isinstance(response, dict):
|
|
519
|
+
return response
|
|
520
|
+
if isinstance(response, str):
|
|
521
|
+
try:
|
|
522
|
+
return json.loads(response)
|
|
523
|
+
except json.JSONDecodeError as parse_err:
|
|
524
|
+
return {"success": False, "error": f"Invalid JSON response: {parse_err}", "raw": response}
|
|
525
|
+
return {"success": False, "error": "Unsupported response type from Unreal", "raw": repr(response)}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
_HOST_SLICE_IMPORT_MARKER = "__CREATELEX_HOST_SLICE_IMPORT_RESULT__"
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _response_text_blob(response: Dict[str, Any]) -> str:
|
|
532
|
+
if not isinstance(response, dict):
|
|
533
|
+
return str(response or "")
|
|
534
|
+
fields = [
|
|
535
|
+
response.get("error"),
|
|
536
|
+
response.get("message"),
|
|
537
|
+
response.get("details"),
|
|
538
|
+
response.get("raw"),
|
|
539
|
+
response.get("output"),
|
|
540
|
+
]
|
|
541
|
+
return " ".join(str(field) for field in fields if field)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _is_unreal_pillow_error(response: Dict[str, Any]) -> bool:
|
|
545
|
+
text = _response_text_blob(response).lower()
|
|
546
|
+
if "pillow" not in text:
|
|
547
|
+
return False
|
|
548
|
+
return any(
|
|
549
|
+
token in text
|
|
550
|
+
for token in [
|
|
551
|
+
"not available",
|
|
552
|
+
"install pillow",
|
|
553
|
+
"no module named",
|
|
554
|
+
"python imaging library",
|
|
555
|
+
]
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _extract_host_import_result(output: str) -> Optional[Dict[str, Any]]:
|
|
560
|
+
if not output:
|
|
561
|
+
return None
|
|
562
|
+
for line in output.splitlines():
|
|
563
|
+
stripped = line.strip()
|
|
564
|
+
if not stripped.startswith(_HOST_SLICE_IMPORT_MARKER):
|
|
565
|
+
continue
|
|
566
|
+
payload = stripped[len(_HOST_SLICE_IMPORT_MARKER):].strip()
|
|
567
|
+
if not payload:
|
|
568
|
+
continue
|
|
569
|
+
try:
|
|
570
|
+
parsed = json.loads(payload)
|
|
571
|
+
if isinstance(parsed, dict):
|
|
572
|
+
return parsed
|
|
573
|
+
except json.JSONDecodeError:
|
|
574
|
+
return None
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _build_unreal_import_script(slice_entries: List[Dict[str, str]], destination_folder: str) -> str:
|
|
579
|
+
entries_json = json.dumps(slice_entries, separators=(",", ":"), ensure_ascii=False)
|
|
580
|
+
return "\n".join(
|
|
581
|
+
[
|
|
582
|
+
"import json",
|
|
583
|
+
"import unreal",
|
|
584
|
+
f"entries = json.loads({json.dumps(entries_json)})",
|
|
585
|
+
f"destination_folder = {json.dumps(destination_folder)}",
|
|
586
|
+
f"result_marker = {json.dumps(_HOST_SLICE_IMPORT_MARKER)}",
|
|
587
|
+
"asset_tools = unreal.AssetToolsHelpers.get_asset_tools()",
|
|
588
|
+
"imported = []",
|
|
589
|
+
"failures = []",
|
|
590
|
+
"for entry in entries:",
|
|
591
|
+
" name = str(entry.get('name') or '').strip()",
|
|
592
|
+
" file_path = str(entry.get('file_path') or '').strip()",
|
|
593
|
+
" if not name or not file_path:",
|
|
594
|
+
" failures.append({'name': name, 'file_path': file_path, 'error': 'Missing name or file_path'})",
|
|
595
|
+
" continue",
|
|
596
|
+
" task = unreal.AssetImportTask()",
|
|
597
|
+
" task.filename = file_path",
|
|
598
|
+
" task.destination_path = destination_folder",
|
|
599
|
+
" task.destination_name = name",
|
|
600
|
+
" task.automated = True",
|
|
601
|
+
" task.replace_existing = True",
|
|
602
|
+
" task.save = True",
|
|
603
|
+
" try:",
|
|
604
|
+
" asset_tools.import_asset_tasks([task])",
|
|
605
|
+
" except Exception as import_error:",
|
|
606
|
+
" failures.append({'name': name, 'file_path': file_path, 'error': str(import_error)})",
|
|
607
|
+
" continue",
|
|
608
|
+
" imported_paths = [str(path) for path in (task.imported_object_paths or []) if path]",
|
|
609
|
+
" asset_path = imported_paths[0] if imported_paths else ''",
|
|
610
|
+
" if asset_path:",
|
|
611
|
+
" imported.append({'name': name, 'asset_path': asset_path, 'imported_object_paths': imported_paths})",
|
|
612
|
+
" else:",
|
|
613
|
+
" failures.append({'name': name, 'file_path': file_path, 'error': 'Import succeeded with no object path'})",
|
|
614
|
+
"result = {'success': len(imported) > 0, 'imported': imported, 'failures': failures}",
|
|
615
|
+
"print(result_marker + json.dumps(result, separators=(',', ':')))",
|
|
616
|
+
]
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _get_manifest_component_slices(manifest: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
621
|
+
if not isinstance(manifest, dict):
|
|
622
|
+
return []
|
|
623
|
+
slices = manifest.get("slices", [])
|
|
624
|
+
if not isinstance(slices, list):
|
|
625
|
+
return []
|
|
626
|
+
return [
|
|
627
|
+
slice_info
|
|
628
|
+
for slice_info in slices
|
|
629
|
+
if isinstance(slice_info, dict) and str(slice_info.get("kind") or "").lower() != "full_mockup"
|
|
630
|
+
]
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _manifest_component_count(manifest: Dict[str, Any]) -> int:
|
|
634
|
+
return len(_get_manifest_component_slices(manifest))
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _should_retry_aggressive_slice(manifest: Dict[str, Any]) -> bool:
|
|
638
|
+
# If "auto" effectively produced only a background/full-image result, force one aggressive retry.
|
|
639
|
+
return _manifest_component_count(manifest) <= 1
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _pick_better_slice_response(
|
|
643
|
+
primary_response: Dict[str, Any],
|
|
644
|
+
aggressive_response: Dict[str, Any],
|
|
645
|
+
) -> Dict[str, Any]:
|
|
646
|
+
primary_manifest = primary_response.get("manifest", {}) if isinstance(primary_response, dict) else {}
|
|
647
|
+
aggressive_manifest = aggressive_response.get("manifest", {}) if isinstance(aggressive_response, dict) else {}
|
|
648
|
+
if _manifest_component_count(aggressive_manifest) > _manifest_component_count(primary_manifest):
|
|
649
|
+
aggressive_response["auto_retry_aggressive"] = True
|
|
650
|
+
aggressive_response["retry_reason"] = (
|
|
651
|
+
"Initial auto segmentation yielded too few component slices; aggressive retry selected."
|
|
652
|
+
)
|
|
653
|
+
return aggressive_response
|
|
654
|
+
return primary_response
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _run_host_slice_import(manifest: Dict[str, Any], destination_folder: str) -> Dict[str, Any]:
|
|
658
|
+
slices = manifest.get("slices", []) if isinstance(manifest, dict) else []
|
|
659
|
+
if not isinstance(slices, list) or not slices:
|
|
660
|
+
return {"success": False, "error": "Host slicing manifest is empty."}
|
|
661
|
+
|
|
662
|
+
slice_entries: List[Dict[str, str]] = []
|
|
663
|
+
for slice_info in slices:
|
|
664
|
+
if not isinstance(slice_info, dict):
|
|
665
|
+
continue
|
|
666
|
+
name = str(slice_info.get("name") or "").strip()
|
|
667
|
+
file_path = str(slice_info.get("file_path") or "").strip()
|
|
668
|
+
if name and file_path:
|
|
669
|
+
slice_entries.append({"name": name, "file_path": file_path})
|
|
670
|
+
|
|
671
|
+
if not slice_entries:
|
|
672
|
+
return {"success": False, "error": "Host slicing produced no valid files to import."}
|
|
673
|
+
|
|
674
|
+
script = _build_unreal_import_script(slice_entries, destination_folder)
|
|
675
|
+
import_response = _ensure_dict_response(send_to_unreal({"type": "execute_python", "script": script}))
|
|
676
|
+
if not import_response.get("success"):
|
|
677
|
+
return {
|
|
678
|
+
"success": False,
|
|
679
|
+
"error": f"Failed to import host-generated slices in Unreal: {_response_text_blob(import_response)}",
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
import_result = _extract_host_import_result(import_response.get("output", ""))
|
|
683
|
+
if not import_result:
|
|
684
|
+
return {
|
|
685
|
+
"success": False,
|
|
686
|
+
"error": "Unreal import script ran, but no parseable import result marker was returned.",
|
|
687
|
+
"output": import_response.get("output", ""),
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
imported_map = {
|
|
691
|
+
str(item.get("name")): str(item.get("asset_path"))
|
|
692
|
+
for item in import_result.get("imported", [])
|
|
693
|
+
if isinstance(item, dict) and item.get("name") and item.get("asset_path")
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
imported_count = 0
|
|
697
|
+
for slice_info in slices:
|
|
698
|
+
if not isinstance(slice_info, dict):
|
|
699
|
+
continue
|
|
700
|
+
asset_path = imported_map.get(str(slice_info.get("name")))
|
|
701
|
+
if asset_path:
|
|
702
|
+
slice_info["asset_path"] = asset_path
|
|
703
|
+
imported_count += 1
|
|
704
|
+
|
|
705
|
+
if imported_count <= 0:
|
|
706
|
+
return {
|
|
707
|
+
"success": False,
|
|
708
|
+
"error": "Host slices were generated but no assets were imported into Unreal.",
|
|
709
|
+
"import_result": import_result,
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
non_fallback_assets = [
|
|
713
|
+
slice_info.get("asset_path")
|
|
714
|
+
for slice_info in slices
|
|
715
|
+
if isinstance(slice_info, dict) and slice_info.get("asset_path") and not slice_info.get("is_fallback")
|
|
716
|
+
]
|
|
717
|
+
any_assets = [
|
|
718
|
+
slice_info.get("asset_path")
|
|
719
|
+
for slice_info in slices
|
|
720
|
+
if isinstance(slice_info, dict) and slice_info.get("asset_path")
|
|
721
|
+
]
|
|
722
|
+
manifest["background_asset_path"] = (
|
|
723
|
+
non_fallback_assets[0] if non_fallback_assets else (any_assets[0] if any_assets else "")
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
"success": True,
|
|
728
|
+
"imported_count": imported_count,
|
|
729
|
+
"import_result": import_result,
|
|
730
|
+
"background_asset_path": manifest.get("background_asset_path", ""),
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _run_auto_slice_with_fallback(
|
|
735
|
+
source_image_path: str,
|
|
736
|
+
destination_folder: str,
|
|
737
|
+
base_name: str,
|
|
738
|
+
max_slices: int,
|
|
739
|
+
min_component_area_ratio: float,
|
|
740
|
+
background_tolerance: int,
|
|
741
|
+
slice_mode: str,
|
|
742
|
+
include_full_image: bool,
|
|
743
|
+
enable_chroma_key: bool = True,
|
|
744
|
+
chroma_tolerance: int = 24,
|
|
745
|
+
enable_host_fallback: bool = True,
|
|
746
|
+
) -> Dict[str, Any]:
|
|
747
|
+
command = {
|
|
748
|
+
"type": "auto_slice_ui_mockup",
|
|
749
|
+
"source_image_path": source_image_path,
|
|
750
|
+
"destination_folder": destination_folder,
|
|
751
|
+
"base_name": base_name,
|
|
752
|
+
"max_slices": max_slices,
|
|
753
|
+
"min_component_area_ratio": min_component_area_ratio,
|
|
754
|
+
"background_tolerance": background_tolerance,
|
|
755
|
+
"slice_mode": slice_mode,
|
|
756
|
+
"include_full_image": include_full_image,
|
|
757
|
+
"enable_chroma_key": enable_chroma_key,
|
|
758
|
+
"chroma_tolerance": chroma_tolerance,
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
unreal_response = _ensure_dict_response(send_to_unreal(command))
|
|
762
|
+
if unreal_response.get("success"):
|
|
763
|
+
if slice_mode == "auto":
|
|
764
|
+
initial_manifest = unreal_response.get("manifest", {})
|
|
765
|
+
if _should_retry_aggressive_slice(initial_manifest):
|
|
766
|
+
aggressive_command = dict(command)
|
|
767
|
+
aggressive_command["slice_mode"] = "aggressive"
|
|
768
|
+
aggressive_response = _ensure_dict_response(send_to_unreal(aggressive_command))
|
|
769
|
+
if aggressive_response.get("success"):
|
|
770
|
+
return _pick_better_slice_response(unreal_response, aggressive_response)
|
|
771
|
+
return unreal_response
|
|
772
|
+
if not enable_host_fallback:
|
|
773
|
+
return unreal_response
|
|
774
|
+
if not _is_unreal_pillow_error(unreal_response):
|
|
775
|
+
return unreal_response
|
|
776
|
+
|
|
777
|
+
if auto_slice_ui_mockup_host is None:
|
|
778
|
+
return {
|
|
779
|
+
"success": False,
|
|
780
|
+
"error": (
|
|
781
|
+
"Unreal auto-slice failed because Pillow is missing, and host fallback module is unavailable. "
|
|
782
|
+
f"Original error: {_response_text_blob(unreal_response)}"
|
|
783
|
+
),
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
host_has_pillow, host_pillow_error = host_slice_has_pillow()
|
|
787
|
+
if not host_has_pillow:
|
|
788
|
+
return {
|
|
789
|
+
"success": False,
|
|
790
|
+
"error": (
|
|
791
|
+
"Unreal auto-slice failed because Pillow is missing, and host fallback also lacks Pillow. "
|
|
792
|
+
f"Host import error: {host_pillow_error}"
|
|
793
|
+
),
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
host_result = auto_slice_ui_mockup_host(
|
|
797
|
+
source_image_path=source_image_path,
|
|
798
|
+
destination_folder=destination_folder,
|
|
799
|
+
base_name=base_name,
|
|
800
|
+
max_slices=max_slices,
|
|
801
|
+
min_component_area_ratio=min_component_area_ratio,
|
|
802
|
+
background_tolerance=background_tolerance,
|
|
803
|
+
include_full_image=include_full_image,
|
|
804
|
+
slice_mode=slice_mode,
|
|
805
|
+
enable_chroma_key=enable_chroma_key,
|
|
806
|
+
chroma_tolerance=chroma_tolerance,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
temp_dirs = set()
|
|
810
|
+
if isinstance(host_result, dict) and host_result.get("temp_dir"):
|
|
811
|
+
temp_dirs.add(str(host_result.get("temp_dir")))
|
|
812
|
+
try:
|
|
813
|
+
if not isinstance(host_result, dict) or not host_result.get("success"):
|
|
814
|
+
host_error = (
|
|
815
|
+
host_result.get("error", "Unknown host slicing error.")
|
|
816
|
+
if isinstance(host_result, dict)
|
|
817
|
+
else "Unknown host slicing error."
|
|
818
|
+
)
|
|
819
|
+
return {
|
|
820
|
+
"success": False,
|
|
821
|
+
"error": (
|
|
822
|
+
"Unreal auto-slice failed because Pillow is missing, and host fallback failed. "
|
|
823
|
+
f"Unreal error: {_response_text_blob(unreal_response)} Host error: {host_error}"
|
|
824
|
+
),
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if slice_mode == "auto":
|
|
828
|
+
initial_manifest = host_result.get("manifest", {}) if isinstance(host_result, dict) else {}
|
|
829
|
+
if _should_retry_aggressive_slice(initial_manifest):
|
|
830
|
+
aggressive_host_result = auto_slice_ui_mockup_host(
|
|
831
|
+
source_image_path=source_image_path,
|
|
832
|
+
destination_folder=destination_folder,
|
|
833
|
+
base_name=base_name,
|
|
834
|
+
max_slices=max_slices,
|
|
835
|
+
min_component_area_ratio=min_component_area_ratio,
|
|
836
|
+
background_tolerance=background_tolerance,
|
|
837
|
+
include_full_image=include_full_image,
|
|
838
|
+
slice_mode="aggressive",
|
|
839
|
+
enable_chroma_key=enable_chroma_key,
|
|
840
|
+
chroma_tolerance=chroma_tolerance,
|
|
841
|
+
)
|
|
842
|
+
if isinstance(aggressive_host_result, dict) and aggressive_host_result.get("temp_dir"):
|
|
843
|
+
temp_dirs.add(str(aggressive_host_result.get("temp_dir")))
|
|
844
|
+
if isinstance(aggressive_host_result, dict) and aggressive_host_result.get("success"):
|
|
845
|
+
host_result = _pick_better_slice_response(host_result, aggressive_host_result)
|
|
846
|
+
|
|
847
|
+
manifest = host_result.get("manifest")
|
|
848
|
+
if not isinstance(manifest, dict):
|
|
849
|
+
return {"success": False, "error": "Host fallback returned no valid manifest."}
|
|
850
|
+
|
|
851
|
+
import_result = _run_host_slice_import(manifest, destination_folder)
|
|
852
|
+
if not import_result.get("success"):
|
|
853
|
+
return {
|
|
854
|
+
"success": False,
|
|
855
|
+
"error": (
|
|
856
|
+
"Host slicing succeeded but Unreal import failed during fallback. "
|
|
857
|
+
f"{import_result.get('error', 'Unknown import error.')}"
|
|
858
|
+
),
|
|
859
|
+
"fallback_reason": _response_text_blob(unreal_response),
|
|
860
|
+
"host_import_result": import_result.get("import_result"),
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return {
|
|
864
|
+
"success": True,
|
|
865
|
+
"message": (
|
|
866
|
+
f"Generated and imported {import_result.get('imported_count', 0)} texture assets via host fallback."
|
|
867
|
+
),
|
|
868
|
+
"slice_count": int(host_result.get("slice_count", import_result.get("imported_count", 0))),
|
|
869
|
+
"background_asset_path": import_result.get("background_asset_path", ""),
|
|
870
|
+
"segmentation_mode": host_result.get("segmentation_mode", ""),
|
|
871
|
+
"segmentation_metrics": host_result.get("segmentation_metrics", {}),
|
|
872
|
+
"manifest": manifest,
|
|
873
|
+
"host_fallback_used": True,
|
|
874
|
+
"fallback_reason": _response_text_blob(unreal_response),
|
|
875
|
+
}
|
|
876
|
+
finally:
|
|
877
|
+
for temp_dir in temp_dirs:
|
|
878
|
+
if temp_dir and os.path.isdir(temp_dir):
|
|
879
|
+
try:
|
|
880
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
881
|
+
except Exception:
|
|
882
|
+
pass
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _collect_widget_nodes(hierarchy_node: Optional[Dict[str, Any]], out_nodes: List[Dict[str, str]]) -> None:
|
|
886
|
+
if not isinstance(hierarchy_node, dict):
|
|
887
|
+
return
|
|
888
|
+
widget_name = str(hierarchy_node.get("name") or "").strip()
|
|
889
|
+
widget_class = str(hierarchy_node.get("class") or "").strip()
|
|
890
|
+
if widget_name:
|
|
891
|
+
out_nodes.append({"name": widget_name, "class": widget_class})
|
|
892
|
+
|
|
893
|
+
children = hierarchy_node.get("children", [])
|
|
894
|
+
if not isinstance(children, list):
|
|
895
|
+
return
|
|
896
|
+
for child in children:
|
|
897
|
+
child_obj = child
|
|
898
|
+
if isinstance(child, dict) and "Object" in child and isinstance(child.get("Object"), dict):
|
|
899
|
+
child_obj = child.get("Object")
|
|
900
|
+
_collect_widget_nodes(child_obj, out_nodes)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def _is_panel_class_name(widget_class_name: str) -> bool:
|
|
904
|
+
class_lc = (widget_class_name or "").lower()
|
|
905
|
+
return any(
|
|
906
|
+
token in class_lc
|
|
907
|
+
for token in [
|
|
908
|
+
"panel",
|
|
909
|
+
"canvas",
|
|
910
|
+
"verticalbox",
|
|
911
|
+
"horizontalbox",
|
|
912
|
+
"overlay",
|
|
913
|
+
"grid",
|
|
914
|
+
"wrapbox",
|
|
915
|
+
"scrollbox",
|
|
916
|
+
"sizebox",
|
|
917
|
+
"border",
|
|
918
|
+
"namedslot",
|
|
919
|
+
"widgetswitcher",
|
|
920
|
+
]
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def _is_button_like_slice_manifest_entry(slice_info: Dict[str, Any], manifest: Dict[str, Any]) -> bool:
|
|
925
|
+
if not isinstance(slice_info, dict):
|
|
926
|
+
return False
|
|
927
|
+
|
|
928
|
+
kind = str(slice_info.get("kind") or "").lower()
|
|
929
|
+
label = str(slice_info.get("label") or "").lower()
|
|
930
|
+
name = str(slice_info.get("name") or "").lower()
|
|
931
|
+
if "button" in kind or "button" in label or "button" in name:
|
|
932
|
+
return True
|
|
933
|
+
|
|
934
|
+
source_width = float(manifest.get("source_width") or 0.0)
|
|
935
|
+
source_height = float(manifest.get("source_height") or 0.0)
|
|
936
|
+
bbox = slice_info.get("bbox", [])
|
|
937
|
+
if (
|
|
938
|
+
source_width <= 0.0
|
|
939
|
+
or source_height <= 0.0
|
|
940
|
+
or not isinstance(bbox, list)
|
|
941
|
+
or len(bbox) != 4
|
|
942
|
+
):
|
|
943
|
+
return False
|
|
944
|
+
|
|
945
|
+
try:
|
|
946
|
+
min_x, min_y, max_x, max_y = [float(value) for value in bbox]
|
|
947
|
+
except (TypeError, ValueError):
|
|
948
|
+
return False
|
|
949
|
+
|
|
950
|
+
width = max(1.0, max_x - min_x + 1.0)
|
|
951
|
+
height = max(1.0, max_y - min_y + 1.0)
|
|
952
|
+
width_ratio = width / source_width
|
|
953
|
+
height_ratio = height / source_height
|
|
954
|
+
area_ratio = (width * height) / max(1.0, source_width * source_height)
|
|
955
|
+
center_y_ratio = ((min_y + max_y) * 0.5) / source_height
|
|
956
|
+
aspect_ratio = width / max(1.0, height)
|
|
957
|
+
|
|
958
|
+
return (
|
|
959
|
+
width_ratio >= 0.15
|
|
960
|
+
and width_ratio <= 0.92
|
|
961
|
+
and height_ratio >= 0.03
|
|
962
|
+
and height_ratio <= 0.25
|
|
963
|
+
and area_ratio <= 0.20
|
|
964
|
+
and center_y_ratio >= 0.40
|
|
965
|
+
and aspect_ratio >= 1.5
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _button_slice_priority(slice_info: Dict[str, Any], manifest: Dict[str, Any]) -> tuple:
|
|
970
|
+
kind = str(slice_info.get("kind") or "").lower()
|
|
971
|
+
explicit_priority = 0
|
|
972
|
+
if kind == "button_primary":
|
|
973
|
+
explicit_priority = 3
|
|
974
|
+
elif kind == "button_secondary":
|
|
975
|
+
explicit_priority = 2
|
|
976
|
+
elif "button" in kind:
|
|
977
|
+
explicit_priority = 1
|
|
978
|
+
|
|
979
|
+
area = 0.0
|
|
980
|
+
try:
|
|
981
|
+
area = float(slice_info.get("area") or 0.0)
|
|
982
|
+
except (TypeError, ValueError):
|
|
983
|
+
area = 0.0
|
|
984
|
+
|
|
985
|
+
is_heuristic_button = 1 if _is_button_like_slice_manifest_entry(slice_info, manifest) else 0
|
|
986
|
+
return (explicit_priority, is_heuristic_button, area)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _is_background_like_slice_manifest_entry(slice_info: Dict[str, Any]) -> bool:
|
|
990
|
+
if not isinstance(slice_info, dict):
|
|
991
|
+
return False
|
|
992
|
+
kind = str(slice_info.get("kind") or "").lower()
|
|
993
|
+
label = str(slice_info.get("label") or "").lower()
|
|
994
|
+
return kind in {"full_mockup", "panel_main", "header"} or any(
|
|
995
|
+
token in kind or token in label
|
|
996
|
+
for token in ["background", "panel", "header", "full"]
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _preferred_widget_base_name(slice_info: Dict[str, Any], manifest: Dict[str, Any]) -> str:
|
|
1001
|
+
kind = str(slice_info.get("kind") or "").lower()
|
|
1002
|
+
label = str(slice_info.get("label") or "").lower()
|
|
1003
|
+
if _is_button_like_slice_manifest_entry(slice_info, manifest):
|
|
1004
|
+
if "primary" in kind or "primary" in label:
|
|
1005
|
+
return "PrimaryButton"
|
|
1006
|
+
if "secondary" in kind or "secondary" in label:
|
|
1007
|
+
return "SecondaryButton"
|
|
1008
|
+
return "ActionButton"
|
|
1009
|
+
if _is_background_like_slice_manifest_entry(slice_info):
|
|
1010
|
+
if "header" in kind or "header" in label:
|
|
1011
|
+
return "HeaderImage"
|
|
1012
|
+
if "panel" in kind or "panel" in label:
|
|
1013
|
+
return "MainPanelImage"
|
|
1014
|
+
return "BackgroundImage"
|
|
1015
|
+
if "icon_left" in kind:
|
|
1016
|
+
return "LeftIconImage"
|
|
1017
|
+
if "icon_right" in kind:
|
|
1018
|
+
return "RightIconImage"
|
|
1019
|
+
if "icon" in kind or "icon" in label:
|
|
1020
|
+
return "IconImage"
|
|
1021
|
+
if "star" in kind or "star" in label:
|
|
1022
|
+
return "StarImage"
|
|
1023
|
+
return "SliceImage"
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _make_unique_widget_name(base_name: str, existing_names: set) -> str:
|
|
1027
|
+
candidate = base_name
|
|
1028
|
+
suffix = 1
|
|
1029
|
+
while candidate in existing_names:
|
|
1030
|
+
suffix += 1
|
|
1031
|
+
candidate = f"{base_name}_{suffix}"
|
|
1032
|
+
return candidate
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _ensure_slice_target_widgets_for_manifest(
|
|
1036
|
+
user_widget_path: str,
|
|
1037
|
+
manifest: Dict[str, Any],
|
|
1038
|
+
max_auto_components: int = 12,
|
|
1039
|
+
max_auto_buttons: int = 3,
|
|
1040
|
+
) -> Dict[str, Any]:
|
|
1041
|
+
if not isinstance(manifest, dict):
|
|
1042
|
+
return {"success": False, "error": "Invalid manifest for target widget provisioning."}
|
|
1043
|
+
|
|
1044
|
+
hierarchy_response = _ensure_dict_response(
|
|
1045
|
+
send_to_unreal({"type": "get_widget_hierarchy", "user_widget_path": user_widget_path})
|
|
1046
|
+
)
|
|
1047
|
+
if not hierarchy_response.get("success"):
|
|
1048
|
+
return {
|
|
1049
|
+
"success": False,
|
|
1050
|
+
"error": (
|
|
1051
|
+
"Failed to inspect widget hierarchy before target provisioning: "
|
|
1052
|
+
f"{hierarchy_response.get('error', 'Unknown error')}"
|
|
1053
|
+
),
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
hierarchy_root = hierarchy_response.get("hierarchy")
|
|
1057
|
+
widget_nodes: List[Dict[str, str]] = []
|
|
1058
|
+
_collect_widget_nodes(hierarchy_root, widget_nodes)
|
|
1059
|
+
|
|
1060
|
+
existing_names = {node.get("name", "") for node in widget_nodes if node.get("name")}
|
|
1061
|
+
existing_image_names = [
|
|
1062
|
+
node.get("name", "")
|
|
1063
|
+
for node in widget_nodes
|
|
1064
|
+
if "image" in (node.get("class", "").lower())
|
|
1065
|
+
]
|
|
1066
|
+
existing_button_names = [
|
|
1067
|
+
node.get("name", "")
|
|
1068
|
+
for node in widget_nodes
|
|
1069
|
+
if "button" in (node.get("class", "").lower())
|
|
1070
|
+
]
|
|
1071
|
+
background_like_images = [
|
|
1072
|
+
name
|
|
1073
|
+
for name in existing_image_names
|
|
1074
|
+
if any(token in name.lower() for token in ["background", "bg", "frame", "panel", "root"])
|
|
1075
|
+
]
|
|
1076
|
+
component_image_budget = max(0, len(existing_image_names) - len(background_like_images))
|
|
1077
|
+
|
|
1078
|
+
root_name = ""
|
|
1079
|
+
root_class = ""
|
|
1080
|
+
if isinstance(hierarchy_root, dict):
|
|
1081
|
+
root_name = str(hierarchy_root.get("name") or "").strip()
|
|
1082
|
+
root_class = str(hierarchy_root.get("class") or "").strip()
|
|
1083
|
+
parent_widget_name = root_name if (root_name and _is_panel_class_name(root_class)) else ""
|
|
1084
|
+
|
|
1085
|
+
slices = manifest.get("slices", [])
|
|
1086
|
+
if not isinstance(slices, list) or not slices:
|
|
1087
|
+
return {"success": True, "added": [], "message": "No slices in manifest."}
|
|
1088
|
+
|
|
1089
|
+
component_slices = [
|
|
1090
|
+
slice_info
|
|
1091
|
+
for slice_info in slices
|
|
1092
|
+
if isinstance(slice_info, dict) and str(slice_info.get("kind") or "").lower() != "full_mockup"
|
|
1093
|
+
]
|
|
1094
|
+
component_slices.sort(
|
|
1095
|
+
key=lambda entry: float(entry.get("area", 0) or 0),
|
|
1096
|
+
reverse=True,
|
|
1097
|
+
)
|
|
1098
|
+
if max_auto_components > 0:
|
|
1099
|
+
component_slices = component_slices[:max_auto_components]
|
|
1100
|
+
|
|
1101
|
+
add_results = []
|
|
1102
|
+
add_failures = []
|
|
1103
|
+
|
|
1104
|
+
def add_widget(widget_type: str, base_name: str) -> Optional[str]:
|
|
1105
|
+
nonlocal add_results, add_failures
|
|
1106
|
+
unique_name = _make_unique_widget_name(base_name, existing_names)
|
|
1107
|
+
add_command = {
|
|
1108
|
+
"type": "add_widget_to_user_widget",
|
|
1109
|
+
"user_widget_path": user_widget_path,
|
|
1110
|
+
"widget_type": widget_type,
|
|
1111
|
+
"widget_name": unique_name,
|
|
1112
|
+
"parent_widget_name": parent_widget_name,
|
|
1113
|
+
}
|
|
1114
|
+
add_response = _ensure_dict_response(send_to_unreal(add_command))
|
|
1115
|
+
if add_response.get("success"):
|
|
1116
|
+
actual_name = str(add_response.get("widget_name") or unique_name)
|
|
1117
|
+
existing_names.add(actual_name)
|
|
1118
|
+
add_results.append({"widget_type": widget_type, "widget_name": actual_name})
|
|
1119
|
+
return actual_name
|
|
1120
|
+
add_failures.append(
|
|
1121
|
+
{
|
|
1122
|
+
"widget_type": widget_type,
|
|
1123
|
+
"requested_name": unique_name,
|
|
1124
|
+
"error": add_response.get("error", "Unknown error"),
|
|
1125
|
+
}
|
|
1126
|
+
)
|
|
1127
|
+
return None
|
|
1128
|
+
|
|
1129
|
+
if not existing_image_names:
|
|
1130
|
+
background_name = "BackgroundImage"
|
|
1131
|
+
add_widget("Image", background_name)
|
|
1132
|
+
existing_image_names = [entry["widget_name"] for entry in add_results if entry["widget_type"] == "Image"]
|
|
1133
|
+
|
|
1134
|
+
created_button_count = 0
|
|
1135
|
+
for slice_info in component_slices:
|
|
1136
|
+
is_button_slice = _is_button_like_slice_manifest_entry(slice_info, manifest)
|
|
1137
|
+
if is_button_slice:
|
|
1138
|
+
if existing_button_names:
|
|
1139
|
+
existing_button_names.pop(0)
|
|
1140
|
+
continue
|
|
1141
|
+
if created_button_count >= max_auto_buttons:
|
|
1142
|
+
continue
|
|
1143
|
+
base_name = _preferred_widget_base_name(slice_info, manifest)
|
|
1144
|
+
created_name = add_widget("Button", base_name)
|
|
1145
|
+
if created_name:
|
|
1146
|
+
created_button_count += 1
|
|
1147
|
+
continue
|
|
1148
|
+
|
|
1149
|
+
if component_image_budget > 0:
|
|
1150
|
+
component_image_budget -= 1
|
|
1151
|
+
continue
|
|
1152
|
+
base_name = _preferred_widget_base_name(slice_info, manifest)
|
|
1153
|
+
created_name = add_widget("Image", base_name)
|
|
1154
|
+
if created_name:
|
|
1155
|
+
component_image_budget += 0
|
|
1156
|
+
|
|
1157
|
+
return {
|
|
1158
|
+
"success": len(add_failures) == 0,
|
|
1159
|
+
"added": add_results,
|
|
1160
|
+
"failures": add_failures,
|
|
1161
|
+
"existing_image_count": len(existing_image_names),
|
|
1162
|
+
"existing_button_count": len(
|
|
1163
|
+
[
|
|
1164
|
+
node.get("name", "")
|
|
1165
|
+
for node in widget_nodes
|
|
1166
|
+
if "button" in (node.get("class", "").lower())
|
|
1167
|
+
]
|
|
1168
|
+
),
|
|
1169
|
+
"parent_widget_name": parent_widget_name,
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _ensure_button_widgets_for_manifest(
|
|
1174
|
+
user_widget_path: str,
|
|
1175
|
+
manifest: Dict[str, Any],
|
|
1176
|
+
max_auto_buttons: int = 2,
|
|
1177
|
+
) -> Dict[str, Any]:
|
|
1178
|
+
if not isinstance(manifest, dict):
|
|
1179
|
+
return {"success": False, "error": "Invalid manifest for button provisioning."}
|
|
1180
|
+
if max_auto_buttons <= 0:
|
|
1181
|
+
return {"success": True, "added": [], "message": "Button auto-provisioning disabled."}
|
|
1182
|
+
|
|
1183
|
+
hierarchy_response = _ensure_dict_response(
|
|
1184
|
+
send_to_unreal({"type": "get_widget_hierarchy", "user_widget_path": user_widget_path})
|
|
1185
|
+
)
|
|
1186
|
+
if not hierarchy_response.get("success"):
|
|
1187
|
+
return {
|
|
1188
|
+
"success": False,
|
|
1189
|
+
"error": (
|
|
1190
|
+
f"Failed to inspect widget hierarchy before button provisioning: "
|
|
1191
|
+
f"{hierarchy_response.get('error', 'Unknown error')}"
|
|
1192
|
+
),
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
hierarchy_root = hierarchy_response.get("hierarchy")
|
|
1196
|
+
widget_nodes: List[Dict[str, str]] = []
|
|
1197
|
+
_collect_widget_nodes(hierarchy_root, widget_nodes)
|
|
1198
|
+
|
|
1199
|
+
existing_names = {node.get("name", "") for node in widget_nodes if node.get("name")}
|
|
1200
|
+
existing_button_names = [
|
|
1201
|
+
node.get("name", "")
|
|
1202
|
+
for node in widget_nodes
|
|
1203
|
+
if "button" in (node.get("class", "").lower())
|
|
1204
|
+
]
|
|
1205
|
+
|
|
1206
|
+
slices = manifest.get("slices", [])
|
|
1207
|
+
if not isinstance(slices, list) or not slices:
|
|
1208
|
+
return {"success": True, "added": [], "message": "No slices in manifest."}
|
|
1209
|
+
|
|
1210
|
+
button_candidates = [
|
|
1211
|
+
slice_info
|
|
1212
|
+
for slice_info in slices
|
|
1213
|
+
if isinstance(slice_info, dict)
|
|
1214
|
+
and str(slice_info.get("kind") or "").lower() != "full_mockup"
|
|
1215
|
+
and _is_button_like_slice_manifest_entry(slice_info, manifest)
|
|
1216
|
+
]
|
|
1217
|
+
if not button_candidates:
|
|
1218
|
+
return {"success": True, "added": [], "message": "No button-like slices detected."}
|
|
1219
|
+
|
|
1220
|
+
button_candidates.sort(
|
|
1221
|
+
key=lambda entry: _button_slice_priority(entry, manifest),
|
|
1222
|
+
reverse=True,
|
|
1223
|
+
)
|
|
1224
|
+
desired_button_count = min(max_auto_buttons, len(button_candidates))
|
|
1225
|
+
if len(existing_button_names) >= desired_button_count:
|
|
1226
|
+
return {
|
|
1227
|
+
"success": True,
|
|
1228
|
+
"added": [],
|
|
1229
|
+
"message": (
|
|
1230
|
+
f"Button widgets already present ({len(existing_button_names)}), no provisioning needed."
|
|
1231
|
+
),
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
root_name = ""
|
|
1235
|
+
root_class = ""
|
|
1236
|
+
if isinstance(hierarchy_root, dict):
|
|
1237
|
+
root_name = str(hierarchy_root.get("name") or "").strip()
|
|
1238
|
+
root_class = str(hierarchy_root.get("class") or "").strip()
|
|
1239
|
+
parent_widget_name = root_name if (root_name and _is_panel_class_name(root_class)) else ""
|
|
1240
|
+
|
|
1241
|
+
preferred_names = ["PrimaryButton", "SecondaryButton", "TertiaryButton", "QuaternaryButton"]
|
|
1242
|
+
added = []
|
|
1243
|
+
provision_failures = []
|
|
1244
|
+
needed = max(0, desired_button_count - len(existing_button_names))
|
|
1245
|
+
for index in range(needed):
|
|
1246
|
+
base_name = preferred_names[index] if index < len(preferred_names) else f"AutoButton{index + 1}"
|
|
1247
|
+
candidate_name = base_name
|
|
1248
|
+
suffix = 1
|
|
1249
|
+
while candidate_name in existing_names:
|
|
1250
|
+
suffix += 1
|
|
1251
|
+
candidate_name = f"{base_name}_{suffix}"
|
|
1252
|
+
|
|
1253
|
+
add_command = {
|
|
1254
|
+
"type": "add_widget_to_user_widget",
|
|
1255
|
+
"user_widget_path": user_widget_path,
|
|
1256
|
+
"widget_type": "Button",
|
|
1257
|
+
"widget_name": candidate_name,
|
|
1258
|
+
"parent_widget_name": parent_widget_name,
|
|
1259
|
+
}
|
|
1260
|
+
add_response = _ensure_dict_response(send_to_unreal(add_command))
|
|
1261
|
+
if add_response.get("success"):
|
|
1262
|
+
actual_name = str(add_response.get("widget_name") or candidate_name)
|
|
1263
|
+
existing_names.add(actual_name)
|
|
1264
|
+
added.append(actual_name)
|
|
1265
|
+
else:
|
|
1266
|
+
provision_failures.append(
|
|
1267
|
+
{
|
|
1268
|
+
"requested_name": candidate_name,
|
|
1269
|
+
"error": add_response.get("error", "Unknown error"),
|
|
1270
|
+
}
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
return {
|
|
1274
|
+
"success": len(provision_failures) == 0,
|
|
1275
|
+
"added": added,
|
|
1276
|
+
"failures": provision_failures,
|
|
1277
|
+
"existing_button_count": len(existing_button_names),
|
|
1278
|
+
"desired_button_count": desired_button_count,
|
|
1279
|
+
"parent_widget_name": parent_widget_name,
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def _coerce_numeric_list(value, expected_length: Optional[int] = None) -> List[float]:
|
|
1284
|
+
if value is None:
|
|
1285
|
+
return []
|
|
1286
|
+
|
|
1287
|
+
if isinstance(value, (list, tuple)):
|
|
1288
|
+
result = []
|
|
1289
|
+
for item in value:
|
|
1290
|
+
try:
|
|
1291
|
+
result.append(float(item))
|
|
1292
|
+
except (TypeError, ValueError):
|
|
1293
|
+
return []
|
|
1294
|
+
return result
|
|
1295
|
+
|
|
1296
|
+
if isinstance(value, str):
|
|
1297
|
+
stripped = value.strip()
|
|
1298
|
+
if not stripped:
|
|
1299
|
+
return []
|
|
1300
|
+
|
|
1301
|
+
try:
|
|
1302
|
+
parsed = json.loads(stripped)
|
|
1303
|
+
return _coerce_numeric_list(parsed, expected_length)
|
|
1304
|
+
except json.JSONDecodeError:
|
|
1305
|
+
pass
|
|
1306
|
+
|
|
1307
|
+
stripped = stripped.strip("[]()")
|
|
1308
|
+
if not stripped:
|
|
1309
|
+
return []
|
|
1310
|
+
tokens = re.split(r"[\s,;]+", stripped)
|
|
1311
|
+
result = []
|
|
1312
|
+
for token in tokens:
|
|
1313
|
+
if not token:
|
|
1314
|
+
continue
|
|
1315
|
+
try:
|
|
1316
|
+
result.append(float(token))
|
|
1317
|
+
except ValueError:
|
|
1318
|
+
return []
|
|
1319
|
+
return result
|
|
1320
|
+
|
|
1321
|
+
return []
|
|
1322
|
+
|
|
1323
|
+
def _ensure_vector(value, fallback: Optional[List[float]] = None) -> List[float]:
|
|
1324
|
+
vector = _coerce_numeric_list(value, expected_length=3)
|
|
1325
|
+
if len(vector) == 3:
|
|
1326
|
+
return vector
|
|
1327
|
+
return [0.0, 0.0, 0.0] if fallback is None else fallback
|
|
1328
|
+
|
|
1329
|
+
def _ensure_two_d(value, fallback: Optional[List[float]] = None) -> List[float]:
|
|
1330
|
+
vector = _coerce_numeric_list(value, expected_length=2)
|
|
1331
|
+
if len(vector) == 2:
|
|
1332
|
+
return vector
|
|
1333
|
+
return [0.0, 0.0] if fallback is None else fallback
|
|
1334
|
+
|
|
1335
|
+
@mcp.tool()
|
|
1336
|
+
def how_to_use() -> str:
|
|
1337
|
+
"""Hey LLM, this grabs the how_to_use.md from knowledge_base—it's your cheat sheet for running Unreal with this MCP. Fetch it at the start of a new chat session to get the lowdown on quirks and how shit works."""
|
|
1338
|
+
try:
|
|
1339
|
+
current_dir = Path(__file__).parent
|
|
1340
|
+
md_file_path = current_dir / "knowledge_base" / "how_to_use.md"
|
|
1341
|
+
if not md_file_path.exists():
|
|
1342
|
+
return "Error: how_to_use.md not found in knowledge_base subfolder."
|
|
1343
|
+
with open(md_file_path, "r", encoding="utf-8") as md_file:
|
|
1344
|
+
return md_file.read()
|
|
1345
|
+
except Exception as e:
|
|
1346
|
+
return f"Error loading how_to_use.md: {str(e)}—fix your shit."
|
|
1347
|
+
|
|
1348
|
+
# Define basic tools for Claude to call
|
|
1349
|
+
@mcp.tool()
|
|
1350
|
+
def handshake_test(message: str) -> str:
|
|
1351
|
+
"""Send a handshake message to Unreal Engine"""
|
|
1352
|
+
try:
|
|
1353
|
+
command = {
|
|
1354
|
+
"type": "handshake",
|
|
1355
|
+
"message": message
|
|
1356
|
+
}
|
|
1357
|
+
response = send_to_unreal(command)
|
|
1358
|
+
if response.get("success"):
|
|
1359
|
+
return f"Handshake successful: {response['message']}"
|
|
1360
|
+
else:
|
|
1361
|
+
return f"Handshake failed: {response.get('error', 'Unknown error')}"
|
|
1362
|
+
except Exception as e:
|
|
1363
|
+
return f"Error communicating with Unreal: {str(e)}"
|
|
1364
|
+
|
|
1365
|
+
@mcp.tool()
|
|
1366
|
+
def execute_python_script(script: str) -> str:
|
|
1367
|
+
"""
|
|
1368
|
+
Execute a Python script within Unreal Engine's Python interpreter.
|
|
1369
|
+
Args:
|
|
1370
|
+
script: A string containing the Python code to execute in Unreal Engine.
|
|
1371
|
+
Returns:
|
|
1372
|
+
Message indicating success, failure, or a request for confirmation.
|
|
1373
|
+
Note:
|
|
1374
|
+
This tool sends the script to Unreal Engine, where it is executed via a temporary file using Unreal's internal Python execution system (similar to GEngine->Exec).
|
|
1375
|
+
This method is stable but may not handle Blueprint-specific APIs as seamlessly as direct Python API calls.
|
|
1376
|
+
For Blueprint manipulation, consider using dedicated tools like `add_node_to_blueprint` or ensuring the script uses stable `unreal` module functions.
|
|
1377
|
+
Use this tool for Python script execution instead of `execute_unreal_command` with 'py' commands.
|
|
1378
|
+
"""
|
|
1379
|
+
try:
|
|
1380
|
+
if is_potentially_destructive(script):
|
|
1381
|
+
return ("This script appears to involve potentially destructive actions (e.g., deleting or saving files) "
|
|
1382
|
+
"that were not explicitly requested. Please confirm if you want to proceed by saying 'Yes, execute it' "
|
|
1383
|
+
"or modify your request to explicitly allow such actions.")
|
|
1384
|
+
|
|
1385
|
+
command = {
|
|
1386
|
+
"type": "execute_python",
|
|
1387
|
+
"script": script
|
|
1388
|
+
}
|
|
1389
|
+
response = send_to_unreal(command)
|
|
1390
|
+
if response.get("success"):
|
|
1391
|
+
output = response.get("output", "No output returned")
|
|
1392
|
+
return f"Script executed successfully. Output: {output}"
|
|
1393
|
+
else:
|
|
1394
|
+
error = response.get("error", "Unknown error")
|
|
1395
|
+
output = response.get("output", "")
|
|
1396
|
+
if output:
|
|
1397
|
+
error += f"\n\nPartial output before error: {output}"
|
|
1398
|
+
return f"Failed to execute script: {response.get('error', 'Unknown error')}"
|
|
1399
|
+
except Exception as e:
|
|
1400
|
+
return f"Error sending script to Unreal: {str(e)}"
|
|
1401
|
+
|
|
1402
|
+
@mcp.tool()
|
|
1403
|
+
def execute_unreal_command(command: str) -> str:
|
|
1404
|
+
"""
|
|
1405
|
+
Execute an Unreal Engine command-line (CMD) command.
|
|
1406
|
+
Args:
|
|
1407
|
+
command: A string containing the Unreal Engine command to execute (e.g., "obj list", "stat fps").
|
|
1408
|
+
Returns:
|
|
1409
|
+
Message indicating success or failure, including any output or errors.
|
|
1410
|
+
Note:
|
|
1411
|
+
This tool executes commands directly in Unreal Engine's command system, similar to the editor's console.
|
|
1412
|
+
It is intended for built-in editor commands (e.g., "stat fps", "obj list") and not for running Python scripts.
|
|
1413
|
+
Do not use this tool with 'py' commands (e.g., "py script.py"); instead, use `execute_python_script` for Python execution,
|
|
1414
|
+
which provides dedicated safety checks and output handling.
|
|
1415
|
+
Output capture is limited; for detailed output, consider wrapping the command in a Python script with `execute_python_script`.
|
|
1416
|
+
"""
|
|
1417
|
+
try:
|
|
1418
|
+
# Check if the command is attempting to run a Python script
|
|
1419
|
+
if command.strip().lower().startswith("py "):
|
|
1420
|
+
return (
|
|
1421
|
+
"Error: Use `execute_python_script` to run Python scripts instead of `execute_unreal_command` with 'py' commands. "
|
|
1422
|
+
"For example, use `execute_python_script(script='your_code_here')` for Python execution.")
|
|
1423
|
+
|
|
1424
|
+
# Check for potentially destructive commands
|
|
1425
|
+
destructive_keywords = ["delete", "save", "quit", "exit", "restart"]
|
|
1426
|
+
if any(keyword in command.lower() for keyword in destructive_keywords):
|
|
1427
|
+
return ("This command appears to involve potentially destructive actions (e.g., deleting or saving). "
|
|
1428
|
+
"Please confirm by saying 'Yes, execute it' or explicitly request such actions.")
|
|
1429
|
+
|
|
1430
|
+
command_dict = {
|
|
1431
|
+
"type": "execute_unreal_command",
|
|
1432
|
+
"command": command
|
|
1433
|
+
}
|
|
1434
|
+
response = send_to_unreal(command_dict)
|
|
1435
|
+
if response.get("success"):
|
|
1436
|
+
output = response.get("output", "Command executed with no detailed output returned")
|
|
1437
|
+
return f"Command '{command}' executed successfully. Output: {output}"
|
|
1438
|
+
else:
|
|
1439
|
+
return f"Failed to execute command '{command}': {response.get('error', 'Unknown error')}"
|
|
1440
|
+
except Exception as e:
|
|
1441
|
+
return f"Error sending command to Unreal: {str(e)}"
|
|
1442
|
+
|
|
1443
|
+
#
|
|
1444
|
+
# Basic Object Commands
|
|
1445
|
+
#
|
|
1446
|
+
|
|
1447
|
+
@mcp.tool()
|
|
1448
|
+
def spawn_object(actor_class: str, location: Optional[List[float]] = None, rotation: Optional[List[float]] = None, scale: Optional[List[float]] = None, actor_label: Optional[str] = None) -> str:
|
|
1449
|
+
"""
|
|
1450
|
+
Spawn an object in the Unreal Engine level
|
|
1451
|
+
Args:
|
|
1452
|
+
actor_class: For basic shapes, use: "Cube", "Sphere", "Cylinder", or "Cone".
|
|
1453
|
+
For other actors, use class name like "PointLight" or full path.
|
|
1454
|
+
location: [X, Y, Z] coordinates
|
|
1455
|
+
rotation: [Pitch, Yaw, Roll] in degrees
|
|
1456
|
+
scale: [X, Y, Z] scale factors
|
|
1457
|
+
actor_label: Optional custom name for the actor
|
|
1458
|
+
Returns:
|
|
1459
|
+
Message indicating success or failure
|
|
1460
|
+
"""
|
|
1461
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
1462
|
+
rotation = _ensure_vector(rotation, [0.0, 0.0, 0.0])
|
|
1463
|
+
scale = _ensure_vector(scale, [1.0, 1.0, 1.0])
|
|
1464
|
+
|
|
1465
|
+
command = {
|
|
1466
|
+
"type": "spawn",
|
|
1467
|
+
"actor_class": actor_class,
|
|
1468
|
+
"location": location,
|
|
1469
|
+
"rotation": rotation,
|
|
1470
|
+
"scale": scale,
|
|
1471
|
+
"actor_label": actor_label
|
|
1472
|
+
}
|
|
1473
|
+
response = send_to_unreal(command)
|
|
1474
|
+
if response.get("success"):
|
|
1475
|
+
return f"Successfully spawned {actor_class}" + (f" with label '{actor_label}'" if actor_label else "")
|
|
1476
|
+
else:
|
|
1477
|
+
error = response.get('error', 'Unknown error')
|
|
1478
|
+
# Add hint for Claude to understand what went wrong
|
|
1479
|
+
if "not found" in error:
|
|
1480
|
+
hint = "\nHint: For basic shapes, use 'Cube', 'Sphere', 'Cylinder', or 'Cone'. For other actors, try using '/Script/Engine.PointLight' format."
|
|
1481
|
+
error += hint
|
|
1482
|
+
return f"Failed to spawn object: {error}"
|
|
1483
|
+
|
|
1484
|
+
@mcp.tool()
|
|
1485
|
+
def edit_component_property(blueprint_path: str, component_name: str, property_name: str, value: str, is_scene_actor: bool = False, actor_name: str = "") -> str:
|
|
1486
|
+
"""
|
|
1487
|
+
Edit a property of a component in a Blueprint or scene actor.
|
|
1488
|
+
Args:
|
|
1489
|
+
blueprint_path: Path to the Blueprint (e.g., "/Game/FlappyBird/BP_FlappyBird") or "" for scene actors
|
|
1490
|
+
component_name: Name of the component (e.g., "BirdMesh", "RootComponent")
|
|
1491
|
+
property_name: Name of the property to edit (e.g., "StaticMesh", "RelativeLocation")
|
|
1492
|
+
value: New value as a string (e.g., "'/Engine/BasicShapes/Sphere.Sphere'", "100,200,300")
|
|
1493
|
+
is_scene_actor: If True, edit a component on a scene actor (default: False)
|
|
1494
|
+
actor_name: Name of the actor in the scene (required if is_scene_actor is True, e.g., "Cube_1")
|
|
1495
|
+
Returns:
|
|
1496
|
+
Message indicating success or failure, with optional property suggestions if the property is not found.
|
|
1497
|
+
Capabilities:
|
|
1498
|
+
- Set component properties in Blueprints (e.g., StaticMesh, bSimulatePhysics).
|
|
1499
|
+
- Modify scene actor components (e.g., position, rotation, scale, material).
|
|
1500
|
+
- Supports scalar types (double, int, bool), objects (e.g., materials), and vectors/rotators (e.g., "100,200,300" for FVector). Note: UE 5.6 uses double for numeric operations.
|
|
1501
|
+
- Examples:
|
|
1502
|
+
- Set a mesh: edit_component_property("/Game/FlappyBird/BP_FlappyBird", "BirdMesh", "StaticMesh", "'/Engine/BasicShapes/Sphere.Sphere'")
|
|
1503
|
+
- Move an actor: edit_component_property("", "RootComponent", "RelativeLocation", "100,200,300", True, "Cube_1")
|
|
1504
|
+
- Rotate an actor: edit_component_property("", "RootComponent", "RelativeRotation", "0,90,0", True, "Cube_1")
|
|
1505
|
+
- Scale an actor: edit_component_property("", "RootComponent", "RelativeScale3D", "2,2,2", True, "Cube_1")
|
|
1506
|
+
- Enable physics: edit_component_property("/Game/FlappyBird/BP_FlappyBird", "BirdMesh", "bSimulatePhysics", "true")
|
|
1507
|
+
"""
|
|
1508
|
+
command = {
|
|
1509
|
+
"type": "edit_component_property",
|
|
1510
|
+
"blueprint_path": blueprint_path,
|
|
1511
|
+
"component_name": component_name,
|
|
1512
|
+
"property_name": property_name,
|
|
1513
|
+
"value": value,
|
|
1514
|
+
"is_scene_actor": is_scene_actor,
|
|
1515
|
+
"actor_name": actor_name
|
|
1516
|
+
}
|
|
1517
|
+
response = send_to_unreal(command)
|
|
1518
|
+
|
|
1519
|
+
# CHANGED: Improved response handling to support both string and dict responses
|
|
1520
|
+
try:
|
|
1521
|
+
# Handle case where response is already a dict
|
|
1522
|
+
if isinstance(response, dict):
|
|
1523
|
+
result = response
|
|
1524
|
+
# Handle case where response is a string
|
|
1525
|
+
elif isinstance(response, str):
|
|
1526
|
+
import json
|
|
1527
|
+
result = json.loads(response)
|
|
1528
|
+
else:
|
|
1529
|
+
return f"Error: Unexpected response type: {type(response)}"
|
|
1530
|
+
|
|
1531
|
+
if result.get("success"):
|
|
1532
|
+
return result.get("message", f"Set {property_name} of {component_name} to {value}")
|
|
1533
|
+
else:
|
|
1534
|
+
error = result.get("error", "Unknown error")
|
|
1535
|
+
if "suggestions" in result:
|
|
1536
|
+
error += f"\nSuggestions: {result['suggestions']}"
|
|
1537
|
+
return f"Failed: {error}"
|
|
1538
|
+
except Exception as e:
|
|
1539
|
+
return f"Error: {str(e)}\nRaw response: {response}"
|
|
1540
|
+
|
|
1541
|
+
@mcp.tool()
|
|
1542
|
+
def create_material(material_name: str, color: List[float]) -> str:
|
|
1543
|
+
"""
|
|
1544
|
+
Create a new material with the specified color
|
|
1545
|
+
Args:
|
|
1546
|
+
material_name: Name for the new material
|
|
1547
|
+
color: [R, G, B] color values (0-1)
|
|
1548
|
+
Returns:
|
|
1549
|
+
Message indicating success or failure, and the material path if successful
|
|
1550
|
+
"""
|
|
1551
|
+
normalized_color = _coerce_numeric_list(color)
|
|
1552
|
+
if len(normalized_color) == 3:
|
|
1553
|
+
color = normalized_color
|
|
1554
|
+
|
|
1555
|
+
command = {
|
|
1556
|
+
"type": "create_material",
|
|
1557
|
+
"material_name": material_name,
|
|
1558
|
+
"color": color
|
|
1559
|
+
}
|
|
1560
|
+
response = send_to_unreal(command)
|
|
1561
|
+
if response.get("success"):
|
|
1562
|
+
return f"Successfully created material '{material_name}' with path: {response.get('material_path')}"
|
|
1563
|
+
else:
|
|
1564
|
+
return f"Failed to create material: {response.get('error', 'Unknown error')}"
|
|
1565
|
+
|
|
1566
|
+
#
|
|
1567
|
+
# Blueprint Commands
|
|
1568
|
+
#
|
|
1569
|
+
|
|
1570
|
+
@mcp.tool()
|
|
1571
|
+
def create_blueprint(blueprint_name: str, parent_class: str = "Actor", save_path: str = "/Game/Blueprints") -> str:
|
|
1572
|
+
"""
|
|
1573
|
+
Create a new Blueprint class
|
|
1574
|
+
Args:
|
|
1575
|
+
blueprint_name: Name for the new Blueprint
|
|
1576
|
+
parent_class: Parent class name or path (e.g., "Actor", "/Script/Engine.Actor")
|
|
1577
|
+
save_path: Path to save the Blueprint asset
|
|
1578
|
+
Returns:
|
|
1579
|
+
Message indicating success or failure
|
|
1580
|
+
"""
|
|
1581
|
+
command = {
|
|
1582
|
+
"type": "create_blueprint",
|
|
1583
|
+
"blueprint_name": blueprint_name,
|
|
1584
|
+
"parent_class": parent_class,
|
|
1585
|
+
"save_path": save_path
|
|
1586
|
+
}
|
|
1587
|
+
response = send_to_unreal(command)
|
|
1588
|
+
if response.get("success"):
|
|
1589
|
+
return f"Successfully created Blueprint '{blueprint_name}' with path: {response.get('blueprint_path', save_path + '/' + blueprint_name)}"
|
|
1590
|
+
else:
|
|
1591
|
+
return f"Failed to create Blueprint: {response.get('error', 'Unknown error')}"
|
|
1592
|
+
|
|
1593
|
+
@mcp.tool()
|
|
1594
|
+
def add_component_to_blueprint(blueprint_path: str, component_class: str, component_name: str = None) -> str:
|
|
1595
|
+
"""
|
|
1596
|
+
Add a component to a Blueprint
|
|
1597
|
+
Args:
|
|
1598
|
+
blueprint_path: Path to the Blueprint asset
|
|
1599
|
+
component_class: Component class to add (e.g., "StaticMeshComponent", "PointLightComponent")
|
|
1600
|
+
component_name: Name for the new component (optional)
|
|
1601
|
+
Returns:
|
|
1602
|
+
Message indicating success or failure
|
|
1603
|
+
"""
|
|
1604
|
+
command = {
|
|
1605
|
+
"type": "add_component",
|
|
1606
|
+
"blueprint_path": blueprint_path,
|
|
1607
|
+
"component_class": component_class,
|
|
1608
|
+
"component_name": component_name
|
|
1609
|
+
}
|
|
1610
|
+
response = send_to_unreal(command)
|
|
1611
|
+
if response.get("success"):
|
|
1612
|
+
return f"Successfully added {component_class} to Blueprint at {blueprint_path}"
|
|
1613
|
+
else:
|
|
1614
|
+
return f"Failed to add component: {response.get('error', 'Unknown error')}"
|
|
1615
|
+
|
|
1616
|
+
@mcp.tool()
|
|
1617
|
+
def add_variable_to_blueprint(blueprint_path: str, variable_name: str, variable_type: str, default_value: str = None, category: str = "Default") -> str:
|
|
1618
|
+
"""
|
|
1619
|
+
Add a variable to a Blueprint
|
|
1620
|
+
Args:
|
|
1621
|
+
blueprint_path: Path to the Blueprint asset
|
|
1622
|
+
variable_name: Name for the new variable
|
|
1623
|
+
variable_type: Type of the variable (e.g., "double", "vector", "boolean") - Note: UE 5.6 uses double for numeric values
|
|
1624
|
+
default_value: Default value for the variable (optional)
|
|
1625
|
+
category: Category for organizing variables in the Blueprint editor (optional)
|
|
1626
|
+
Returns:
|
|
1627
|
+
Message indicating success or failure
|
|
1628
|
+
"""
|
|
1629
|
+
# Convert default_value to string if it's a number
|
|
1630
|
+
if default_value is not None and not isinstance(default_value, str):
|
|
1631
|
+
default_value = str(default_value)
|
|
1632
|
+
|
|
1633
|
+
command = {
|
|
1634
|
+
"type": "add_variable",
|
|
1635
|
+
"blueprint_path": blueprint_path,
|
|
1636
|
+
"variable_name": variable_name,
|
|
1637
|
+
"variable_type": variable_type,
|
|
1638
|
+
"default_value": default_value,
|
|
1639
|
+
"category": category
|
|
1640
|
+
}
|
|
1641
|
+
response = send_to_unreal(command)
|
|
1642
|
+
if response.get("success"):
|
|
1643
|
+
return f"Successfully added {variable_type} variable '{variable_name}' to Blueprint at {blueprint_path}"
|
|
1644
|
+
else:
|
|
1645
|
+
return f"Failed to add variable: {response.get('error', 'Unknown error')}"
|
|
1646
|
+
|
|
1647
|
+
@mcp.tool()
|
|
1648
|
+
def add_function_to_blueprint(blueprint_path: str, function_name: str, inputs: Optional[List[Dict]] = None, outputs: Optional[List[Dict]] = None) -> str:
|
|
1649
|
+
"""
|
|
1650
|
+
Add a function to a Blueprint using UE 5.6 Kismet type system
|
|
1651
|
+
Args:
|
|
1652
|
+
blueprint_path: Path to the Blueprint asset
|
|
1653
|
+
function_name: Name for the new function
|
|
1654
|
+
inputs: List of input parameters with UE 5.6 types:
|
|
1655
|
+
[{"name": "param1", "type": "double"}, {"name": "param2", "type": "string"}, ...]
|
|
1656
|
+
Supported UE 5.6 types: boolean, byte, int, double, float, string, text, name,
|
|
1657
|
+
vector, rotator, transform, color, object (with class name)
|
|
1658
|
+
outputs: List of output parameters with same type format as inputs
|
|
1659
|
+
Returns:
|
|
1660
|
+
Message indicating success or failure
|
|
1661
|
+
"""
|
|
1662
|
+
if inputs is None:
|
|
1663
|
+
inputs = []
|
|
1664
|
+
if outputs is None:
|
|
1665
|
+
outputs = []
|
|
1666
|
+
|
|
1667
|
+
command = {
|
|
1668
|
+
"type": "add_function",
|
|
1669
|
+
"blueprint_path": blueprint_path,
|
|
1670
|
+
"function_name": function_name,
|
|
1671
|
+
"inputs": inputs,
|
|
1672
|
+
"outputs": outputs
|
|
1673
|
+
}
|
|
1674
|
+
response = send_to_unreal(command)
|
|
1675
|
+
if response.get("success"):
|
|
1676
|
+
return f"Successfully added function '{function_name}' to Blueprint at {blueprint_path} with ID: {response.get('function_id', 'unknown')}"
|
|
1677
|
+
else:
|
|
1678
|
+
return f"Failed to add function: {response.get('error', 'Unknown error')}"
|
|
1679
|
+
|
|
1680
|
+
@mcp.tool()
|
|
1681
|
+
def add_node_to_blueprint(blueprint_path: str, function_id: str, node_type: str, node_position: Optional[List[int]] = None, node_properties: Optional[Dict] = None) -> str:
|
|
1682
|
+
"""
|
|
1683
|
+
Add a node to a Blueprint graph with comprehensive node discovery
|
|
1684
|
+
Args:
|
|
1685
|
+
blueprint_path: Path to the Blueprint asset
|
|
1686
|
+
function_id: ID of the function to add the node to
|
|
1687
|
+
node_type: Type of node to add. The system now supports ANY Blueprint-callable function with intelligent discovery:
|
|
1688
|
+
|
|
1689
|
+
**FLEXIBLE NODE TYPES** - The system automatically finds the correct function:
|
|
1690
|
+
- Simple names: "Add", "Multiply", "SetActorRotation", "PrintString"
|
|
1691
|
+
- Library.Function format: "KismetMathLibrary.Add", "Actor.SetActorLocation"
|
|
1692
|
+
- Variations: "SetRotation", "MoveComponent", "CreateWidget"
|
|
1693
|
+
|
|
1694
|
+
**COMPREHENSIVE COVERAGE** - Searches across ALL Blueprint libraries:
|
|
1695
|
+
- Math: "Add", "Multiply", "Divide", "Subtract", "MakeVector", "BreakVector"
|
|
1696
|
+
- Actor: "SetActorLocation", "SetActorRotation", "GetActorLocation", "SetActorScale3D"
|
|
1697
|
+
- Components: "SetWorldLocation", "SetRelativeLocation", "SetWorldRotation"
|
|
1698
|
+
- String: "Append", "Contains", "Split", "ToUpper", "ToLower"
|
|
1699
|
+
- Array: "Add Item", "Remove Item", "Get", "Length", "Contains"
|
|
1700
|
+
- System: "PrintString", "Delay", "Branch", "Sequence", "IsValid"
|
|
1701
|
+
- Gameplay: "GetPlayerController", "GetPlayerPawn", "SpawnActor"
|
|
1702
|
+
|
|
1703
|
+
**INTELLIGENT FALLBACKS** - If exact match fails, automatically tries:
|
|
1704
|
+
- Common variations (Set/Get prefixes, library corrections)
|
|
1705
|
+
- Searches ALL loaded Blueprint-accessible classes
|
|
1706
|
+
- Returns helpful suggestions for similar functions
|
|
1707
|
+
|
|
1708
|
+
**EXAMPLES THAT NOW WORK**:
|
|
1709
|
+
- "SetActorRotation" → finds Actor.SetActorRotation
|
|
1710
|
+
- "KismetMathLibrary.SetActorRotation" → corrects to Actor.SetActorRotation
|
|
1711
|
+
- "Add" → finds KismetMathLibrary.Add (with proper type suffix)
|
|
1712
|
+
- "MoveComponent" → finds appropriate component movement function
|
|
1713
|
+
|
|
1714
|
+
node_position: Position of the node in the graph [X, Y]. **IMPORTANT**: Space nodes at least 400 units apart horizontally and 300 units vertically to avoid overlap and ensure a clean, organized graph (e.g., [0, 0], [400, 0], [800, 0] for a chain).
|
|
1715
|
+
node_properties: Properties to set on the node (optional)
|
|
1716
|
+
Returns:
|
|
1717
|
+
On success: The node ID (GUID) and confirmation message
|
|
1718
|
+
On failure: Detailed error with suggestions for alternative node types
|
|
1719
|
+
Note:
|
|
1720
|
+
The enhanced discovery system searches across 30+ Blueprint libraries and ALL loaded classes.
|
|
1721
|
+
It handles incorrect library prefixes, tries common variations, and provides intelligent suggestions.
|
|
1722
|
+
This dramatically improves the success rate for any Blueprint-callable function.
|
|
1723
|
+
"""
|
|
1724
|
+
if node_properties is None:
|
|
1725
|
+
node_properties = {}
|
|
1726
|
+
|
|
1727
|
+
if isinstance(node_position, str) or node_position is None:
|
|
1728
|
+
node_position = _ensure_two_d(node_position, [0.0, 0.0])
|
|
1729
|
+
elif isinstance(node_position, (list, tuple)):
|
|
1730
|
+
node_position = _ensure_two_d(node_position, [0.0, 0.0])
|
|
1731
|
+
else:
|
|
1732
|
+
node_position = [0.0, 0.0]
|
|
1733
|
+
|
|
1734
|
+
command = {
|
|
1735
|
+
"type": "add_node",
|
|
1736
|
+
"blueprint_path": blueprint_path,
|
|
1737
|
+
"function_id": function_id,
|
|
1738
|
+
"node_type": node_type,
|
|
1739
|
+
"node_position": node_position,
|
|
1740
|
+
"node_properties": node_properties
|
|
1741
|
+
}
|
|
1742
|
+
response = send_to_unreal(command)
|
|
1743
|
+
|
|
1744
|
+
if response.get("success") and response.get("node_id"):
|
|
1745
|
+
return f"Successfully added {node_type} node to function {function_id} in Blueprint at {blueprint_path} with ID: {response.get('node_id')}"
|
|
1746
|
+
else:
|
|
1747
|
+
error_msg = response.get('error', 'Unknown error')
|
|
1748
|
+
|
|
1749
|
+
# Check if the response contains suggestions
|
|
1750
|
+
if "SUGGESTIONS:" in error_msg:
|
|
1751
|
+
return f"Node '{node_type}' not found, but here are alternatives:\n{error_msg}"
|
|
1752
|
+
else:
|
|
1753
|
+
return f"Failed to add node '{node_type}': {error_msg}\n\nTip: The system now searches comprehensively across all Blueprint libraries. Try simpler names like 'Add', 'SetActorLocation', or 'PrintString'."
|
|
1754
|
+
|
|
1755
|
+
@mcp.tool()
|
|
1756
|
+
def get_node_suggestions(node_type: str) -> str:
|
|
1757
|
+
"""
|
|
1758
|
+
Get suggestions for a node type in Unreal Blueprints using comprehensive discovery
|
|
1759
|
+
Args:
|
|
1760
|
+
node_type: The partial or full node type to get suggestions for (e.g., "Add", "Vector", "Math", "Actor")
|
|
1761
|
+
Returns:
|
|
1762
|
+
A string with suggestions or error message
|
|
1763
|
+
|
|
1764
|
+
Enhanced Discovery Features:
|
|
1765
|
+
- Searches across ALL Blueprint-accessible classes (30+ libraries)
|
|
1766
|
+
- Handles any node name format (simple names, Library.Function, variations)
|
|
1767
|
+
- Provides intelligent scoring and ranking
|
|
1768
|
+
- Includes functions from: Math, Actor, Component, String, Array, System, Gameplay, etc.
|
|
1769
|
+
|
|
1770
|
+
Examples that now work comprehensively:
|
|
1771
|
+
- "Add" → finds all addition functions across libraries
|
|
1772
|
+
- "Vector" → finds all vector-related functions
|
|
1773
|
+
- "Actor" → finds all Actor class functions
|
|
1774
|
+
- "SetRotation" → finds rotation-setting functions across all classes
|
|
1775
|
+
- "Math" → finds all mathematical operations
|
|
1776
|
+
- "Component" → finds all component-related functions
|
|
1777
|
+
|
|
1778
|
+
The system now searches universally across all loaded Blueprint classes,
|
|
1779
|
+
so virtually any Blueprint-callable function can be discovered.
|
|
1780
|
+
"""
|
|
1781
|
+
command = {
|
|
1782
|
+
"type": "get_node_suggestions",
|
|
1783
|
+
"node_type": node_type
|
|
1784
|
+
}
|
|
1785
|
+
response = send_to_unreal(command)
|
|
1786
|
+
if response.get("success"):
|
|
1787
|
+
suggestions = response.get("suggestions", [])
|
|
1788
|
+
if suggestions:
|
|
1789
|
+
return f"Found {len(suggestions)} suggestions for '{node_type}' using comprehensive discovery:\n" + "\n".join(suggestions)
|
|
1790
|
+
else:
|
|
1791
|
+
return f"No suggestions found for '{node_type}' - try broader search terms like 'Math', 'Actor', 'Vector', or 'String'"
|
|
1792
|
+
else:
|
|
1793
|
+
error = response.get("error", "Unknown error")
|
|
1794
|
+
return f"Failed to get suggestions for '{node_type}': {error}"
|
|
1795
|
+
|
|
1796
|
+
@mcp.tool()
|
|
1797
|
+
def delete_node_from_blueprint(blueprint_path: str, function_id: str, node_id: str) -> str:
|
|
1798
|
+
"""
|
|
1799
|
+
Delete a node from a Blueprint graph
|
|
1800
|
+
Args:
|
|
1801
|
+
blueprint_path: Path to the Blueprint asset
|
|
1802
|
+
function_id: ID of the function containing the node
|
|
1803
|
+
node_id: ID of the node to delete
|
|
1804
|
+
Returns:
|
|
1805
|
+
Success or failure message
|
|
1806
|
+
"""
|
|
1807
|
+
command = {
|
|
1808
|
+
"type": "delete_node",
|
|
1809
|
+
"blueprint_path": blueprint_path,
|
|
1810
|
+
"function_id": function_id,
|
|
1811
|
+
"node_id": node_id
|
|
1812
|
+
}
|
|
1813
|
+
response = send_to_unreal(command)
|
|
1814
|
+
if response.get("success"):
|
|
1815
|
+
return f"Successfully deleted node {node_id} from function {function_id} in Blueprint at {blueprint_path}"
|
|
1816
|
+
else:
|
|
1817
|
+
return f"Failed to delete node: {response.get('error', 'Unknown error')}"
|
|
1818
|
+
|
|
1819
|
+
@mcp.tool()
|
|
1820
|
+
def get_all_nodes_in_graph(blueprint_path: str, function_id: str) -> str:
|
|
1821
|
+
"""
|
|
1822
|
+
Get all nodes in a Blueprint graph with their positions and types
|
|
1823
|
+
Args:
|
|
1824
|
+
blueprint_path: Path to the Blueprint asset
|
|
1825
|
+
function_id: ID of the function to get nodes from
|
|
1826
|
+
Returns:
|
|
1827
|
+
JSON string containing all nodes with their GUIDs, types, and positions
|
|
1828
|
+
"""
|
|
1829
|
+
command = {
|
|
1830
|
+
"type": "get_all_nodes",
|
|
1831
|
+
"blueprint_path": blueprint_path,
|
|
1832
|
+
"function_id": function_id
|
|
1833
|
+
}
|
|
1834
|
+
response = send_to_unreal(command)
|
|
1835
|
+
if response.get("success"):
|
|
1836
|
+
return response.get("nodes", "[]")
|
|
1837
|
+
else:
|
|
1838
|
+
return f"Failed to get nodes: {response.get('error', 'Unknown error')}"
|
|
1839
|
+
|
|
1840
|
+
@mcp.tool()
|
|
1841
|
+
def connect_blueprint_nodes(blueprint_path: str, function_id: str, source_node_id: str, source_pin: str, target_node_id: str, target_pin: str) -> str:
|
|
1842
|
+
command = {
|
|
1843
|
+
"type": "connect_nodes",
|
|
1844
|
+
"blueprint_path": blueprint_path,
|
|
1845
|
+
"function_id": function_id,
|
|
1846
|
+
"source_node_id": source_node_id,
|
|
1847
|
+
"source_pin": source_pin,
|
|
1848
|
+
"target_node_id": target_node_id,
|
|
1849
|
+
"target_pin": target_pin
|
|
1850
|
+
}
|
|
1851
|
+
response = send_to_unreal(command)
|
|
1852
|
+
if response.get("success"):
|
|
1853
|
+
return f"Successfully connected {source_node_id}.{source_pin} to {target_node_id}.{target_pin} in Blueprint at {blueprint_path}"
|
|
1854
|
+
else:
|
|
1855
|
+
error = response.get("error", "Unknown error")
|
|
1856
|
+
if "source_available_pins" in response and "target_available_pins" in response:
|
|
1857
|
+
error += f"\nAvailable pins on source ({source_node_id}): {json.dumps(response['source_available_pins'], indent=2)}"
|
|
1858
|
+
error += f"\nAvailable pins on target ({target_node_id}): {json.dumps(response['target_available_pins'], indent=2)}"
|
|
1859
|
+
return f"Failed to connect nodes: {error}"
|
|
1860
|
+
|
|
1861
|
+
@mcp.tool()
|
|
1862
|
+
def compile_blueprint(blueprint_path: str) -> str:
|
|
1863
|
+
"""
|
|
1864
|
+
Compile a Blueprint
|
|
1865
|
+
Args:
|
|
1866
|
+
blueprint_path: Path to the Blueprint asset
|
|
1867
|
+
Returns:
|
|
1868
|
+
Message indicating success or failure
|
|
1869
|
+
"""
|
|
1870
|
+
command = {
|
|
1871
|
+
"type": "compile_blueprint",
|
|
1872
|
+
"blueprint_path": blueprint_path
|
|
1873
|
+
}
|
|
1874
|
+
response = send_to_unreal(command)
|
|
1875
|
+
if response.get("success"):
|
|
1876
|
+
return f"Successfully compiled Blueprint at {blueprint_path}"
|
|
1877
|
+
else:
|
|
1878
|
+
return f"Failed to compile Blueprint: {response.get('error', 'Unknown error')}"
|
|
1879
|
+
|
|
1880
|
+
@mcp.tool()
|
|
1881
|
+
def spawn_blueprint_actor(blueprint_path: str, location: Optional[List[float]] = None, rotation: Optional[List[float]] = None, scale: Optional[List[float]] = None, actor_label: Optional[str] = None) -> str:
|
|
1882
|
+
"""
|
|
1883
|
+
Spawn a Blueprint actor in the level
|
|
1884
|
+
Args:
|
|
1885
|
+
blueprint_path: Path to the Blueprint asset
|
|
1886
|
+
location: [X, Y, Z] coordinates
|
|
1887
|
+
rotation: [Pitch, Yaw, Roll] in degrees
|
|
1888
|
+
scale: [X, Y, Z] scale factors
|
|
1889
|
+
actor_label: Optional custom name for the actor
|
|
1890
|
+
Returns:
|
|
1891
|
+
Message indicating success or failure
|
|
1892
|
+
"""
|
|
1893
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
1894
|
+
rotation = _ensure_vector(rotation, [0.0, 0.0, 0.0])
|
|
1895
|
+
scale = _ensure_vector(scale, [1.0, 1.0, 1.0])
|
|
1896
|
+
|
|
1897
|
+
command = {
|
|
1898
|
+
"type": "spawn_blueprint",
|
|
1899
|
+
"blueprint_path": blueprint_path,
|
|
1900
|
+
"location": location,
|
|
1901
|
+
"rotation": rotation,
|
|
1902
|
+
"scale": scale,
|
|
1903
|
+
"actor_label": actor_label
|
|
1904
|
+
}
|
|
1905
|
+
response = send_to_unreal(command)
|
|
1906
|
+
if response.get("success"):
|
|
1907
|
+
return f"Successfully spawned Blueprint {blueprint_path}" + (
|
|
1908
|
+
f" with label '{actor_label}'" if actor_label else "")
|
|
1909
|
+
else:
|
|
1910
|
+
return f"Failed to spawn Blueprint: {response.get('error', 'Unknown error')}"
|
|
1911
|
+
|
|
1912
|
+
@mcp.tool()
|
|
1913
|
+
def add_component_with_events(blueprint_path: str, component_name: str, component_class: str) -> str:
|
|
1914
|
+
"""
|
|
1915
|
+
Add a component to a Blueprint with overlap events if applicable.
|
|
1916
|
+
Args:
|
|
1917
|
+
blueprint_path: Path to the Blueprint (e.g., "/Game/FlappyBird/BP_FlappyBird")
|
|
1918
|
+
component_name: Name of the new component (e.g., "TriggerBox")
|
|
1919
|
+
component_class: Class of the component (e.g., "BoxComponent")
|
|
1920
|
+
Returns:
|
|
1921
|
+
Message with success, error, and event GUIDs if created
|
|
1922
|
+
"""
|
|
1923
|
+
command = {
|
|
1924
|
+
"type": "add_component_with_events",
|
|
1925
|
+
"blueprint_path": blueprint_path,
|
|
1926
|
+
"component_name": component_name,
|
|
1927
|
+
"component_class": component_class
|
|
1928
|
+
}
|
|
1929
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
1930
|
+
if response.get("success"):
|
|
1931
|
+
msg = response.get("message", f"Added component {component_name}")
|
|
1932
|
+
begin_guid = response.get("begin_overlap_guid")
|
|
1933
|
+
end_guid = response.get("end_overlap_guid")
|
|
1934
|
+
if not (begin_guid or end_guid):
|
|
1935
|
+
events_payload = response.get("events")
|
|
1936
|
+
events_data = {}
|
|
1937
|
+
if isinstance(events_payload, str):
|
|
1938
|
+
try:
|
|
1939
|
+
events_data = json.loads(events_payload)
|
|
1940
|
+
except json.JSONDecodeError:
|
|
1941
|
+
events_data = {}
|
|
1942
|
+
elif isinstance(events_payload, dict):
|
|
1943
|
+
events_data = events_payload
|
|
1944
|
+
begin_guid = events_data.get("begin_guid", begin_guid)
|
|
1945
|
+
end_guid = events_data.get("end_guid", end_guid)
|
|
1946
|
+
if begin_guid or end_guid:
|
|
1947
|
+
msg += (
|
|
1948
|
+
f"\nBeginOverlap GUID: {begin_guid or 'N/A'}"
|
|
1949
|
+
f"\nEndOverlap GUID: {end_guid or 'N/A'}"
|
|
1950
|
+
)
|
|
1951
|
+
return msg
|
|
1952
|
+
error_message = response.get("error", "Unknown error")
|
|
1953
|
+
raw_suffix = f"\nRaw response: {response.get('raw')}" if "raw" in response else ""
|
|
1954
|
+
return f"Failed: {error_message}{raw_suffix}"
|
|
1955
|
+
|
|
1956
|
+
@mcp.tool()
|
|
1957
|
+
def connect_blueprint_nodes_bulk(blueprint_path: str, function_id: str, connections: List[Dict], function_name: Optional[str] = None) -> str:
|
|
1958
|
+
"""
|
|
1959
|
+
Connect multiple pairs of nodes in a Blueprint graph
|
|
1960
|
+
Args:
|
|
1961
|
+
blueprint_path: Path to the Blueprint asset
|
|
1962
|
+
function_id: ID of the function containing the nodes
|
|
1963
|
+
connections: Array of connection definitions, each containing:
|
|
1964
|
+
- source_node_id: ID of the source node
|
|
1965
|
+
- source_pin: Name of the source pin
|
|
1966
|
+
- target_node_id: ID of the target node
|
|
1967
|
+
- target_pin: Name of the target pin
|
|
1968
|
+
Returns:
|
|
1969
|
+
Message indicating success or failure, with details on which connections succeeded or failed
|
|
1970
|
+
"""
|
|
1971
|
+
if isinstance(connections, str):
|
|
1972
|
+
try:
|
|
1973
|
+
connections = json.loads(connections)
|
|
1974
|
+
except json.JSONDecodeError as parse_err:
|
|
1975
|
+
return f"Failed to parse connections: {parse_err}"
|
|
1976
|
+
|
|
1977
|
+
normalized_connections: List[Dict] = []
|
|
1978
|
+
raw_connections = connections
|
|
1979
|
+
if isinstance(raw_connections, dict):
|
|
1980
|
+
raw_connections = [raw_connections]
|
|
1981
|
+
|
|
1982
|
+
for entry in raw_connections or []:
|
|
1983
|
+
if isinstance(entry, str):
|
|
1984
|
+
try:
|
|
1985
|
+
entry = json.loads(entry)
|
|
1986
|
+
except json.JSONDecodeError:
|
|
1987
|
+
continue
|
|
1988
|
+
if not isinstance(entry, dict):
|
|
1989
|
+
continue
|
|
1990
|
+
normalized_connections.append({
|
|
1991
|
+
"source_node_id": entry.get("source_node_id") or entry.get("source_node_guid"),
|
|
1992
|
+
"source_pin": entry.get("source_pin", "exec"),
|
|
1993
|
+
"target_node_id": entry.get("target_node_id") or entry.get("target_node_guid"),
|
|
1994
|
+
"target_pin": entry.get("target_pin", "exec")
|
|
1995
|
+
})
|
|
1996
|
+
|
|
1997
|
+
if not normalized_connections and isinstance(connections, list):
|
|
1998
|
+
normalized_connections = connections
|
|
1999
|
+
|
|
2000
|
+
if not normalized_connections:
|
|
2001
|
+
return "Failed: no valid connections provided"
|
|
2002
|
+
|
|
2003
|
+
command = {
|
|
2004
|
+
"type": "connect_nodes_bulk",
|
|
2005
|
+
"blueprint_path": blueprint_path,
|
|
2006
|
+
"function_id": function_id,
|
|
2007
|
+
"function_name": function_name or function_id,
|
|
2008
|
+
"connections": normalized_connections
|
|
2009
|
+
}
|
|
2010
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
2011
|
+
|
|
2012
|
+
# Handle the new detailed response format
|
|
2013
|
+
if response.get("success"):
|
|
2014
|
+
successful = response.get("successful_connections", 0)
|
|
2015
|
+
total = response.get("total_connections", 0)
|
|
2016
|
+
return f"Successfully connected {successful}/{total} node pairs in Blueprint at {blueprint_path}"
|
|
2017
|
+
else:
|
|
2018
|
+
# Extract detailed error information
|
|
2019
|
+
error_message = response.get("error", "Unknown error")
|
|
2020
|
+
failed_connections = []
|
|
2021
|
+
|
|
2022
|
+
for result in response.get("results", []):
|
|
2023
|
+
if not result.get("success", False):
|
|
2024
|
+
idx = result.get("connection_index", -1)
|
|
2025
|
+
src = result.get("source_node", "unknown")
|
|
2026
|
+
tgt = result.get("target_node", "unknown")
|
|
2027
|
+
err = result.get("error", "unknown error")
|
|
2028
|
+
failed_connections.append(f"Connection {idx}: {src} to {tgt} - {err}")
|
|
2029
|
+
|
|
2030
|
+
raw_suffix = f"\nRaw response: {response.get('raw')}" if "raw" in response else ""
|
|
2031
|
+
if failed_connections:
|
|
2032
|
+
detailed_errors = "\n- " + "\n- ".join(failed_connections)
|
|
2033
|
+
return f"Failed to connect nodes: {error_message}{detailed_errors}{raw_suffix}"
|
|
2034
|
+
else:
|
|
2035
|
+
return f"Failed to connect nodes: {error_message}{raw_suffix}"
|
|
2036
|
+
|
|
2037
|
+
@mcp.tool()
|
|
2038
|
+
def get_blueprint_node_guid(blueprint_path: str, graph_type: str = "EventGraph", node_name: str = None, function_id: str = None) -> str:
|
|
2039
|
+
"""
|
|
2040
|
+
Retrieve the GUID of a pre-existing node in a Blueprint graph.
|
|
2041
|
+
Args:
|
|
2042
|
+
blueprint_path: Path to the Blueprint asset (e.g., "/Game/Blueprints/TestBulkBlueprint")
|
|
2043
|
+
graph_type: Type of graph to query ("EventGraph" or "FunctionGraph", default: "EventGraph")
|
|
2044
|
+
node_name: Name of the node to find (e.g., "BeginPlay" for EventGraph, optional if using function_id)
|
|
2045
|
+
function_id: ID of the function to get the FunctionEntry node for (optional, used with graph_type="FunctionGraph")
|
|
2046
|
+
Returns:
|
|
2047
|
+
Message with the node's GUID or an error if not found
|
|
2048
|
+
"""
|
|
2049
|
+
command = {
|
|
2050
|
+
"type": "get_node_guid",
|
|
2051
|
+
"blueprint_path": blueprint_path,
|
|
2052
|
+
"graph_type": graph_type,
|
|
2053
|
+
"node_name": node_name if node_name else "",
|
|
2054
|
+
"function_id": function_id if function_id else ""
|
|
2055
|
+
}
|
|
2056
|
+
response = send_to_unreal(command)
|
|
2057
|
+
if response.get("success"):
|
|
2058
|
+
guid = response.get("node_guid")
|
|
2059
|
+
return f"Node GUID for {node_name or 'FunctionEntry'} in {graph_type} of {blueprint_path}: {guid}"
|
|
2060
|
+
else:
|
|
2061
|
+
return f"Failed to get node GUID: {response.get('error', 'Unknown error')}"
|
|
2062
|
+
|
|
2063
|
+
# Scene Control
|
|
2064
|
+
@mcp.tool()
|
|
2065
|
+
def get_all_scene_objects() -> str:
|
|
2066
|
+
"""
|
|
2067
|
+
Retrieve all actors in the current Unreal Engine level.
|
|
2068
|
+
Returns:
|
|
2069
|
+
JSON string of actors with their names, classes, and locations.
|
|
2070
|
+
"""
|
|
2071
|
+
command = {"type": "get_all_scene_objects"}
|
|
2072
|
+
response = send_to_unreal(command)
|
|
2073
|
+
return json.dumps(response) if response.get("success") else f"Failed: {response.get('error', 'Unknown error')}"
|
|
2074
|
+
|
|
2075
|
+
@mcp.tool()
|
|
2076
|
+
def get_blueprint_context(include_editor_state: bool = True, include_graph_info: bool = True,
|
|
2077
|
+
include_selected_nodes: bool = True, include_open_editors: bool = False,
|
|
2078
|
+
specific_blueprint_path: str = None, auto_discover_nodes: bool = True) -> str:
|
|
2079
|
+
"""
|
|
2080
|
+
Get comprehensive context about the currently active Blueprint editor in Unreal Engine.
|
|
2081
|
+
This provides AI tools with complete awareness of what Blueprint is being edited,
|
|
2082
|
+
what graph is active, selected nodes, compilation status, and more.
|
|
2083
|
+
|
|
2084
|
+
When called from "Sync with AI" button, this also discovers available Blueprint nodes
|
|
2085
|
+
for the current UE version and project, solving node creation issues.
|
|
2086
|
+
|
|
2087
|
+
Args:
|
|
2088
|
+
include_editor_state: Include detailed editor state (compilation status, Blueprint type, etc.) - default True
|
|
2089
|
+
include_graph_info: Include information about all graphs in the Blueprint - default True
|
|
2090
|
+
include_selected_nodes: Include information about currently selected nodes - default True
|
|
2091
|
+
include_open_editors: Include list of all open Blueprint editors - default False
|
|
2092
|
+
specific_blueprint_path: Focus on a specific Blueprint instead of the active one (optional)
|
|
2093
|
+
auto_discover_nodes: Automatically discover available Blueprint nodes for this UE version - default True
|
|
2094
|
+
|
|
2095
|
+
Returns:
|
|
2096
|
+
JSON string containing comprehensive Blueprint context information including:
|
|
2097
|
+
- Active Blueprint name and path
|
|
2098
|
+
- Current graph type (EventGraph, FunctionGraph, etc.)
|
|
2099
|
+
- Selected nodes with details (class, position, pins)
|
|
2100
|
+
- Compilation status and errors
|
|
2101
|
+
- All available graphs
|
|
2102
|
+
- Available Blueprint nodes for this UE version (if auto_discover_nodes=True)
|
|
2103
|
+
- Quick summary for AI analysis
|
|
2104
|
+
|
|
2105
|
+
Example response structure:
|
|
2106
|
+
{
|
|
2107
|
+
"success": true,
|
|
2108
|
+
"has_active_editor": true,
|
|
2109
|
+
"active_blueprint": {"name": "MyActor", "path": "/Game/Blueprints/MyActor"},
|
|
2110
|
+
"active_graph": {"name": "EventGraph", "type": "EventGraph"},
|
|
2111
|
+
"available_nodes": {...}, // Discovered nodes for this UE version
|
|
2112
|
+
"summary": {
|
|
2113
|
+
"active_blueprint_name": "MyActor",
|
|
2114
|
+
"active_blueprint_path": "/Game/Blueprints/MyActor",
|
|
2115
|
+
"active_graph_name": "EventGraph",
|
|
2116
|
+
"active_graph_type": "EventGraph",
|
|
2117
|
+
"has_selected_nodes": true,
|
|
2118
|
+
"nodes_discovered": true
|
|
2119
|
+
},
|
|
2120
|
+
"editor_state": {...}, // If include_editor_state = true
|
|
2121
|
+
"graph_info": {...}, // If include_graph_info = true
|
|
2122
|
+
"selected_nodes": {...} // If include_selected_nodes = true
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
This tool enables context-aware AI assistance by providing complete visibility
|
|
2126
|
+
into the current Blueprint editing session AND available node inventory.
|
|
2127
|
+
"""
|
|
2128
|
+
try:
|
|
2129
|
+
command = {
|
|
2130
|
+
"type": "get_blueprint_context",
|
|
2131
|
+
"include_editor_state": include_editor_state,
|
|
2132
|
+
"include_graph_info": include_graph_info,
|
|
2133
|
+
"include_selected_nodes": include_selected_nodes,
|
|
2134
|
+
"include_open_editors": include_open_editors
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
if specific_blueprint_path:
|
|
2138
|
+
command["specific_blueprint_path"] = specific_blueprint_path
|
|
2139
|
+
|
|
2140
|
+
response = send_to_unreal(command)
|
|
2141
|
+
|
|
2142
|
+
if response.get("success"):
|
|
2143
|
+
# If auto_discover_nodes is enabled, also discover available nodes
|
|
2144
|
+
if auto_discover_nodes:
|
|
2145
|
+
print("Auto-discovering available Blueprint nodes for this UE version...")
|
|
2146
|
+
discovery_result = discover_available_blueprint_nodes()
|
|
2147
|
+
try:
|
|
2148
|
+
discovery_data = json.loads(discovery_result)
|
|
2149
|
+
if discovery_data.get("success"):
|
|
2150
|
+
response["available_nodes"] = discovery_data.get("discovered_nodes", {})
|
|
2151
|
+
response["node_discovery"] = {
|
|
2152
|
+
"success": True,
|
|
2153
|
+
"total_nodes_found": discovery_data.get("total_nodes_found", 0),
|
|
2154
|
+
"unreal_version": discovery_data.get("unreal_version", "Unknown")
|
|
2155
|
+
}
|
|
2156
|
+
print(f"Node discovery completed: {discovery_data.get('total_nodes_found', 0)} nodes found")
|
|
2157
|
+
else:
|
|
2158
|
+
response["available_nodes"] = {}
|
|
2159
|
+
response["node_discovery"] = {
|
|
2160
|
+
"success": False,
|
|
2161
|
+
"error": "Node discovery failed"
|
|
2162
|
+
}
|
|
2163
|
+
except json.JSONDecodeError as e:
|
|
2164
|
+
print(f"Failed to parse discovery result: {e}")
|
|
2165
|
+
response["available_nodes"] = {}
|
|
2166
|
+
response["node_discovery"] = {
|
|
2167
|
+
"success": False,
|
|
2168
|
+
"error": f"JSON decode error: {str(e)}"
|
|
2169
|
+
}
|
|
2170
|
+
try:
|
|
2171
|
+
discovery_data = json.loads(discovery_result)
|
|
2172
|
+
if discovery_data.get("success"):
|
|
2173
|
+
# CURSOR STATUS FIX: Limit available_nodes to prevent large responses
|
|
2174
|
+
discovered_nodes = discovery_data.get("discovered_nodes", {})
|
|
2175
|
+
limited_nodes = {}
|
|
2176
|
+
|
|
2177
|
+
for category, nodes in discovered_nodes.items():
|
|
2178
|
+
if isinstance(nodes, list):
|
|
2179
|
+
# Limit each category to first 10 nodes
|
|
2180
|
+
limited_nodes[category] = nodes[:10]
|
|
2181
|
+
if len(nodes) > 10:
|
|
2182
|
+
limited_nodes[category].append(f"... and {len(nodes) - 10} more {category} nodes")
|
|
2183
|
+
else:
|
|
2184
|
+
limited_nodes[category] = nodes
|
|
2185
|
+
|
|
2186
|
+
response["available_nodes"] = limited_nodes
|
|
2187
|
+
response["node_discovery"] = {
|
|
2188
|
+
"success": True,
|
|
2189
|
+
"total_nodes_found": discovery_data.get("total_nodes_found", 0),
|
|
2190
|
+
"unreal_version": discovery_data.get("unreal_version", "Unknown"),
|
|
2191
|
+
"limited": True,
|
|
2192
|
+
"note": "Node lists limited to 10 per category for Cursor compatibility"
|
|
2193
|
+
}
|
|
2194
|
+
print(f"Node discovery completed: {discovery_data.get('total_nodes_found', 0)} nodes found (limited for Cursor)")
|
|
2195
|
+
else:
|
|
2196
|
+
response["node_discovery"] = {
|
|
2197
|
+
"success": False,
|
|
2198
|
+
"error": discovery_data.get("error", "Discovery failed")
|
|
2199
|
+
}
|
|
2200
|
+
print("Node discovery failed, but Blueprint context still available")
|
|
2201
|
+
except Exception as e:
|
|
2202
|
+
response["node_discovery"] = {
|
|
2203
|
+
"success": False,
|
|
2204
|
+
"error": f"Discovery parsing error: {str(e)}"
|
|
2205
|
+
}
|
|
2206
|
+
print(f"Node discovery parsing failed: {str(e)}")
|
|
2207
|
+
|
|
2208
|
+
# CURSOR STATUS FIX: Limit response size to prevent truncation
|
|
2209
|
+
response_str = json.dumps(response, indent=2)
|
|
2210
|
+
response_size = len(response_str.encode('utf-8'))
|
|
2211
|
+
|
|
2212
|
+
# If response is too large (>80KB), truncate the all_nodes data
|
|
2213
|
+
if response_size > 80 * 1024: # 80KB limit
|
|
2214
|
+
print(f"Blueprint context response too large ({response_size} bytes), truncating node data...")
|
|
2215
|
+
|
|
2216
|
+
# Keep essential data but limit all_nodes
|
|
2217
|
+
if "all_nodes" in response and "nodes" in response["all_nodes"]:
|
|
2218
|
+
original_nodes = response["all_nodes"]["nodes"]
|
|
2219
|
+
node_count = len(original_nodes)
|
|
2220
|
+
|
|
2221
|
+
# Keep only first 3 nodes with essential data
|
|
2222
|
+
truncated_nodes = []
|
|
2223
|
+
for i, node in enumerate(original_nodes[:3]):
|
|
2224
|
+
truncated_node = {
|
|
2225
|
+
"title": node.get("title", ""),
|
|
2226
|
+
"class_name": node.get("class_name", ""),
|
|
2227
|
+
"pos_x": node.get("pos_x", 0),
|
|
2228
|
+
"pos_y": node.get("pos_y", 0),
|
|
2229
|
+
"guid": node.get("guid", ""),
|
|
2230
|
+
"is_enabled": node.get("is_enabled", True)
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
# Keep only essential pins (first 2)
|
|
2234
|
+
if "pins" in node:
|
|
2235
|
+
truncated_node["pins"] = []
|
|
2236
|
+
for pin in node["pins"][:2]:
|
|
2237
|
+
truncated_pin = {
|
|
2238
|
+
"name": pin.get("name", ""),
|
|
2239
|
+
"direction": pin.get("direction", ""),
|
|
2240
|
+
"type": pin.get("type", ""),
|
|
2241
|
+
"connected": pin.get("connected", False)
|
|
2242
|
+
}
|
|
2243
|
+
truncated_node["pins"].append(truncated_pin)
|
|
2244
|
+
|
|
2245
|
+
if len(node["pins"]) > 2:
|
|
2246
|
+
truncated_node["pins"].append({
|
|
2247
|
+
"name": f"... and {len(node['pins']) - 2} more pins",
|
|
2248
|
+
"direction": "Info",
|
|
2249
|
+
"type": "truncated",
|
|
2250
|
+
"connected": False
|
|
2251
|
+
})
|
|
2252
|
+
|
|
2253
|
+
truncated_nodes.append(truncated_node)
|
|
2254
|
+
|
|
2255
|
+
# Add summary info
|
|
2256
|
+
if node_count > 3:
|
|
2257
|
+
truncated_nodes.append({
|
|
2258
|
+
"title": f"... and {node_count - 3} more nodes (use get_all_nodes_in_graph for full data)",
|
|
2259
|
+
"class_name": "TruncatedInfo",
|
|
2260
|
+
"pos_x": 0,
|
|
2261
|
+
"pos_y": 0,
|
|
2262
|
+
"guid": "TRUNCATED",
|
|
2263
|
+
"is_enabled": True,
|
|
2264
|
+
"pins": []
|
|
2265
|
+
})
|
|
2266
|
+
|
|
2267
|
+
response["all_nodes"]["nodes"] = truncated_nodes
|
|
2268
|
+
response["all_nodes"]["total_nodes"] = node_count
|
|
2269
|
+
response["all_nodes"]["truncated"] = True
|
|
2270
|
+
response["all_nodes"]["truncation_reason"] = "Response too large for Cursor MCP - use get_all_nodes_in_graph for complete data"
|
|
2271
|
+
|
|
2272
|
+
# Recalculate response size
|
|
2273
|
+
response_str = json.dumps(response, indent=2)
|
|
2274
|
+
new_size = len(response_str.encode('utf-8'))
|
|
2275
|
+
print(f"Response truncated: {response_size} -> {new_size} bytes")
|
|
2276
|
+
|
|
2277
|
+
# Return the (possibly truncated) context as JSON for AI analysis
|
|
2278
|
+
return response_str
|
|
2279
|
+
else:
|
|
2280
|
+
return f"Failed to get Blueprint context: {response.get('error', 'No active Blueprint editor found')}"
|
|
2281
|
+
|
|
2282
|
+
except Exception as e:
|
|
2283
|
+
return f"Error getting Blueprint context: {str(e)}"
|
|
2284
|
+
|
|
2285
|
+
def _validate_blueprint_for_functions(blueprint_context: dict) -> dict:
|
|
2286
|
+
"""
|
|
2287
|
+
Validate if a Blueprint can have custom functions added to it.
|
|
2288
|
+
|
|
2289
|
+
Args:
|
|
2290
|
+
blueprint_context: Blueprint context information
|
|
2291
|
+
|
|
2292
|
+
Returns:
|
|
2293
|
+
dict: Validation result with success status and recommendations
|
|
2294
|
+
"""
|
|
2295
|
+
blueprint_info = blueprint_context.get("blueprint_info", {})
|
|
2296
|
+
blueprint_type = blueprint_info.get("type", "")
|
|
2297
|
+
blueprint_name = blueprint_info.get("name", "Unknown")
|
|
2298
|
+
blueprint_path = blueprint_info.get("path", "")
|
|
2299
|
+
|
|
2300
|
+
# Level Script Blueprints DO support custom functions - removed incorrect restriction
|
|
2301
|
+
|
|
2302
|
+
return {
|
|
2303
|
+
"can_add_functions": True,
|
|
2304
|
+
"blueprint_type": blueprint_type,
|
|
2305
|
+
"blueprint_name": blueprint_name
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
def _generate_blueprint_function_from_context(function_description: str, analysis_approach: str = "smart",
|
|
2309
|
+
target_blueprint_path: str = None) -> str:
|
|
2310
|
+
"""
|
|
2311
|
+
Generate a complete Blueprint function based on the current Blueprint context and a description.
|
|
2312
|
+
This tool analyzes the existing Blueprint structure and creates contextually appropriate functions.
|
|
2313
|
+
|
|
2314
|
+
IMPORTANT: Level Script Blueprints do not support custom functions. If targeting a Level Script,
|
|
2315
|
+
this tool will suggest creating a regular Blueprint class instead.
|
|
2316
|
+
|
|
2317
|
+
Args:
|
|
2318
|
+
function_description: Natural language description of what the function should do
|
|
2319
|
+
(e.g., "Create a health system that damages the player when they take damage")
|
|
2320
|
+
analysis_approach: How to analyze the context - "smart" (default), "minimal", or "comprehensive"
|
|
2321
|
+
target_blueprint_path: Specific Blueprint to target (optional, uses active Blueprint if not specified)
|
|
2322
|
+
|
|
2323
|
+
Returns:
|
|
2324
|
+
JSON string containing the generated function details and implementation steps
|
|
2325
|
+
|
|
2326
|
+
Example:
|
|
2327
|
+
generate_blueprint_function_from_context(
|
|
2328
|
+
"Add a jump ability that plays a sound effect and particle effect",
|
|
2329
|
+
"smart"
|
|
2330
|
+
)
|
|
2331
|
+
|
|
2332
|
+
This tool will:
|
|
2333
|
+
1. Get current Blueprint context (nodes, connections, variables, etc.)
|
|
2334
|
+
2. Analyze existing patterns and architecture
|
|
2335
|
+
3. Generate appropriate function with inputs/outputs
|
|
2336
|
+
4. Create node sequence that integrates well with existing code
|
|
2337
|
+
5. Return implementation details and reasoning
|
|
2338
|
+
"""
|
|
2339
|
+
try:
|
|
2340
|
+
# Use cached context if available, otherwise fetch fresh
|
|
2341
|
+
context_response = get_cached_blueprint_context()
|
|
2342
|
+
|
|
2343
|
+
# If specific Blueprint path is provided or cache is invalid, fetch fresh
|
|
2344
|
+
if target_blueprint_path or not context_response:
|
|
2345
|
+
context_cmd = {
|
|
2346
|
+
"type": "get_blueprint_context",
|
|
2347
|
+
"include_editor_state": True,
|
|
2348
|
+
"include_graph_info": True,
|
|
2349
|
+
"include_selected_nodes": True,
|
|
2350
|
+
"include_open_editors": False
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
if target_blueprint_path:
|
|
2354
|
+
context_cmd["specific_blueprint_path"] = target_blueprint_path
|
|
2355
|
+
|
|
2356
|
+
context_response = send_to_unreal(context_cmd)
|
|
2357
|
+
|
|
2358
|
+
# Update cache with fresh context
|
|
2359
|
+
if context_response.get("success"):
|
|
2360
|
+
global _blueprint_context_cache
|
|
2361
|
+
_blueprint_context_cache["cached_context"] = context_response
|
|
2362
|
+
_blueprint_context_cache["cache_timestamp"] = time.time()
|
|
2363
|
+
_blueprint_context_cache["cache_blueprint_path"] = context_response.get("active_blueprint", {}).get("path", "")
|
|
2364
|
+
|
|
2365
|
+
if not context_response or not context_response.get("success"):
|
|
2366
|
+
return f"Failed to get Blueprint context: {context_response.get('error', 'Unknown error') if context_response else 'No context available'}"
|
|
2367
|
+
|
|
2368
|
+
# Level Script Blueprints DO support custom functions - removed incorrect validation
|
|
2369
|
+
|
|
2370
|
+
# Analyze the Blueprint context for function generation
|
|
2371
|
+
analysis = analyze_blueprint_for_function_generation(context_response, function_description, analysis_approach)
|
|
2372
|
+
|
|
2373
|
+
# Generate the function specification
|
|
2374
|
+
function_spec = generate_function_specification(analysis, function_description)
|
|
2375
|
+
|
|
2376
|
+
# Create implementation plan
|
|
2377
|
+
implementation_plan = create_implementation_plan(function_spec, analysis)
|
|
2378
|
+
|
|
2379
|
+
# Return comprehensive function generation result
|
|
2380
|
+
result = {
|
|
2381
|
+
"success": True,
|
|
2382
|
+
"function_description": function_description,
|
|
2383
|
+
"target_blueprint": analysis.get("blueprint_info", {}).get("name", "Unknown"),
|
|
2384
|
+
"analysis_approach": analysis_approach,
|
|
2385
|
+
"context_analysis": analysis,
|
|
2386
|
+
"function_specification": function_spec,
|
|
2387
|
+
"implementation_plan": implementation_plan,
|
|
2388
|
+
"integration_suggestions": generate_integration_suggestions(analysis, function_spec)
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
return json.dumps(result, indent=2)
|
|
2392
|
+
|
|
2393
|
+
except Exception as e:
|
|
2394
|
+
return f"Error generating Blueprint function: {str(e)}"
|
|
2395
|
+
|
|
2396
|
+
def analyze_blueprint_for_function_generation(context, function_description, approach):
|
|
2397
|
+
"""Analyze Blueprint context to inform function generation"""
|
|
2398
|
+
analysis = {
|
|
2399
|
+
"blueprint_info": {},
|
|
2400
|
+
"existing_patterns": [],
|
|
2401
|
+
"available_variables": [],
|
|
2402
|
+
"node_usage_patterns": [],
|
|
2403
|
+
"architectural_insights": [],
|
|
2404
|
+
"integration_opportunities": []
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
# Extract basic Blueprint info
|
|
2408
|
+
if context.get("active_blueprint"):
|
|
2409
|
+
analysis["blueprint_info"] = {
|
|
2410
|
+
"name": context["active_blueprint"].get("name", "Unknown"),
|
|
2411
|
+
"path": context["active_blueprint"].get("path", ""),
|
|
2412
|
+
"type": context.get("editor_state", {}).get("blueprint_type", "Unknown")
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
# Analyze existing nodes and patterns
|
|
2416
|
+
all_nodes = context.get("all_nodes", {}).get("nodes", [])
|
|
2417
|
+
if all_nodes:
|
|
2418
|
+
# Count node types
|
|
2419
|
+
node_types = {}
|
|
2420
|
+
execution_patterns = []
|
|
2421
|
+
|
|
2422
|
+
for node in all_nodes:
|
|
2423
|
+
node_class = node.get("class_name", "Unknown")
|
|
2424
|
+
node_types[node_class] = node_types.get(node_class, 0) + 1
|
|
2425
|
+
|
|
2426
|
+
# Analyze execution patterns
|
|
2427
|
+
if node.get("pins"):
|
|
2428
|
+
exec_outputs = [pin for pin in node["pins"]
|
|
2429
|
+
if pin.get("type") == "exec" and pin.get("direction") == "Output"]
|
|
2430
|
+
if exec_outputs and any(pin.get("connected") for pin in exec_outputs):
|
|
2431
|
+
execution_patterns.append({
|
|
2432
|
+
"node": node.get("title", "Unknown"),
|
|
2433
|
+
"class": node_class,
|
|
2434
|
+
"connects_to": [conn.get("connected_node_title")
|
|
2435
|
+
for pin in exec_outputs
|
|
2436
|
+
for conn in pin.get("connections", [])]
|
|
2437
|
+
})
|
|
2438
|
+
|
|
2439
|
+
analysis["node_usage_patterns"] = [
|
|
2440
|
+
{"type": node_type, "count": count}
|
|
2441
|
+
for node_type, count in node_types.items()
|
|
2442
|
+
]
|
|
2443
|
+
analysis["execution_patterns"] = execution_patterns
|
|
2444
|
+
|
|
2445
|
+
# Analyze architectural patterns (enhanced)
|
|
2446
|
+
node_titles = [node.get("title", "") for node in all_nodes]
|
|
2447
|
+
node_classes = [node.get("class_name", "") for node in all_nodes]
|
|
2448
|
+
|
|
2449
|
+
# Check for common Blueprint patterns
|
|
2450
|
+
if any("Event" in title for title in node_titles):
|
|
2451
|
+
analysis["architectural_insights"].append("Blueprint uses event-driven architecture")
|
|
2452
|
+
if any("Widget" in title or "UI" in title for title in node_titles):
|
|
2453
|
+
analysis["architectural_insights"].append("Blueprint includes UI/Widget functionality")
|
|
2454
|
+
if any("Component" in title or "Component" in cls for title, cls in zip(node_titles, node_classes)):
|
|
2455
|
+
analysis["architectural_insights"].append("Blueprint uses component-based architecture")
|
|
2456
|
+
if any("Timer" in title for title in node_titles):
|
|
2457
|
+
analysis["architectural_insights"].append("Blueprint uses timer-based functionality")
|
|
2458
|
+
if any("Input" in title for title in node_titles):
|
|
2459
|
+
analysis["architectural_insights"].append("Blueprint handles input events")
|
|
2460
|
+
if any("Animation" in title or "Anim" in title for title in node_titles):
|
|
2461
|
+
analysis["architectural_insights"].append("Blueprint includes animation system")
|
|
2462
|
+
if any("Collision" in title or "Overlap" in title for title in node_titles):
|
|
2463
|
+
analysis["architectural_insights"].append("Blueprint handles collision/overlap events")
|
|
2464
|
+
if any("AI" in title or "Pawn" in title for title in node_titles):
|
|
2465
|
+
analysis["architectural_insights"].append("Blueprint includes AI/character behavior")
|
|
2466
|
+
|
|
2467
|
+
# Analyze variable usage patterns from nodes
|
|
2468
|
+
variable_nodes = [node for node in all_nodes if "Variable" in node.get("class_name", "") or "Get" in node.get("title", "") or "Set" in node.get("title", "")]
|
|
2469
|
+
if variable_nodes:
|
|
2470
|
+
analysis["architectural_insights"].append(f"Blueprint uses {len(variable_nodes)} variable operations")
|
|
2471
|
+
|
|
2472
|
+
# Detect common game patterns
|
|
2473
|
+
if any(word in " ".join(node_titles).lower() for word in ["health", "damage", "death"]):
|
|
2474
|
+
analysis["architectural_insights"].append("Blueprint implements health/damage system")
|
|
2475
|
+
if any(word in " ".join(node_titles).lower() for word in ["score", "points", "win", "lose"]):
|
|
2476
|
+
analysis["architectural_insights"].append("Blueprint implements scoring/game state system")
|
|
2477
|
+
if any(word in " ".join(node_titles).lower() for word in ["spawn", "enemy", "projectile"]):
|
|
2478
|
+
analysis["architectural_insights"].append("Blueprint implements spawning/combat system")
|
|
2479
|
+
|
|
2480
|
+
# Suggest integration opportunities based on existing nodes
|
|
2481
|
+
existing_node_titles = [node.get("title", "") for node in all_nodes]
|
|
2482
|
+
if "Event BeginPlay" in existing_node_titles:
|
|
2483
|
+
analysis["integration_opportunities"].append("Can integrate with game initialization")
|
|
2484
|
+
if any("Create" in title for title in existing_node_titles):
|
|
2485
|
+
analysis["integration_opportunities"].append("Can extend object creation patterns")
|
|
2486
|
+
|
|
2487
|
+
return analysis
|
|
2488
|
+
|
|
2489
|
+
def generate_function_specification(analysis, function_description):
|
|
2490
|
+
"""Generate detailed function specification based on analysis"""
|
|
2491
|
+
|
|
2492
|
+
# Determine function name from description (enhanced)
|
|
2493
|
+
import re
|
|
2494
|
+
|
|
2495
|
+
# Extract meaningful words and create a proper function name
|
|
2496
|
+
words = function_description.lower().split()
|
|
2497
|
+
|
|
2498
|
+
# Remove common stop words and get meaningful terms
|
|
2499
|
+
stop_words = {"a", "an", "the", "to", "for", "with", "by", "from", "and", "or", "but", "in", "on", "at", "that", "this", "is", "are", "was", "were", "be", "been", "being"}
|
|
2500
|
+
meaningful_words = [word for word in words if word.isalpha() and word not in stop_words]
|
|
2501
|
+
|
|
2502
|
+
# Take first 2-3 meaningful words and capitalize them (without adding "Function" suffix)
|
|
2503
|
+
if meaningful_words:
|
|
2504
|
+
if len(meaningful_words) >= 3:
|
|
2505
|
+
function_name = ''.join(word.capitalize() for word in meaningful_words[:3])
|
|
2506
|
+
elif len(meaningful_words) == 2:
|
|
2507
|
+
function_name = ''.join(word.capitalize() for word in meaningful_words)
|
|
2508
|
+
else:
|
|
2509
|
+
function_name = meaningful_words[0].capitalize()
|
|
2510
|
+
else:
|
|
2511
|
+
function_name = "GeneratedFunction"
|
|
2512
|
+
|
|
2513
|
+
# Ensure function name starts with a verb if possible
|
|
2514
|
+
action_words = {"create", "add", "remove", "delete", "update", "set", "get", "play", "stop", "start", "enable", "disable", "spawn", "destroy", "move", "rotate", "scale", "handle", "process", "check", "validate"}
|
|
2515
|
+
first_word = words[0] if words else ""
|
|
2516
|
+
|
|
2517
|
+
if first_word in action_words:
|
|
2518
|
+
# Good, starts with action word
|
|
2519
|
+
pass
|
|
2520
|
+
elif any(word in action_words for word in words[:3]):
|
|
2521
|
+
# Move action word to front
|
|
2522
|
+
action_word = next(word for word in words[:3] if word in action_words)
|
|
2523
|
+
other_words = [word for word in meaningful_words[:3] if word != action_word]
|
|
2524
|
+
function_name = action_word.capitalize() + ''.join(word.capitalize() for word in other_words)
|
|
2525
|
+
|
|
2526
|
+
# Clean up the function name
|
|
2527
|
+
function_name = re.sub(r'[^a-zA-Z0-9]', '', function_name)
|
|
2528
|
+
if not function_name or not function_name[0].isalpha():
|
|
2529
|
+
function_name = "GeneratedFunction"
|
|
2530
|
+
|
|
2531
|
+
spec = {
|
|
2532
|
+
"function_name": function_name,
|
|
2533
|
+
"description": function_description,
|
|
2534
|
+
"inputs": [],
|
|
2535
|
+
"outputs": [],
|
|
2536
|
+
"suggested_nodes": [],
|
|
2537
|
+
"complexity_estimate": "Medium"
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
# Analyze description for common patterns and suggest inputs/outputs
|
|
2541
|
+
desc_lower = function_description.lower()
|
|
2542
|
+
|
|
2543
|
+
# Enhanced input patterns based on actual description analysis
|
|
2544
|
+
words = function_description.lower().split()
|
|
2545
|
+
|
|
2546
|
+
# Look for specific damage/health parameters
|
|
2547
|
+
if "damage" in desc_lower:
|
|
2548
|
+
if any(word in desc_lower for word in ["amount", "value", "points"]):
|
|
2549
|
+
spec["inputs"].append({"name": "DamageAmount", "type": "float", "description": "Amount of damage to apply"})
|
|
2550
|
+
if "player" in desc_lower or "character" in desc_lower:
|
|
2551
|
+
spec["inputs"].append({"name": "DamagedActor", "type": "object", "description": "Actor receiving damage"})
|
|
2552
|
+
|
|
2553
|
+
# Look for health system parameters
|
|
2554
|
+
if "health" in desc_lower:
|
|
2555
|
+
if "max" in desc_lower or "maximum" in desc_lower:
|
|
2556
|
+
spec["inputs"].append({"name": "MaxHealth", "type": "float", "description": "Maximum health value"})
|
|
2557
|
+
if "current" in desc_lower or "set" in desc_lower:
|
|
2558
|
+
spec["inputs"].append({"name": "NewHealthValue", "type": "float", "description": "New health amount"})
|
|
2559
|
+
|
|
2560
|
+
# Look for movement/jump parameters
|
|
2561
|
+
if "jump" in desc_lower:
|
|
2562
|
+
if "force" in desc_lower or "power" in desc_lower or "strength" in desc_lower:
|
|
2563
|
+
spec["inputs"].append({"name": "JumpForce", "type": "vector", "description": "Jump force vector"})
|
|
2564
|
+
|
|
2565
|
+
# Look for spawn parameters
|
|
2566
|
+
if "spawn" in desc_lower or "create" in desc_lower:
|
|
2567
|
+
if "location" in desc_lower or "position" in desc_lower:
|
|
2568
|
+
spec["inputs"].append({"name": "SpawnLocation", "type": "vector", "description": "Location to spawn object"})
|
|
2569
|
+
if "class" in desc_lower or "type" in desc_lower:
|
|
2570
|
+
spec["inputs"].append({"name": "ActorClass", "type": "class", "description": "Class of actor to spawn"})
|
|
2571
|
+
|
|
2572
|
+
# Look for UI/widget parameters
|
|
2573
|
+
if any(word in desc_lower for word in ["widget", "ui", "screen", "menu"]):
|
|
2574
|
+
if "show" in desc_lower or "display" in desc_lower:
|
|
2575
|
+
spec["inputs"].append({"name": "WidgetClass", "type": "class", "description": "Widget class to display"})
|
|
2576
|
+
|
|
2577
|
+
# Look for sound parameters
|
|
2578
|
+
if "sound" in desc_lower or "audio" in desc_lower:
|
|
2579
|
+
spec["inputs"].append({"name": "SoundToPlay", "type": "object", "description": "Sound asset to play"})
|
|
2580
|
+
if "location" in desc_lower:
|
|
2581
|
+
spec["inputs"].append({"name": "SoundLocation", "type": "vector", "description": "Location to play sound"})
|
|
2582
|
+
|
|
2583
|
+
# Look for timer parameters
|
|
2584
|
+
if "timer" in desc_lower:
|
|
2585
|
+
if "delay" in desc_lower or "time" in desc_lower or "duration" in desc_lower:
|
|
2586
|
+
spec["inputs"].append({"name": "TimerDuration", "type": "float", "description": "Timer duration in seconds"})
|
|
2587
|
+
|
|
2588
|
+
# Common output patterns
|
|
2589
|
+
if any(word in desc_lower for word in ["success", "result", "completed"]):
|
|
2590
|
+
spec["outputs"].append({"name": "Success", "type": "boolean", "description": "Whether operation succeeded"})
|
|
2591
|
+
if any(word in desc_lower for word in ["create", "spawn", "generate"]):
|
|
2592
|
+
spec["outputs"].append({"name": "CreatedObject", "type": "object", "description": "The created object reference"})
|
|
2593
|
+
|
|
2594
|
+
# Suggest nodes based on description (enhanced with more patterns)
|
|
2595
|
+
if "damage" in desc_lower:
|
|
2596
|
+
spec["suggested_nodes"].extend(["Apply Damage", "Set Health", "Branch", "Greater", "Print String"])
|
|
2597
|
+
if "sound" in desc_lower:
|
|
2598
|
+
spec["suggested_nodes"].extend(["Play Sound", "Play Sound at Location", "Spawn Sound 2D"])
|
|
2599
|
+
if "jump" in desc_lower:
|
|
2600
|
+
spec["suggested_nodes"].extend(["Launch Character", "Set Movement Mode", "Branch", "Get Velocity"])
|
|
2601
|
+
if "widget" in desc_lower or "ui" in desc_lower:
|
|
2602
|
+
spec["suggested_nodes"].extend(["Create Widget", "Add to Viewport", "Set Visibility"])
|
|
2603
|
+
if "timer" in desc_lower:
|
|
2604
|
+
spec["suggested_nodes"].extend(["Set Timer by Function Name", "Clear Timer", "Get Timer Remaining"])
|
|
2605
|
+
if "move" in desc_lower or "movement" in desc_lower:
|
|
2606
|
+
spec["suggested_nodes"].extend(["Add Movement Input", "Set Actor Location", "Get Forward Vector"])
|
|
2607
|
+
if "health" in desc_lower:
|
|
2608
|
+
spec["suggested_nodes"].extend(["Set Health", "Get Health", "Branch", "Less Equal"])
|
|
2609
|
+
if "spawn" in desc_lower or "create" in desc_lower:
|
|
2610
|
+
spec["suggested_nodes"].extend(["Spawn Actor from Class", "Set Actor Location", "IsValid"])
|
|
2611
|
+
if "collision" in desc_lower or "overlap" in desc_lower:
|
|
2612
|
+
spec["suggested_nodes"].extend(["Set Collision Response", "OnComponentBeginOverlap", "Cast To"])
|
|
2613
|
+
if "animation" in desc_lower or "anim" in desc_lower:
|
|
2614
|
+
spec["suggested_nodes"].extend(["Play Animation", "Set Animation Mode", "Get Anim Instance"])
|
|
2615
|
+
if "input" in desc_lower:
|
|
2616
|
+
spec["suggested_nodes"].extend(["InputAction", "InputAxis", "Enable Input", "Disable Input"])
|
|
2617
|
+
|
|
2618
|
+
# Add common utility nodes for most functions
|
|
2619
|
+
if not spec["suggested_nodes"]: # If no specific nodes were suggested
|
|
2620
|
+
spec["suggested_nodes"].extend(["Branch", "Print String", "IsValid"])
|
|
2621
|
+
|
|
2622
|
+
# Always add debug/utility nodes for complex functions
|
|
2623
|
+
if len(spec["suggested_nodes"]) > 3:
|
|
2624
|
+
spec["suggested_nodes"].append("Print String") # For debugging
|
|
2625
|
+
|
|
2626
|
+
# Context-aware node suggestions based on existing Blueprint patterns
|
|
2627
|
+
existing_patterns = analysis.get("node_usage_patterns", [])
|
|
2628
|
+
architectural_insights = analysis.get("architectural_insights", [])
|
|
2629
|
+
|
|
2630
|
+
# Suggest nodes based on existing Blueprint architecture
|
|
2631
|
+
if any("health" in insight.lower() for insight in architectural_insights):
|
|
2632
|
+
if "health" in desc_lower and "Set Health" not in spec["suggested_nodes"]:
|
|
2633
|
+
spec["suggested_nodes"].insert(0, "Set Health")
|
|
2634
|
+
|
|
2635
|
+
if any("ui" in insight.lower() or "widget" in insight.lower() for insight in architectural_insights):
|
|
2636
|
+
if any(word in desc_lower for word in ["show", "display", "update"]) and "Get Widget" not in spec["suggested_nodes"]:
|
|
2637
|
+
spec["suggested_nodes"].append("Get Widget")
|
|
2638
|
+
|
|
2639
|
+
if any("input" in insight.lower() for insight in architectural_insights):
|
|
2640
|
+
if any(word in desc_lower for word in ["enable", "disable", "control"]):
|
|
2641
|
+
spec["suggested_nodes"].append("Set Input Mode")
|
|
2642
|
+
|
|
2643
|
+
# Add commonly used nodes from existing Blueprint
|
|
2644
|
+
common_node_types = [pattern["type"] for pattern in existing_patterns if pattern["count"] > 2]
|
|
2645
|
+
for node_type in common_node_types[:2]: # Add up to 2 commonly used node types
|
|
2646
|
+
if any(common in node_type for common in ["K2Node_CallFunction", "K2Node_Event"]):
|
|
2647
|
+
continue # Skip these base types
|
|
2648
|
+
if node_type not in spec["suggested_nodes"]:
|
|
2649
|
+
spec["suggested_nodes"].append(node_type.replace("K2Node_", "").replace("_", " "))
|
|
2650
|
+
|
|
2651
|
+
# Remove duplicates while preserving order
|
|
2652
|
+
seen = set()
|
|
2653
|
+
spec["suggested_nodes"] = [node for node in spec["suggested_nodes"] if not (node in seen or seen.add(node))]
|
|
2654
|
+
|
|
2655
|
+
# Ensure reasonable node count (between 2-8 nodes typically)
|
|
2656
|
+
if len(spec["suggested_nodes"]) > 8:
|
|
2657
|
+
spec["suggested_nodes"] = spec["suggested_nodes"][:8]
|
|
2658
|
+
elif len(spec["suggested_nodes"]) < 2:
|
|
2659
|
+
spec["suggested_nodes"].extend(["Branch", "Print String"])
|
|
2660
|
+
|
|
2661
|
+
return spec
|
|
2662
|
+
|
|
2663
|
+
def create_implementation_plan(function_spec, analysis):
|
|
2664
|
+
"""Create step-by-step implementation plan"""
|
|
2665
|
+
plan = {
|
|
2666
|
+
"steps": [],
|
|
2667
|
+
"estimated_nodes": len(function_spec["suggested_nodes"]) + 2, # +2 for entry/result
|
|
2668
|
+
"integration_points": [],
|
|
2669
|
+
"considerations": []
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
# Step 1: Create function
|
|
2673
|
+
plan["steps"].append({
|
|
2674
|
+
"step": 1,
|
|
2675
|
+
"action": "create_function",
|
|
2676
|
+
"description": f"Create function '{function_spec['function_name']}'",
|
|
2677
|
+
"details": {
|
|
2678
|
+
"function_name": function_spec["function_name"],
|
|
2679
|
+
"inputs": function_spec["inputs"],
|
|
2680
|
+
"outputs": function_spec["outputs"]
|
|
2681
|
+
}
|
|
2682
|
+
})
|
|
2683
|
+
|
|
2684
|
+
# Step 2: Add nodes
|
|
2685
|
+
for i, node_type in enumerate(function_spec["suggested_nodes"]):
|
|
2686
|
+
plan["steps"].append({
|
|
2687
|
+
"step": i + 2,
|
|
2688
|
+
"action": "add_node",
|
|
2689
|
+
"description": f"Add {node_type} node",
|
|
2690
|
+
"details": {
|
|
2691
|
+
"node_type": node_type,
|
|
2692
|
+
"position": [300 + i * 200, 100],
|
|
2693
|
+
"purpose": f"Implements part of: {function_spec['description']}"
|
|
2694
|
+
}
|
|
2695
|
+
})
|
|
2696
|
+
|
|
2697
|
+
# Step 3: Connect nodes
|
|
2698
|
+
plan["steps"].append({
|
|
2699
|
+
"step": len(function_spec["suggested_nodes"]) + 2,
|
|
2700
|
+
"action": "connect_nodes",
|
|
2701
|
+
"description": "Connect all nodes in execution order",
|
|
2702
|
+
"details": "Sequential execution flow from function entry to result"
|
|
2703
|
+
})
|
|
2704
|
+
|
|
2705
|
+
# Integration suggestions
|
|
2706
|
+
existing_patterns = analysis.get("execution_patterns", [])
|
|
2707
|
+
if existing_patterns:
|
|
2708
|
+
plan["integration_points"].append(
|
|
2709
|
+
f"Can integrate with existing pattern: {existing_patterns[0].get('node', 'Unknown')}"
|
|
2710
|
+
)
|
|
2711
|
+
|
|
2712
|
+
# Considerations
|
|
2713
|
+
blueprint_type = analysis.get("blueprint_info", {}).get("type", "")
|
|
2714
|
+
if blueprint_type == "LevelScript":
|
|
2715
|
+
plan["considerations"].append("Level Blueprint - consider using global events")
|
|
2716
|
+
if "Widget" in str(analysis):
|
|
2717
|
+
plan["considerations"].append("UI elements present - consider widget interactions")
|
|
2718
|
+
|
|
2719
|
+
return plan
|
|
2720
|
+
|
|
2721
|
+
def generate_integration_suggestions(analysis, function_spec):
|
|
2722
|
+
"""Generate suggestions for integrating the new function"""
|
|
2723
|
+
suggestions = []
|
|
2724
|
+
|
|
2725
|
+
# Based on existing nodes
|
|
2726
|
+
existing_events = []
|
|
2727
|
+
all_nodes = analysis.get("execution_patterns", [])
|
|
2728
|
+
|
|
2729
|
+
for pattern in all_nodes:
|
|
2730
|
+
if "Event" in pattern.get("node", ""):
|
|
2731
|
+
existing_events.append(pattern["node"])
|
|
2732
|
+
|
|
2733
|
+
if existing_events:
|
|
2734
|
+
suggestions.append({
|
|
2735
|
+
"type": "event_integration",
|
|
2736
|
+
"description": f"Call {function_spec['function_name']} from existing event",
|
|
2737
|
+
"target": existing_events[0],
|
|
2738
|
+
"implementation": f"Add call to {function_spec['function_name']} after {existing_events[0]}"
|
|
2739
|
+
})
|
|
2740
|
+
|
|
2741
|
+
# Based on Blueprint type
|
|
2742
|
+
blueprint_type = analysis.get("blueprint_info", {}).get("type", "")
|
|
2743
|
+
if blueprint_type == "LevelScript":
|
|
2744
|
+
suggestions.append({
|
|
2745
|
+
"type": "level_limitation_warning",
|
|
2746
|
+
"description": "Level Script Blueprints cannot have custom functions",
|
|
2747
|
+
"implementation": "Create a regular Blueprint class (Actor, Object, etc.) for custom functions, or use events in the Level Script"
|
|
2748
|
+
})
|
|
2749
|
+
suggestions.append({
|
|
2750
|
+
"type": "level_integration",
|
|
2751
|
+
"description": "Integrate with level-specific gameplay events",
|
|
2752
|
+
"implementation": "Consider calling from BeginPlay or level-specific triggers"
|
|
2753
|
+
})
|
|
2754
|
+
|
|
2755
|
+
return suggestions
|
|
2756
|
+
|
|
2757
|
+
def _implement_generated_blueprint_function(generation_result: str, execute_plan: bool = True) -> str:
|
|
2758
|
+
"""
|
|
2759
|
+
Actually implement a Blueprint function that was generated by generate_blueprint_function_from_context.
|
|
2760
|
+
This tool takes the output from the generator and creates the actual Blueprint function.
|
|
2761
|
+
|
|
2762
|
+
Args:
|
|
2763
|
+
generation_result: JSON string output from generate_blueprint_function_from_context
|
|
2764
|
+
execute_plan: Whether to automatically execute the implementation plan (default: True)
|
|
2765
|
+
|
|
2766
|
+
Returns:
|
|
2767
|
+
JSON string with implementation results and created function details
|
|
2768
|
+
|
|
2769
|
+
Example:
|
|
2770
|
+
1. First generate: result = generate_blueprint_function_from_context("Add jump ability")
|
|
2771
|
+
2. Then implement: implement_generated_blueprint_function(result, True)
|
|
2772
|
+
|
|
2773
|
+
This tool will:
|
|
2774
|
+
1. Parse the generation result
|
|
2775
|
+
2. Create the Blueprint function with specified inputs/outputs
|
|
2776
|
+
3. Add all suggested nodes in correct positions
|
|
2777
|
+
4. Connect nodes according to the implementation plan
|
|
2778
|
+
5. Return success/failure details with function ID for further use
|
|
2779
|
+
"""
|
|
2780
|
+
try:
|
|
2781
|
+
# Parse the generation result
|
|
2782
|
+
try:
|
|
2783
|
+
gen_data = json.loads(generation_result)
|
|
2784
|
+
except json.JSONDecodeError:
|
|
2785
|
+
return json.dumps({
|
|
2786
|
+
"success": False,
|
|
2787
|
+
"error": "Invalid generation result JSON"
|
|
2788
|
+
})
|
|
2789
|
+
|
|
2790
|
+
if not gen_data.get("success"):
|
|
2791
|
+
return json.dumps({
|
|
2792
|
+
"success": False,
|
|
2793
|
+
"error": "Generation result indicates failure"
|
|
2794
|
+
})
|
|
2795
|
+
|
|
2796
|
+
function_spec = gen_data.get("function_specification", {})
|
|
2797
|
+
implementation_plan = gen_data.get("implementation_plan", {})
|
|
2798
|
+
target_blueprint = gen_data.get("target_blueprint", "Unknown")
|
|
2799
|
+
|
|
2800
|
+
# Get the target Blueprint path from context
|
|
2801
|
+
context_cmd = {"type": "get_blueprint_context", "include_editor_state": True}
|
|
2802
|
+
context_response = send_to_unreal(context_cmd)
|
|
2803
|
+
|
|
2804
|
+
if not context_response.get("success"):
|
|
2805
|
+
return json.dumps({
|
|
2806
|
+
"success": False,
|
|
2807
|
+
"error": f"Cannot get Blueprint context: {context_response.get('error')}"
|
|
2808
|
+
})
|
|
2809
|
+
|
|
2810
|
+
blueprint_path = context_response.get("active_blueprint", {}).get("path", "")
|
|
2811
|
+
if not blueprint_path:
|
|
2812
|
+
return json.dumps({
|
|
2813
|
+
"success": False,
|
|
2814
|
+
"error": "No active Blueprint path found"
|
|
2815
|
+
})
|
|
2816
|
+
|
|
2817
|
+
implementation_results = {
|
|
2818
|
+
"success": True,
|
|
2819
|
+
"target_blueprint": target_blueprint,
|
|
2820
|
+
"blueprint_path": blueprint_path,
|
|
2821
|
+
"function_name": function_spec.get("function_name", "Unknown"),
|
|
2822
|
+
"implementation_steps": [],
|
|
2823
|
+
"created_function_id": None,
|
|
2824
|
+
"created_nodes": [],
|
|
2825
|
+
"errors": []
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
if not execute_plan:
|
|
2829
|
+
implementation_results["message"] = "Plan parsed successfully but not executed (execute_plan=False)"
|
|
2830
|
+
return json.dumps(implementation_results, indent=2)
|
|
2831
|
+
|
|
2832
|
+
# Step 1: Create the function (check if it already exists first)
|
|
2833
|
+
step1 = implementation_plan.get("steps", [{}])[0]
|
|
2834
|
+
if step1.get("action") == "create_function":
|
|
2835
|
+
details = step1.get("details", {})
|
|
2836
|
+
function_name = details.get("function_name", "GeneratedFunction")
|
|
2837
|
+
|
|
2838
|
+
# Check if function already exists to prevent duplication
|
|
2839
|
+
check_function_cmd = {
|
|
2840
|
+
"type": "get_all_nodes",
|
|
2841
|
+
"blueprint_path": blueprint_path
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
existing_check = send_to_unreal(check_function_cmd)
|
|
2845
|
+
function_already_exists = False
|
|
2846
|
+
existing_function_id = None
|
|
2847
|
+
|
|
2848
|
+
if existing_check.get("success"):
|
|
2849
|
+
# Check if function with EXACT same name already exists (prevent duplicates)
|
|
2850
|
+
for graph_name, graph_data in existing_check.get("graphs", {}).items():
|
|
2851
|
+
# Only match exact function names, not partial matches
|
|
2852
|
+
graph_clean_name = graph_name.replace(" ", "").lower()
|
|
2853
|
+
function_clean_name = function_name.replace(" ", "").lower()
|
|
2854
|
+
|
|
2855
|
+
if graph_clean_name == function_clean_name:
|
|
2856
|
+
function_already_exists = True
|
|
2857
|
+
# Try to get the function entry node ID
|
|
2858
|
+
for node in graph_data.get("nodes", []):
|
|
2859
|
+
if node.get("type") == "FunctionEntry":
|
|
2860
|
+
existing_function_id = node.get("id")
|
|
2861
|
+
break
|
|
2862
|
+
break
|
|
2863
|
+
|
|
2864
|
+
# If function exists, suggest a unique name instead of reusing
|
|
2865
|
+
if function_already_exists:
|
|
2866
|
+
counter = 2
|
|
2867
|
+
original_name = function_name
|
|
2868
|
+
while function_already_exists:
|
|
2869
|
+
function_name = f"{original_name}{counter}"
|
|
2870
|
+
function_already_exists = False
|
|
2871
|
+
for graph_name, graph_data in existing_check.get("graphs", {}).items():
|
|
2872
|
+
graph_clean_name = graph_name.replace(" ", "").lower()
|
|
2873
|
+
function_clean_name = function_name.replace(" ", "").lower()
|
|
2874
|
+
if graph_clean_name == function_clean_name:
|
|
2875
|
+
function_already_exists = True
|
|
2876
|
+
break
|
|
2877
|
+
counter += 1
|
|
2878
|
+
if counter > 10: # Prevent infinite loop
|
|
2879
|
+
break
|
|
2880
|
+
|
|
2881
|
+
# Update the function name in details
|
|
2882
|
+
details["function_name"] = function_name
|
|
2883
|
+
function_already_exists = False # Create with new name
|
|
2884
|
+
|
|
2885
|
+
if function_already_exists and existing_function_id:
|
|
2886
|
+
# Function already exists, use it instead of creating a new one
|
|
2887
|
+
implementation_results["created_function_id"] = existing_function_id
|
|
2888
|
+
implementation_results["implementation_steps"].append({
|
|
2889
|
+
"step": 1,
|
|
2890
|
+
"action": "use_existing_function",
|
|
2891
|
+
"success": True,
|
|
2892
|
+
"result": f"Using existing function: {function_name}"
|
|
2893
|
+
})
|
|
2894
|
+
function_result = {"success": True, "function_id": existing_function_id}
|
|
2895
|
+
else:
|
|
2896
|
+
# Create new function
|
|
2897
|
+
inputs_json = json.dumps(details.get("inputs", []))
|
|
2898
|
+
outputs_json = json.dumps(details.get("outputs", []))
|
|
2899
|
+
|
|
2900
|
+
create_function_cmd = {
|
|
2901
|
+
"type": "add_function",
|
|
2902
|
+
"blueprint_path": blueprint_path,
|
|
2903
|
+
"function_name": function_name,
|
|
2904
|
+
"inputs": inputs_json,
|
|
2905
|
+
"outputs": outputs_json
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
function_result = send_to_unreal(create_function_cmd)
|
|
2909
|
+
|
|
2910
|
+
if function_result.get("success") or function_result.get("function_id"):
|
|
2911
|
+
function_id = function_result.get("function_id", "")
|
|
2912
|
+
implementation_results["created_function_id"] = function_id
|
|
2913
|
+
implementation_results["implementation_steps"].append({
|
|
2914
|
+
"step": 1,
|
|
2915
|
+
"action": "create_function",
|
|
2916
|
+
"success": True,
|
|
2917
|
+
"result": f"Created function: {details.get('function_name')}"
|
|
2918
|
+
})
|
|
2919
|
+
|
|
2920
|
+
# Step 2: Add nodes to the function
|
|
2921
|
+
suggested_nodes = function_spec.get("suggested_nodes", [])
|
|
2922
|
+
node_positions = {}
|
|
2923
|
+
|
|
2924
|
+
for i, node_type in enumerate(suggested_nodes):
|
|
2925
|
+
add_node_cmd = {
|
|
2926
|
+
"type": "add_node",
|
|
2927
|
+
"blueprint_path": blueprint_path,
|
|
2928
|
+
"function_id": function_id,
|
|
2929
|
+
"node_type": node_type,
|
|
2930
|
+
"node_position": [300 + i * 250, 100 + (i % 2) * 150],
|
|
2931
|
+
"node_properties": {}
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
node_result = send_to_unreal(add_node_cmd)
|
|
2935
|
+
|
|
2936
|
+
if node_result.get("success") or node_result.get("node_id"):
|
|
2937
|
+
node_id = node_result.get("node_id", "")
|
|
2938
|
+
implementation_results["created_nodes"].append({
|
|
2939
|
+
"node_type": node_type,
|
|
2940
|
+
"node_id": node_id,
|
|
2941
|
+
"position": add_node_cmd["node_position"]
|
|
2942
|
+
})
|
|
2943
|
+
node_positions[node_type] = node_id
|
|
2944
|
+
|
|
2945
|
+
implementation_results["implementation_steps"].append({
|
|
2946
|
+
"step": i + 2,
|
|
2947
|
+
"action": "add_node",
|
|
2948
|
+
"success": True,
|
|
2949
|
+
"result": f"Added {node_type} node"
|
|
2950
|
+
})
|
|
2951
|
+
else:
|
|
2952
|
+
implementation_results["errors"].append(f"Failed to add {node_type} node: {node_result.get('error', 'Unknown error')}")
|
|
2953
|
+
|
|
2954
|
+
# Step 3: Connect nodes (basic sequential connection)
|
|
2955
|
+
if len(implementation_results["created_nodes"]) > 1:
|
|
2956
|
+
connections = []
|
|
2957
|
+
|
|
2958
|
+
# Connect function entry to first node
|
|
2959
|
+
if implementation_results["created_nodes"]:
|
|
2960
|
+
first_node = implementation_results["created_nodes"][0]
|
|
2961
|
+
connections.append({
|
|
2962
|
+
"source_node_id": "function_entry", # Will need to get actual entry node ID
|
|
2963
|
+
"source_pin": "exec",
|
|
2964
|
+
"target_node_id": first_node["node_id"],
|
|
2965
|
+
"target_pin": "exec"
|
|
2966
|
+
})
|
|
2967
|
+
|
|
2968
|
+
# Connect nodes sequentially
|
|
2969
|
+
for i in range(len(implementation_results["created_nodes"]) - 1):
|
|
2970
|
+
source_node = implementation_results["created_nodes"][i]
|
|
2971
|
+
target_node = implementation_results["created_nodes"][i + 1]
|
|
2972
|
+
|
|
2973
|
+
connections.append({
|
|
2974
|
+
"source_node_id": source_node["node_id"],
|
|
2975
|
+
"source_pin": "exec", # Default exec pin
|
|
2976
|
+
"target_node_id": target_node["node_id"],
|
|
2977
|
+
"target_pin": "exec"
|
|
2978
|
+
})
|
|
2979
|
+
|
|
2980
|
+
# Attempt to connect nodes
|
|
2981
|
+
if connections:
|
|
2982
|
+
connect_cmd = {
|
|
2983
|
+
"type": "connect_nodes_bulk",
|
|
2984
|
+
"blueprint_path": blueprint_path,
|
|
2985
|
+
"function_id": function_id,
|
|
2986
|
+
"connections": connections
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
connect_result = send_to_unreal(connect_cmd)
|
|
2990
|
+
|
|
2991
|
+
if connect_result.get("success") or connect_result.get("successful_connections", 0) > 0:
|
|
2992
|
+
implementation_results["implementation_steps"].append({
|
|
2993
|
+
"step": len(suggested_nodes) + 2,
|
|
2994
|
+
"action": "connect_nodes",
|
|
2995
|
+
"success": True,
|
|
2996
|
+
"result": f"Connected {connect_result.get('successful_connections', 0)} node pairs"
|
|
2997
|
+
})
|
|
2998
|
+
else:
|
|
2999
|
+
implementation_results["errors"].append(f"Node connection failed: {connect_result.get('error', 'Unknown error')}")
|
|
3000
|
+
|
|
3001
|
+
# Compile the Blueprint
|
|
3002
|
+
compile_cmd = {
|
|
3003
|
+
"type": "compile_blueprint",
|
|
3004
|
+
"blueprint_path": blueprint_path
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
compile_result = send_to_unreal(compile_cmd)
|
|
3008
|
+
implementation_results["implementation_steps"].append({
|
|
3009
|
+
"step": len(suggested_nodes) + 3,
|
|
3010
|
+
"action": "compile_blueprint",
|
|
3011
|
+
"success": compile_result.get("success", False),
|
|
3012
|
+
"result": "Blueprint compilation " + ("succeeded" if compile_result.get("success") else "failed")
|
|
3013
|
+
})
|
|
3014
|
+
|
|
3015
|
+
else:
|
|
3016
|
+
implementation_results["success"] = False
|
|
3017
|
+
implementation_results["errors"].append(f"Failed to create function: {function_result.get('error', 'Unknown error')}")
|
|
3018
|
+
|
|
3019
|
+
# Final assessment
|
|
3020
|
+
if implementation_results["errors"]:
|
|
3021
|
+
implementation_results["success"] = len(implementation_results["errors"]) < len(implementation_results["implementation_steps"])
|
|
3022
|
+
implementation_results["message"] = f"Completed with {len(implementation_results['errors'])} errors"
|
|
3023
|
+
else:
|
|
3024
|
+
implementation_results["message"] = "Function implemented successfully"
|
|
3025
|
+
|
|
3026
|
+
return json.dumps(implementation_results, indent=2)
|
|
3027
|
+
|
|
3028
|
+
except Exception as e:
|
|
3029
|
+
return json.dumps({
|
|
3030
|
+
"success": False,
|
|
3031
|
+
"error": f"Implementation failed: {str(e)}"
|
|
3032
|
+
})
|
|
3033
|
+
|
|
3034
|
+
# Node Discovery System
|
|
3035
|
+
@mcp.tool()
|
|
3036
|
+
def discover_available_blueprint_nodes(search_categories: Optional[List[str]] = None) -> str:
|
|
3037
|
+
"""
|
|
3038
|
+
Discover what Blueprint nodes are actually available in the current Unreal Engine version and project.
|
|
3039
|
+
This runs automatically when 'Sync with AI' is clicked to build a real-time inventory.
|
|
3040
|
+
|
|
3041
|
+
Args:
|
|
3042
|
+
search_categories: List of categories to search ["math", "vector", "string", "gameplay", "system", "all"]
|
|
3043
|
+
If None, searches all categories
|
|
3044
|
+
|
|
3045
|
+
Returns:
|
|
3046
|
+
JSON string with comprehensive node inventory including:
|
|
3047
|
+
- Available node types and their exact names
|
|
3048
|
+
- Library mappings (KismetMathLibrary, etc.)
|
|
3049
|
+
- UE version-specific naming conventions
|
|
3050
|
+
- Custom project nodes and plugins
|
|
3051
|
+
|
|
3052
|
+
This solves the "MakeVector not found" issue by showing what's actually available.
|
|
3053
|
+
"""
|
|
3054
|
+
try:
|
|
3055
|
+
if search_categories is None:
|
|
3056
|
+
search_categories = ["math", "vector", "string", "gameplay", "system", "conversion"]
|
|
3057
|
+
|
|
3058
|
+
command = {
|
|
3059
|
+
"type": "discover_available_blueprint_nodes",
|
|
3060
|
+
"search_categories": search_categories,
|
|
3061
|
+
"include_libraries": True,
|
|
3062
|
+
"include_custom_nodes": True,
|
|
3063
|
+
"include_plugin_nodes": True
|
|
3064
|
+
}
|
|
3065
|
+
response = send_to_unreal(command)
|
|
3066
|
+
|
|
3067
|
+
if response.get("success"):
|
|
3068
|
+
# Cache the discovered nodes for this session
|
|
3069
|
+
global _discovered_nodes_cache
|
|
3070
|
+
_discovered_nodes_cache = response.get("discovered_nodes", {})
|
|
3071
|
+
|
|
3072
|
+
return json.dumps({
|
|
3073
|
+
"success": True,
|
|
3074
|
+
"message": "Node discovery completed successfully",
|
|
3075
|
+
"unreal_version": response.get("unreal_version", "Unknown"),
|
|
3076
|
+
"total_nodes_found": response.get("total_nodes_found", 0),
|
|
3077
|
+
"categories_searched": search_categories,
|
|
3078
|
+
"discovered_nodes": response.get("discovered_nodes", {}),
|
|
3079
|
+
"library_mappings": response.get("library_mappings", {}),
|
|
3080
|
+
"recommended_alternatives": response.get("recommended_alternatives", {}),
|
|
3081
|
+
"discovery_timestamp": response.get("discovery_timestamp", "Unknown")
|
|
3082
|
+
}, indent=2)
|
|
3083
|
+
else:
|
|
3084
|
+
return json.dumps({
|
|
3085
|
+
"success": False,
|
|
3086
|
+
"error": response.get("error", "Failed to discover nodes"),
|
|
3087
|
+
"message": "Node discovery unavailable - implementing fallback discovery",
|
|
3088
|
+
"fallback_attempted": True
|
|
3089
|
+
}, indent=2)
|
|
3090
|
+
except Exception as e:
|
|
3091
|
+
return json.dumps({
|
|
3092
|
+
"success": False,
|
|
3093
|
+
"error": f"Error discovering nodes: {str(e)}",
|
|
3094
|
+
"message": "Critical error in node discovery system"
|
|
3095
|
+
}, indent=2)
|
|
3096
|
+
|
|
3097
|
+
@mcp.tool()
|
|
3098
|
+
def get_node_alternatives(failed_node_name: str) -> str:
|
|
3099
|
+
"""
|
|
3100
|
+
Get alternative node names for a failed node creation attempt.
|
|
3101
|
+
Uses the discovered node cache to suggest working alternatives.
|
|
3102
|
+
|
|
3103
|
+
Args:
|
|
3104
|
+
failed_node_name: The node name that failed to be created (e.g., "MakeVector")
|
|
3105
|
+
|
|
3106
|
+
Returns:
|
|
3107
|
+
JSON string with alternative node names that might work
|
|
3108
|
+
"""
|
|
3109
|
+
try:
|
|
3110
|
+
command = {
|
|
3111
|
+
"type": "get_node_alternatives",
|
|
3112
|
+
"failed_node_name": failed_node_name,
|
|
3113
|
+
"use_discovery_cache": True
|
|
3114
|
+
}
|
|
3115
|
+
response = send_to_unreal(command)
|
|
3116
|
+
|
|
3117
|
+
if response.get("success"):
|
|
3118
|
+
return json.dumps(response, indent=2)
|
|
3119
|
+
else:
|
|
3120
|
+
# Fallback: use cached discovery data if available
|
|
3121
|
+
global _discovered_nodes_cache
|
|
3122
|
+
if _discovered_nodes_cache:
|
|
3123
|
+
alternatives = []
|
|
3124
|
+
search_term = failed_node_name.lower()
|
|
3125
|
+
|
|
3126
|
+
for category, nodes in _discovered_nodes_cache.items():
|
|
3127
|
+
for node_name in nodes:
|
|
3128
|
+
if search_term in node_name.lower() or "vector" in node_name.lower():
|
|
3129
|
+
alternatives.append({
|
|
3130
|
+
"node_name": node_name,
|
|
3131
|
+
"category": category,
|
|
3132
|
+
"confidence": "medium"
|
|
3133
|
+
})
|
|
3134
|
+
|
|
3135
|
+
return json.dumps({
|
|
3136
|
+
"success": True,
|
|
3137
|
+
"failed_node": failed_node_name,
|
|
3138
|
+
"alternatives_found": len(alternatives),
|
|
3139
|
+
"alternatives": alternatives,
|
|
3140
|
+
"source": "cached_discovery"
|
|
3141
|
+
}, indent=2)
|
|
3142
|
+
else:
|
|
3143
|
+
return json.dumps({
|
|
3144
|
+
"success": False,
|
|
3145
|
+
"error": "No alternatives found and no discovery cache available",
|
|
3146
|
+
"suggestion": "Run discover_available_blueprint_nodes first"
|
|
3147
|
+
}, indent=2)
|
|
3148
|
+
except Exception as e:
|
|
3149
|
+
return json.dumps({
|
|
3150
|
+
"success": False,
|
|
3151
|
+
"error": f"Error getting alternatives: {str(e)}"
|
|
3152
|
+
}, indent=2)
|
|
3153
|
+
|
|
3154
|
+
@mcp.tool()
|
|
3155
|
+
def smart_add_node_with_discovery(blueprint_path: str, function_id: str, requested_node: str,
|
|
3156
|
+
node_position: list = [0, 0], auto_find_alternatives: bool = True) -> str:
|
|
3157
|
+
"""
|
|
3158
|
+
Smart node addition with enhanced discovery - now largely redundant due to comprehensive built-in discovery.
|
|
3159
|
+
The main add_node_to_blueprint function now handles comprehensive discovery automatically.
|
|
3160
|
+
|
|
3161
|
+
Args:
|
|
3162
|
+
blueprint_path: Path to the Blueprint asset
|
|
3163
|
+
function_id: ID of the function to add the node to
|
|
3164
|
+
requested_node: The node type you want (any format accepted - "Add", "SetActorRotation", "KismetMathLibrary.MakeVector", etc.)
|
|
3165
|
+
node_position: Position of the node in the graph [X, Y]
|
|
3166
|
+
auto_find_alternatives: If True, tries additional alternatives (usually not needed with enhanced discovery)
|
|
3167
|
+
|
|
3168
|
+
Returns:
|
|
3169
|
+
JSON string with creation result and any alternatives tried
|
|
3170
|
+
|
|
3171
|
+
Note:
|
|
3172
|
+
With the enhanced discovery system, this function is mostly a wrapper around add_node_to_blueprint.
|
|
3173
|
+
The main function now handles comprehensive discovery, variations, and suggestions automatically.
|
|
3174
|
+
This function is maintained for backward compatibility and additional fallback options.
|
|
3175
|
+
"""
|
|
3176
|
+
try:
|
|
3177
|
+
result = {
|
|
3178
|
+
"success": False,
|
|
3179
|
+
"requested_node": requested_node,
|
|
3180
|
+
"attempts": [],
|
|
3181
|
+
"final_node_id": None,
|
|
3182
|
+
"discovery_method": "enhanced_comprehensive",
|
|
3183
|
+
"alternatives_tried": []
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
# The enhanced add_node_to_blueprint now handles comprehensive discovery automatically
|
|
3187
|
+
first_attempt = add_node_to_blueprint(blueprint_path, function_id, requested_node, node_position)
|
|
3188
|
+
result["attempts"].append({
|
|
3189
|
+
"node_name": requested_node,
|
|
3190
|
+
"result": first_attempt,
|
|
3191
|
+
"success": "Successfully added" in first_attempt,
|
|
3192
|
+
"method": "comprehensive_discovery"
|
|
3193
|
+
})
|
|
3194
|
+
|
|
3195
|
+
if "Successfully added" in first_attempt:
|
|
3196
|
+
result["success"] = True
|
|
3197
|
+
if "with ID:" in first_attempt:
|
|
3198
|
+
result["final_node_id"] = first_attempt.split("with ID:")[-1].strip()
|
|
3199
|
+
result["message"] = "Node created successfully using enhanced discovery system"
|
|
3200
|
+
return json.dumps(result, indent=2)
|
|
3201
|
+
|
|
3202
|
+
# If it still failed, check if suggestions were provided
|
|
3203
|
+
if "SUGGESTIONS:" in first_attempt:
|
|
3204
|
+
result["suggestions_provided"] = True
|
|
3205
|
+
result["message"] = "Enhanced discovery provided suggestions for alternatives"
|
|
3206
|
+
else:
|
|
3207
|
+
result["suggestions_provided"] = False
|
|
3208
|
+
result["message"] = "Node creation failed even with comprehensive discovery"
|
|
3209
|
+
|
|
3210
|
+
# Only try additional alternatives if specifically requested and no suggestions were provided
|
|
3211
|
+
if auto_find_alternatives and not result["suggestions_provided"]:
|
|
3212
|
+
alternatives_response = get_node_alternatives(requested_node)
|
|
3213
|
+
try:
|
|
3214
|
+
alternatives_data = json.loads(alternatives_response)
|
|
3215
|
+
if alternatives_data.get("success") and alternatives_data.get("alternatives"):
|
|
3216
|
+
|
|
3217
|
+
for alt in alternatives_data["alternatives"][:2]: # Try top 2 alternatives as fallback
|
|
3218
|
+
alt_name = alt["node_name"]
|
|
3219
|
+
alt_attempt = add_node_to_blueprint(blueprint_path, function_id, alt_name, node_position)
|
|
3220
|
+
|
|
3221
|
+
result["attempts"].append({
|
|
3222
|
+
"node_name": alt_name,
|
|
3223
|
+
"result": alt_attempt,
|
|
3224
|
+
"success": "Successfully added" in alt_attempt,
|
|
3225
|
+
"alternative_for": requested_node,
|
|
3226
|
+
"method": "fallback_alternatives"
|
|
3227
|
+
})
|
|
3228
|
+
result["alternatives_tried"].append(alt_name)
|
|
3229
|
+
|
|
3230
|
+
if "Successfully added" in alt_attempt:
|
|
3231
|
+
result["success"] = True
|
|
3232
|
+
if "with ID:" in alt_attempt:
|
|
3233
|
+
result["final_node_id"] = alt_attempt.split("with ID:")[-1].strip()
|
|
3234
|
+
result["successful_alternative"] = alt_name
|
|
3235
|
+
result["message"] = f"Node created using fallback alternative: {alt_name}"
|
|
3236
|
+
break
|
|
3237
|
+
except:
|
|
3238
|
+
pass # Continue with original failure
|
|
3239
|
+
|
|
3240
|
+
return json.dumps(result, indent=2)
|
|
3241
|
+
|
|
3242
|
+
except Exception as e:
|
|
3243
|
+
return json.dumps({
|
|
3244
|
+
"success": False,
|
|
3245
|
+
"error": f"Error in smart node addition: {str(e)}",
|
|
3246
|
+
"requested_node": requested_node
|
|
3247
|
+
}, indent=2)
|
|
3248
|
+
|
|
3249
|
+
# Global cache for discovered nodes
|
|
3250
|
+
_discovered_nodes_cache = {}
|
|
3251
|
+
|
|
3252
|
+
@mcp.tool()
|
|
3253
|
+
def set_node_pin_default_value(blueprint_path: str, function_guid: str, node_guid: str, pin_name: str, value: str) -> str:
|
|
3254
|
+
"""
|
|
3255
|
+
Set the default value of a node pin - SOLVES the automation limitation!
|
|
3256
|
+
This is the key function that allows full automation without manual steps.
|
|
3257
|
+
|
|
3258
|
+
Args:
|
|
3259
|
+
blueprint_path: Path to the Blueprint asset
|
|
3260
|
+
function_guid: GUID of the function containing the node
|
|
3261
|
+
node_guid: GUID of the node (returned from add_node_to_blueprint)
|
|
3262
|
+
pin_name: Name of the pin to set (e.g., "X", "Y", "Z" for MakeVector)
|
|
3263
|
+
value: Value to set (e.g., "0.0", "1.0")
|
|
3264
|
+
|
|
3265
|
+
Returns:
|
|
3266
|
+
Success/failure message
|
|
3267
|
+
|
|
3268
|
+
Example usage for MakeVector:
|
|
3269
|
+
set_node_pin_default_value(blueprint_path, func_id, makevector_node_id, "X", "0.0")
|
|
3270
|
+
set_node_pin_default_value(blueprint_path, func_id, makevector_node_id, "Y", "0.0")
|
|
3271
|
+
set_node_pin_default_value(blueprint_path, func_id, makevector_node_id, "Z", "1.0")
|
|
3272
|
+
"""
|
|
3273
|
+
try:
|
|
3274
|
+
command = {
|
|
3275
|
+
"type": "set_node_pin_default_value",
|
|
3276
|
+
"blueprint_path": blueprint_path,
|
|
3277
|
+
"function_guid": function_guid,
|
|
3278
|
+
"node_guid": node_guid,
|
|
3279
|
+
"pin_name": pin_name,
|
|
3280
|
+
"value": value
|
|
3281
|
+
}
|
|
3282
|
+
response = send_to_unreal(command)
|
|
3283
|
+
|
|
3284
|
+
if response.get("success"):
|
|
3285
|
+
return f"Successfully set {pin_name} = {value} on node {node_guid}"
|
|
3286
|
+
else:
|
|
3287
|
+
return f"Failed to set pin value: {response.get('error', 'Unknown error')}"
|
|
3288
|
+
|
|
3289
|
+
except Exception as e:
|
|
3290
|
+
return f"Error setting pin value: {str(e)}"
|
|
3291
|
+
|
|
3292
|
+
@mcp.tool()
|
|
3293
|
+
def get_node_pin_info(blueprint_path: str, function_guid: str, node_guid: str) -> str:
|
|
3294
|
+
"""
|
|
3295
|
+
Get information about all pins on a node.
|
|
3296
|
+
Useful for discovering what pins are available and their current values.
|
|
3297
|
+
|
|
3298
|
+
Args:
|
|
3299
|
+
blueprint_path: Path to the Blueprint asset
|
|
3300
|
+
function_guid: GUID of the function containing the node
|
|
3301
|
+
node_guid: GUID of the node
|
|
3302
|
+
|
|
3303
|
+
Returns:
|
|
3304
|
+
JSON string with pin information including names, types, directions, and current values
|
|
3305
|
+
"""
|
|
3306
|
+
try:
|
|
3307
|
+
command = {
|
|
3308
|
+
"type": "get_node_pin_info",
|
|
3309
|
+
"blueprint_path": blueprint_path,
|
|
3310
|
+
"function_guid": function_guid,
|
|
3311
|
+
"node_guid": node_guid
|
|
3312
|
+
}
|
|
3313
|
+
response = send_to_unreal(command)
|
|
3314
|
+
|
|
3315
|
+
if response.get("success"):
|
|
3316
|
+
return json.dumps(response, indent=2)
|
|
3317
|
+
else:
|
|
3318
|
+
return json.dumps({
|
|
3319
|
+
"success": False,
|
|
3320
|
+
"error": response.get("error", "Failed to get pin info"),
|
|
3321
|
+
"node_guid": node_guid
|
|
3322
|
+
}, indent=2)
|
|
3323
|
+
|
|
3324
|
+
except Exception as e:
|
|
3325
|
+
return json.dumps({
|
|
3326
|
+
"success": False,
|
|
3327
|
+
"error": f"Error getting pin info: {str(e)}",
|
|
3328
|
+
"node_guid": node_guid
|
|
3329
|
+
}, indent=2)
|
|
3330
|
+
|
|
3331
|
+
# Project Control
|
|
3332
|
+
@mcp.tool()
|
|
3333
|
+
def create_project_folder(folder_path: str) -> str:
|
|
3334
|
+
"""
|
|
3335
|
+
Create a new folder in the Unreal project content directory.
|
|
3336
|
+
Args:
|
|
3337
|
+
folder_path: Path relative to /Game (e.g., "FlappyBird/Assets")
|
|
3338
|
+
"""
|
|
3339
|
+
command = {"type": "create_project_folder", "folder_path": folder_path}
|
|
3340
|
+
response = send_to_unreal(command)
|
|
3341
|
+
return response.get("message", f"Failed: {response.get('error', 'Unknown error')}")
|
|
3342
|
+
|
|
3343
|
+
@mcp.tool()
|
|
3344
|
+
def get_files_in_folder(folder_path: str) -> str:
|
|
3345
|
+
"""
|
|
3346
|
+
List all files in a specified project folder.
|
|
3347
|
+
Args:
|
|
3348
|
+
folder_path: Path relative to /Game (e.g., "FlappyBird/Assets")
|
|
3349
|
+
"""
|
|
3350
|
+
command = {"type": "get_files_in_folder", "folder_path": folder_path}
|
|
3351
|
+
response = send_to_unreal(command)
|
|
3352
|
+
return json.dumps(response.get("files", [])) if response.get("success") else f"Failed: {response.get('error')}"
|
|
3353
|
+
|
|
3354
|
+
@mcp.tool()
|
|
3355
|
+
def create_game_mode(game_mode_path: str, pawn_blueprint_path: str, base_class: str = "GameModeBase") -> str:
|
|
3356
|
+
"""Create a game mode Blueprint, set its default pawn, and assign it as the current scene's default game mode.
|
|
3357
|
+
Args:
|
|
3358
|
+
game_mode_path: Path for new game mode (e.g., "/Game/MyGameMode")
|
|
3359
|
+
pawn_blueprint_path: Path to pawn Blueprint (e.g., "/Game/Blueprints/BP_Player")
|
|
3360
|
+
base_class: Base class for game mode (default: "GameModeBase")
|
|
3361
|
+
"""
|
|
3362
|
+
try:
|
|
3363
|
+
command = {
|
|
3364
|
+
"type": "create_game_mode",
|
|
3365
|
+
"game_mode_path": game_mode_path,
|
|
3366
|
+
"pawn_blueprint_path": pawn_blueprint_path,
|
|
3367
|
+
"base_class": base_class
|
|
3368
|
+
}
|
|
3369
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3370
|
+
if response.get("success"):
|
|
3371
|
+
return response.get(
|
|
3372
|
+
"message",
|
|
3373
|
+
f"Created game mode {game_mode_path} with pawn {pawn_blueprint_path}"
|
|
3374
|
+
)
|
|
3375
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
3376
|
+
except Exception as e:
|
|
3377
|
+
return f"Error creating game mode: {str(e)}"
|
|
3378
|
+
|
|
3379
|
+
@mcp.tool()
|
|
3380
|
+
def add_widget_to_user_widget(user_widget_path: str, widget_type: str, widget_name: str, parent_widget_name: str = "") -> str:
|
|
3381
|
+
"""
|
|
3382
|
+
Adds a new widget (like TextBlock, Button, Image, CanvasPanel, VerticalBox) to a User Widget Blueprint.
|
|
3383
|
+
Args:
|
|
3384
|
+
user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
|
|
3385
|
+
widget_type: Class name of the widget to add (e.g., "TextBlock", "Button", "Image", "CanvasPanel", "VerticalBox", "HorizontalBox", "SizeBox", "Border"). Case-sensitive.
|
|
3386
|
+
widget_name: A unique desired name for the new widget variable (e.g., "TitleText", "StartButton", "PlayerHealthBar"). The actual name might get adjusted for uniqueness.
|
|
3387
|
+
parent_widget_name: Optional. The name of an existing Panel widget (like CanvasPanel, VerticalBox) inside the User Widget to attach this new widget to. If empty, attempts to attach to the root or the first available CanvasPanel.
|
|
3388
|
+
Returns:
|
|
3389
|
+
JSON string indicating success (with actual widget name) or failure with an error message.
|
|
3390
|
+
"""
|
|
3391
|
+
command = {
|
|
3392
|
+
"type": "add_widget_to_user_widget",
|
|
3393
|
+
"user_widget_path": user_widget_path,
|
|
3394
|
+
"widget_type": widget_type,
|
|
3395
|
+
"widget_name": widget_name,
|
|
3396
|
+
"parent_widget_name": parent_widget_name
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
# Use json.loads to parse the JSON string returned by send_to_unreal
|
|
3400
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3401
|
+
if response.get("success"):
|
|
3402
|
+
actual_name = response.get("widget_name", widget_name)
|
|
3403
|
+
return response.get(
|
|
3404
|
+
"message",
|
|
3405
|
+
f"Successfully added widget '{actual_name}' of type '{widget_type}' to '{user_widget_path}'."
|
|
3406
|
+
)
|
|
3407
|
+
error_message = response.get("error", "Unknown C++ error")
|
|
3408
|
+
raw_suffix = f" Raw response: {response.get('raw')}" if "raw" in response else ""
|
|
3409
|
+
return f"Failed to add widget: {error_message}{raw_suffix}"
|
|
3410
|
+
|
|
3411
|
+
@mcp.tool()
|
|
3412
|
+
def edit_widget_property(user_widget_path: str, widget_name: str, property_name: str, value: str) -> str:
|
|
3413
|
+
"""
|
|
3414
|
+
Edits a property of a specific widget within a User Widget Blueprint.
|
|
3415
|
+
Args:
|
|
3416
|
+
user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
|
|
3417
|
+
widget_name: The name of the widget inside the User Widget whose property you want to change (e.g., "TitleText", "StartButton").
|
|
3418
|
+
property_name: The name of the property to edit. For layout properties controlled by the parent panel (like position, size, anchors in a CanvasPanel), prefix with "Slot." (e.g., "Text", "ColorAndOpacity", "Brush.ImageSize", "Slot.Position", "Slot.Size", "Slot.Anchors", "Slot.Alignment"). For UImage textures you can also use "Texture", "BrushFromTexture", or "Brush.ResourceObject". For UButton styles use "ButtonTexture" (all states) or "NormalTexture"/"HoveredTexture"/"PressedTexture". Case-sensitive.
|
|
3419
|
+
value: The new value for the property, formatted as a string EXACTLY as Unreal expects for ImportText. Examples:
|
|
3420
|
+
- Text: '"Hello World!"' (Note: String literal requires inner quotes)
|
|
3421
|
+
- Float: '150.0'
|
|
3422
|
+
- Integer: '10'
|
|
3423
|
+
- Boolean: 'true' or 'false'
|
|
3424
|
+
- LinearColor: '(R=1.0,G=0.0,B=0.0,A=1.0)'
|
|
3425
|
+
- Vector2D (for Size, Position): '(X=200.0,Y=50.0)'
|
|
3426
|
+
- Anchors: '(Minimum=(X=0.5,Y=0.0),Maximum=(X=0.5,Y=0.0))' (Top Center Anchor)
|
|
3427
|
+
- Alignment (Vector2D): '(X=0.5,Y=0.5)' (Center Alignment)
|
|
3428
|
+
- Font (FSlateFontInfo): "(FontObject=Font'/Engine/EngineFonts/Roboto.Roboto',Size=24)"
|
|
3429
|
+
- Texture (Object Path): "Texture2D'/Game/Textures/MyIcon.MyIcon'" OR "/Game/Textures/MyIcon"
|
|
3430
|
+
- Enum (e.g., Stretch): 'ScaleToFit'
|
|
3431
|
+
Returns:
|
|
3432
|
+
JSON string indicating success or failure with an error message.
|
|
3433
|
+
"""
|
|
3434
|
+
command = {
|
|
3435
|
+
"type": "edit_widget_property",
|
|
3436
|
+
"user_widget_path": user_widget_path,
|
|
3437
|
+
"widget_name": widget_name,
|
|
3438
|
+
"property_name": property_name,
|
|
3439
|
+
"value": value # Pass the string value directly
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
# Use json.loads to parse the JSON string returned by send_to_unreal
|
|
3443
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3444
|
+
if response.get("success"):
|
|
3445
|
+
return response.get(
|
|
3446
|
+
"message",
|
|
3447
|
+
f"Successfully set property '{property_name}' on widget '{widget_name}'."
|
|
3448
|
+
)
|
|
3449
|
+
error_message = response.get("error", "Unknown C++ error")
|
|
3450
|
+
raw_suffix = f" Raw response: {response.get('raw')}" if "raw" in response else ""
|
|
3451
|
+
return f"Failed to edit widget property: {error_message}{raw_suffix}"
|
|
3452
|
+
|
|
3453
|
+
# --- NEW UI TOOLS ---
|
|
3454
|
+
|
|
3455
|
+
@mcp.tool()
|
|
3456
|
+
def create_user_widget(widget_name: str, save_path: str = "/Game/UI", root_widget_type: str = "CanvasPanel") -> str:
|
|
3457
|
+
"""
|
|
3458
|
+
Creates a new empty Widget Blueprint with a configurable root panel.
|
|
3459
|
+
Args:
|
|
3460
|
+
widget_name: Name for the new Widget Blueprint asset (e.g., "WBP_MainMenu", "WBP_HUD").
|
|
3461
|
+
save_path: Content folder path to save the Widget Blueprint to (e.g., "/Game/UI", "/Game/Widgets"). Defaults to "/Game/UI".
|
|
3462
|
+
root_widget_type: Class name of the root panel widget. Must be a panel type. Common values: "CanvasPanel" (default, free-form positioning), "VerticalBox" (vertical layout), "HorizontalBox" (horizontal layout), "Overlay" (stacked layers), "ScrollBox" (scrollable container).
|
|
3463
|
+
Returns:
|
|
3464
|
+
JSON with success, widget_path, and root_widget_name.
|
|
3465
|
+
"""
|
|
3466
|
+
command = {
|
|
3467
|
+
"type": "create_user_widget",
|
|
3468
|
+
"widget_name": widget_name,
|
|
3469
|
+
"save_path": save_path,
|
|
3470
|
+
"root_widget_type": root_widget_type
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3474
|
+
if response.get("success"):
|
|
3475
|
+
widget_path = response.get("widget_path", "")
|
|
3476
|
+
root_name = response.get("root_widget_name", "")
|
|
3477
|
+
return response.get("message", f"Created Widget Blueprint '{widget_name}' at {widget_path} with root '{root_name}'.")
|
|
3478
|
+
error_message = response.get("error", "Unknown error")
|
|
3479
|
+
raw_suffix = f" Raw response: {response.get('raw')}" if "raw" in response else ""
|
|
3480
|
+
return f"Failed to create widget: {error_message}{raw_suffix}"
|
|
3481
|
+
|
|
3482
|
+
|
|
3483
|
+
@mcp.tool()
|
|
3484
|
+
def get_widget_hierarchy(user_widget_path: str) -> str:
|
|
3485
|
+
"""
|
|
3486
|
+
Inspects the full widget tree structure of a User Widget Blueprint. Returns a nested JSON hierarchy showing every widget, its type, slot type, and children.
|
|
3487
|
+
Use this to understand the current state of a UI before making changes.
|
|
3488
|
+
Args:
|
|
3489
|
+
user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
|
|
3490
|
+
Returns:
|
|
3491
|
+
JSON with nested hierarchy tree: each node has "name", "class", "slot_type", "is_variable", and "children" array.
|
|
3492
|
+
"""
|
|
3493
|
+
command = {
|
|
3494
|
+
"type": "get_widget_hierarchy",
|
|
3495
|
+
"user_widget_path": user_widget_path
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3499
|
+
if response.get("success"):
|
|
3500
|
+
# Return the full JSON response so the AI can parse the hierarchy
|
|
3501
|
+
return json.dumps(response)
|
|
3502
|
+
error_message = response.get("error", "Unknown error")
|
|
3503
|
+
return f"Failed to get widget hierarchy: {error_message}"
|
|
3504
|
+
|
|
3505
|
+
|
|
3506
|
+
@mcp.tool()
|
|
3507
|
+
def get_widget_properties(user_widget_path: str, widget_name: str, property_names: str = "") -> str:
|
|
3508
|
+
"""
|
|
3509
|
+
Reads current property values of a specific widget within a User Widget Blueprint.
|
|
3510
|
+
Args:
|
|
3511
|
+
user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
|
|
3512
|
+
widget_name: Name of the widget to inspect (e.g., "TitleText", "StartButton").
|
|
3513
|
+
property_names: Optional comma-separated list of specific properties to read (e.g., "Text,ColorAndOpacity,Slot.Size"). If empty, returns all properties.
|
|
3514
|
+
Returns:
|
|
3515
|
+
JSON with widget properties and slot properties.
|
|
3516
|
+
"""
|
|
3517
|
+
command = {
|
|
3518
|
+
"type": "get_widget_properties",
|
|
3519
|
+
"user_widget_path": user_widget_path,
|
|
3520
|
+
"widget_name": widget_name,
|
|
3521
|
+
"property_names": property_names
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3525
|
+
if response.get("success"):
|
|
3526
|
+
return json.dumps(response)
|
|
3527
|
+
error_message = response.get("error", "Unknown error")
|
|
3528
|
+
return f"Failed to get widget properties: {error_message}"
|
|
3529
|
+
|
|
3530
|
+
|
|
3531
|
+
@mcp.tool()
|
|
3532
|
+
def remove_widget_from_user_widget(user_widget_path: str, widget_name: str, remove_children: bool = True) -> str:
|
|
3533
|
+
"""
|
|
3534
|
+
Removes a widget from a User Widget Blueprint's hierarchy.
|
|
3535
|
+
Args:
|
|
3536
|
+
user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
|
|
3537
|
+
widget_name: Name of the widget to remove (e.g., "OldButton", "UnusedImage").
|
|
3538
|
+
remove_children: If True (default), removes the widget and all its children. If False, reparents children to the removed widget's parent panel.
|
|
3539
|
+
Returns:
|
|
3540
|
+
JSON indicating success or failure.
|
|
3541
|
+
"""
|
|
3542
|
+
command = {
|
|
3543
|
+
"type": "remove_widget_from_user_widget",
|
|
3544
|
+
"user_widget_path": user_widget_path,
|
|
3545
|
+
"widget_name": widget_name,
|
|
3546
|
+
"remove_children": remove_children
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3550
|
+
if response.get("success"):
|
|
3551
|
+
return response.get("message", f"Successfully removed widget '{widget_name}'.")
|
|
3552
|
+
error_message = response.get("error", "Unknown error")
|
|
3553
|
+
return f"Failed to remove widget: {error_message}"
|
|
3554
|
+
|
|
3555
|
+
|
|
3556
|
+
@mcp.tool()
|
|
3557
|
+
def add_widgets_bulk(user_widget_path: str, widgets: str) -> str:
|
|
3558
|
+
"""
|
|
3559
|
+
Adds multiple widgets to a User Widget Blueprint in a single call. Much more efficient than calling add_widget_to_user_widget multiple times.
|
|
3560
|
+
Args:
|
|
3561
|
+
user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
|
|
3562
|
+
widgets: JSON array string where each element is an object with:
|
|
3563
|
+
- "widget_type": Class name (e.g., "TextBlock", "Button", "Image")
|
|
3564
|
+
- "widget_name": Desired unique name (e.g., "TitleText", "StartButton")
|
|
3565
|
+
- "parent_widget_name": Optional parent panel name (e.g., "RootPanel"). If omitted, attaches to root.
|
|
3566
|
+
Example: '[{"widget_type":"TextBlock","widget_name":"Title","parent_widget_name":"RootPanel"},{"widget_type":"Button","widget_name":"PlayBtn","parent_widget_name":"RootPanel"}]'
|
|
3567
|
+
Returns:
|
|
3568
|
+
JSON with results array (per-widget success/failure), failed_count, and total_count.
|
|
3569
|
+
"""
|
|
3570
|
+
# Parse the widgets string to ensure it's valid JSON, then pass as list
|
|
3571
|
+
try:
|
|
3572
|
+
widgets_list = json.loads(widgets) if isinstance(widgets, str) else widgets
|
|
3573
|
+
except json.JSONDecodeError:
|
|
3574
|
+
return f"Failed to parse widgets JSON: invalid JSON format"
|
|
3575
|
+
|
|
3576
|
+
command = {
|
|
3577
|
+
"type": "add_widgets_bulk",
|
|
3578
|
+
"user_widget_path": user_widget_path,
|
|
3579
|
+
"widgets": widgets_list
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3583
|
+
if response.get("success"):
|
|
3584
|
+
total = response.get("total_count", 0)
|
|
3585
|
+
failed = response.get("failed_count", 0)
|
|
3586
|
+
return f"Successfully added {total - failed}/{total} widgets to '{user_widget_path}'."
|
|
3587
|
+
# Even partial success returns useful info
|
|
3588
|
+
if "results" in response:
|
|
3589
|
+
total = response.get("total_count", 0)
|
|
3590
|
+
failed = response.get("failed_count", 0)
|
|
3591
|
+
return f"Bulk add partially completed: {total - failed}/{total} succeeded. Details: {json.dumps(response.get('results', []))}"
|
|
3592
|
+
error_message = response.get("error", "Unknown error")
|
|
3593
|
+
return f"Failed bulk add: {error_message}"
|
|
3594
|
+
|
|
3595
|
+
|
|
3596
|
+
@mcp.tool()
|
|
3597
|
+
def edit_widget_properties_bulk(user_widget_path: str, edits: str) -> str:
|
|
3598
|
+
"""
|
|
3599
|
+
Sets multiple properties across multiple widgets in a single call. Much more efficient than calling edit_widget_property multiple times.
|
|
3600
|
+
Args:
|
|
3601
|
+
user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
|
|
3602
|
+
edits: JSON array string where each element is an object with:
|
|
3603
|
+
- "widget_name": Name of the target widget
|
|
3604
|
+
- "property_name": Property to edit (prefix "Slot." for layout properties)
|
|
3605
|
+
- "value": New value string in Unreal ImportText format
|
|
3606
|
+
Example: '[{"widget_name":"Title","property_name":"Text","value":"\"Welcome!\""},{"widget_name":"Title","property_name":"Slot.Size","value":"(X=400.0,Y=60.0)"}]'
|
|
3607
|
+
Returns:
|
|
3608
|
+
JSON with results array (per-edit success/failure), failed_count, and total_count.
|
|
3609
|
+
"""
|
|
3610
|
+
try:
|
|
3611
|
+
edits_list = json.loads(edits) if isinstance(edits, str) else edits
|
|
3612
|
+
except json.JSONDecodeError:
|
|
3613
|
+
return f"Failed to parse edits JSON: invalid JSON format"
|
|
3614
|
+
|
|
3615
|
+
command = {
|
|
3616
|
+
"type": "edit_widget_properties_bulk",
|
|
3617
|
+
"user_widget_path": user_widget_path,
|
|
3618
|
+
"edits": edits_list
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3622
|
+
if response.get("success"):
|
|
3623
|
+
total = response.get("total_count", 0)
|
|
3624
|
+
failed = response.get("failed_count", 0)
|
|
3625
|
+
return f"Successfully applied {total - failed}/{total} property edits in '{user_widget_path}'."
|
|
3626
|
+
if "results" in response:
|
|
3627
|
+
total = response.get("total_count", 0)
|
|
3628
|
+
failed = response.get("failed_count", 0)
|
|
3629
|
+
return f"Bulk edit partially completed: {total - failed}/{total} succeeded. Details: {json.dumps(response.get('results', []))}"
|
|
3630
|
+
error_message = response.get("error", "Unknown error")
|
|
3631
|
+
return f"Failed bulk edit: {error_message}"
|
|
3632
|
+
|
|
3633
|
+
|
|
3634
|
+
@mcp.tool()
|
|
3635
|
+
def list_available_widget_types(include_engine_widgets: bool = True, include_project_widgets: bool = True) -> str:
|
|
3636
|
+
"""
|
|
3637
|
+
Lists all available widget types that can be used with add_widget_to_user_widget and add_widgets_bulk.
|
|
3638
|
+
Use this to discover which widget classes are available before building a UI.
|
|
3639
|
+
Args:
|
|
3640
|
+
include_engine_widgets: Include built-in UMG/Engine widget types (TextBlock, Button, Image, etc.). Default: True.
|
|
3641
|
+
include_project_widgets: Include project-specific (user-created) widget types. Default: True.
|
|
3642
|
+
Returns:
|
|
3643
|
+
JSON with categorized list of widget types. Each entry has: class_name, category (Layout/Input/Display/Utility/Other), is_panel (can contain children), is_engine.
|
|
3644
|
+
"""
|
|
3645
|
+
command = {
|
|
3646
|
+
"type": "list_available_widget_types",
|
|
3647
|
+
"include_engine_widgets": include_engine_widgets,
|
|
3648
|
+
"include_project_widgets": include_project_widgets
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3652
|
+
if response.get("success"):
|
|
3653
|
+
return json.dumps(response)
|
|
3654
|
+
error_message = response.get("error", "Unknown error")
|
|
3655
|
+
return f"Failed to list widget types: {error_message}"
|
|
3656
|
+
|
|
3657
|
+
|
|
3658
|
+
@mcp.tool()
|
|
3659
|
+
def bind_widget_event(user_widget_path: str, widget_name: str, event_name: str) -> str:
|
|
3660
|
+
"""
|
|
3661
|
+
Binds a widget event (like OnClicked, OnHovered) to a Blueprint event node in the Widget Blueprint's EventGraph.
|
|
3662
|
+
Automatically sets the widget as a variable (bIsVariable=true) if not already set.
|
|
3663
|
+
Args:
|
|
3664
|
+
user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
|
|
3665
|
+
widget_name: Name of the widget to bind the event on (e.g., "StartButton", "SettingsCheckbox").
|
|
3666
|
+
event_name: Name of the multicast delegate event on the widget. Common events:
|
|
3667
|
+
- Button: "OnClicked", "OnPressed", "OnReleased", "OnHovered", "OnUnhovered"
|
|
3668
|
+
- CheckBox: "OnCheckStateChanged"
|
|
3669
|
+
- Slider: "OnValueChanged", "OnMouseCaptureBegin", "OnMouseCaptureEnd"
|
|
3670
|
+
- ComboBoxString: "OnSelectionChanged", "OnOpening"
|
|
3671
|
+
- EditableText/EditableTextBox: "OnTextChanged", "OnTextCommitted"
|
|
3672
|
+
Returns:
|
|
3673
|
+
JSON with success, event_node_guid, event_name, and widget_name.
|
|
3674
|
+
"""
|
|
3675
|
+
command = {
|
|
3676
|
+
"type": "bind_widget_event",
|
|
3677
|
+
"user_widget_path": user_widget_path,
|
|
3678
|
+
"widget_name": widget_name,
|
|
3679
|
+
"event_name": event_name
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3683
|
+
if response.get("success"):
|
|
3684
|
+
event_guid = response.get("event_node_guid", "")
|
|
3685
|
+
return response.get("message", f"Successfully bound event '{event_name}' on '{widget_name}' (GUID: {event_guid}).")
|
|
3686
|
+
error_message = response.get("error", "Unknown error")
|
|
3687
|
+
raw_suffix = f" Raw response: {response.get('raw')}" if "raw" in response else ""
|
|
3688
|
+
return f"Failed to bind widget event: {error_message}{raw_suffix}"
|
|
3689
|
+
|
|
3690
|
+
|
|
3691
|
+
@mcp.tool()
|
|
3692
|
+
def validate_ui_spec(ui_spec: str) -> str:
|
|
3693
|
+
"""
|
|
3694
|
+
Validate a strict JSON UI spec before generation.
|
|
3695
|
+
Args:
|
|
3696
|
+
ui_spec: JSON object string with schema:
|
|
3697
|
+
{
|
|
3698
|
+
"schema_version": 1,
|
|
3699
|
+
"blueprint": {
|
|
3700
|
+
"widget_name": "WBP_Menu",
|
|
3701
|
+
"save_path": "/Game/UI",
|
|
3702
|
+
"root_widget_type": "CanvasPanel",
|
|
3703
|
+
"allow_overwrite": false
|
|
3704
|
+
},
|
|
3705
|
+
"widgets": [
|
|
3706
|
+
{"widget_name":"TitleText","widget_type":"TextBlock","parent_widget_name":"$root"}
|
|
3707
|
+
],
|
|
3708
|
+
"properties": [
|
|
3709
|
+
{"widget_name":"TitleText","property_name":"Text","value":"\"Hello\""}
|
|
3710
|
+
]
|
|
3711
|
+
}
|
|
3712
|
+
Returns:
|
|
3713
|
+
Full JSON validation response with `success`, `errors`, `warnings`, and summary metadata.
|
|
3714
|
+
"""
|
|
3715
|
+
try:
|
|
3716
|
+
parsed_spec = json.loads(ui_spec) if isinstance(ui_spec, str) else ui_spec
|
|
3717
|
+
except json.JSONDecodeError as parse_error:
|
|
3718
|
+
return f"Failed to parse ui_spec JSON: {parse_error}"
|
|
3719
|
+
|
|
3720
|
+
if not isinstance(parsed_spec, dict):
|
|
3721
|
+
return "ui_spec must be a JSON object"
|
|
3722
|
+
|
|
3723
|
+
command = {
|
|
3724
|
+
"type": "validate_ui_spec",
|
|
3725
|
+
"ui_spec": parsed_spec,
|
|
3726
|
+
}
|
|
3727
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3728
|
+
if response.get("success") or "errors" in response or "warnings" in response:
|
|
3729
|
+
return json.dumps(response)
|
|
3730
|
+
error_message = response.get("error", "Unknown error")
|
|
3731
|
+
raw_suffix = f" Raw response: {response.get('raw')}" if "raw" in response else ""
|
|
3732
|
+
return f"Failed to validate ui_spec: {error_message}{raw_suffix}"
|
|
3733
|
+
|
|
3734
|
+
|
|
3735
|
+
@mcp.tool()
|
|
3736
|
+
def generate_widget_blueprint_from_spec(ui_spec: str) -> str:
|
|
3737
|
+
"""
|
|
3738
|
+
Generate or regenerate a Widget Blueprint from a strict JSON UI spec.
|
|
3739
|
+
This is the preferred code-first UI pipeline and does not require Pillow/image slicing.
|
|
3740
|
+
Args:
|
|
3741
|
+
ui_spec: JSON object string in the strict schema validated by `validate_ui_spec`.
|
|
3742
|
+
Returns:
|
|
3743
|
+
Full JSON response with `widget_path`, operation-level results, warnings, and failures.
|
|
3744
|
+
"""
|
|
3745
|
+
try:
|
|
3746
|
+
parsed_spec = json.loads(ui_spec) if isinstance(ui_spec, str) else ui_spec
|
|
3747
|
+
except json.JSONDecodeError as parse_error:
|
|
3748
|
+
return f"Failed to parse ui_spec JSON: {parse_error}"
|
|
3749
|
+
|
|
3750
|
+
if not isinstance(parsed_spec, dict):
|
|
3751
|
+
return "ui_spec must be a JSON object"
|
|
3752
|
+
|
|
3753
|
+
command = {
|
|
3754
|
+
"type": "generate_widget_blueprint_from_spec",
|
|
3755
|
+
"ui_spec": parsed_spec,
|
|
3756
|
+
}
|
|
3757
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3758
|
+
if response.get("success") or "widget_results" in response or "property_results" in response:
|
|
3759
|
+
return json.dumps(response)
|
|
3760
|
+
error_message = response.get("error", "Unknown error")
|
|
3761
|
+
raw_suffix = f" Raw response: {response.get('raw')}" if "raw" in response else ""
|
|
3762
|
+
return f"Failed to generate widget from spec: {error_message}{raw_suffix}"
|
|
3763
|
+
|
|
3764
|
+
|
|
3765
|
+
@mcp.tool()
|
|
3766
|
+
def export_widget_to_ui_spec(
|
|
3767
|
+
user_widget_path: str,
|
|
3768
|
+
include_properties: bool = True,
|
|
3769
|
+
property_names: str = ""
|
|
3770
|
+
) -> str:
|
|
3771
|
+
"""
|
|
3772
|
+
Export an existing Widget Blueprint into the strict JSON UI spec format.
|
|
3773
|
+
Args:
|
|
3774
|
+
user_widget_path: Path to widget blueprint (e.g., "/Game/UI/WBP_Menu").
|
|
3775
|
+
include_properties: Include property operations in exported spec.
|
|
3776
|
+
property_names: Optional comma-separated filter (e.g., "Text,Visibility,Slot.LayoutData,Texture").
|
|
3777
|
+
Empty exports all available properties.
|
|
3778
|
+
Returns:
|
|
3779
|
+
JSON response containing `ui_spec`.
|
|
3780
|
+
"""
|
|
3781
|
+
command = {
|
|
3782
|
+
"type": "export_widget_to_ui_spec",
|
|
3783
|
+
"user_widget_path": user_widget_path,
|
|
3784
|
+
"include_properties": include_properties,
|
|
3785
|
+
"property_names": property_names,
|
|
3786
|
+
}
|
|
3787
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3788
|
+
if response.get("success"):
|
|
3789
|
+
return json.dumps(response)
|
|
3790
|
+
error_message = response.get("error", "Unknown error")
|
|
3791
|
+
raw_suffix = f" Raw response: {response.get('raw')}" if "raw" in response else ""
|
|
3792
|
+
return f"Failed to export widget to ui_spec: {error_message}{raw_suffix}"
|
|
3793
|
+
|
|
3794
|
+
|
|
3795
|
+
@mcp.tool()
|
|
3796
|
+
def auto_slice_ui_mockup(
|
|
3797
|
+
source_image_path: str,
|
|
3798
|
+
destination_folder: str = "/Game/UI/AutoSlices",
|
|
3799
|
+
base_name: str = "UI_Mockup",
|
|
3800
|
+
max_slices: int = 16,
|
|
3801
|
+
min_component_area_ratio: float = 0.0025,
|
|
3802
|
+
background_tolerance: int = 28,
|
|
3803
|
+
slice_mode: str = "auto",
|
|
3804
|
+
include_full_image: bool = True,
|
|
3805
|
+
enable_chroma_key: bool = True,
|
|
3806
|
+
chroma_tolerance: int = 24,
|
|
3807
|
+
enable_host_fallback: bool = True
|
|
3808
|
+
) -> str:
|
|
3809
|
+
"""
|
|
3810
|
+
Auto-slice a flattened UI mockup image into multiple textures and import them into Unreal.
|
|
3811
|
+
This is the first step for turning a single concept image into assignable widget art assets.
|
|
3812
|
+
Args:
|
|
3813
|
+
source_image_path: Absolute/local filesystem path to the mockup image file (PNG/JPG).
|
|
3814
|
+
destination_folder: Unreal content folder for imported textures (must start with /Game).
|
|
3815
|
+
base_name: Prefix used for generated texture asset names.
|
|
3816
|
+
max_slices: Maximum number of connected-component slices to generate.
|
|
3817
|
+
min_component_area_ratio: Ignore tiny components below this percentage of the full image area.
|
|
3818
|
+
background_tolerance: Color-distance threshold for separating foreground from a flat/checkerboard background.
|
|
3819
|
+
slice_mode: Segmentation mode: "auto" (default), "connected", or "aggressive".
|
|
3820
|
+
include_full_image: Also import the original full image as a fallback background texture.
|
|
3821
|
+
enable_chroma_key: If true, attempt simple corner-color chroma keying on each extracted slice.
|
|
3822
|
+
chroma_tolerance: RGB tolerance used by chroma-keying (higher removes more near-matching background).
|
|
3823
|
+
enable_host_fallback: If Unreal-side Pillow is missing, slice on MCP host and import results into Unreal.
|
|
3824
|
+
Returns:
|
|
3825
|
+
Full JSON response with a `manifest` field containing imported asset paths and slice metadata.
|
|
3826
|
+
"""
|
|
3827
|
+
response = _run_auto_slice_with_fallback(
|
|
3828
|
+
source_image_path=source_image_path,
|
|
3829
|
+
destination_folder=destination_folder,
|
|
3830
|
+
base_name=base_name,
|
|
3831
|
+
max_slices=max_slices,
|
|
3832
|
+
min_component_area_ratio=min_component_area_ratio,
|
|
3833
|
+
background_tolerance=background_tolerance,
|
|
3834
|
+
slice_mode=slice_mode,
|
|
3835
|
+
include_full_image=include_full_image,
|
|
3836
|
+
enable_chroma_key=enable_chroma_key,
|
|
3837
|
+
chroma_tolerance=chroma_tolerance,
|
|
3838
|
+
enable_host_fallback=enable_host_fallback,
|
|
3839
|
+
)
|
|
3840
|
+
if response.get("success"):
|
|
3841
|
+
return json.dumps(response)
|
|
3842
|
+
error_message = response.get("error", "Unknown error")
|
|
3843
|
+
raw_suffix = f" Raw response: {response.get('raw')}" if "raw" in response else ""
|
|
3844
|
+
return f"Failed to auto-slice UI mockup: {error_message}{raw_suffix}"
|
|
3845
|
+
|
|
3846
|
+
|
|
3847
|
+
@mcp.tool()
|
|
3848
|
+
def apply_ui_slices_to_widget(
|
|
3849
|
+
user_widget_path: str,
|
|
3850
|
+
slice_manifest: str,
|
|
3851
|
+
assign_background: bool = True,
|
|
3852
|
+
assign_components: bool = True,
|
|
3853
|
+
max_component_assignments: int = 12,
|
|
3854
|
+
apply_layout_from_slices: bool = True,
|
|
3855
|
+
layout_scale: float = 1.0,
|
|
3856
|
+
auto_create_buttons_if_missing: bool = True
|
|
3857
|
+
) -> str:
|
|
3858
|
+
"""
|
|
3859
|
+
Apply an auto-slice manifest to a Widget Blueprint's UImage/UButton widgets.
|
|
3860
|
+
The tool uses name/geometry heuristics to map generated textures, and can also place widgets
|
|
3861
|
+
using slice bbox coordinates from the source image.
|
|
3862
|
+
Args:
|
|
3863
|
+
user_widget_path: Target widget blueprint path (e.g., "/Game/UI/WBP_RateUs").
|
|
3864
|
+
slice_manifest: JSON string from `auto_slice_ui_mockup` response field `manifest`.
|
|
3865
|
+
assign_background: If true, assign the best background candidate first.
|
|
3866
|
+
assign_components: If true, assign remaining slices to image/button widgets heuristically.
|
|
3867
|
+
max_component_assignments: Max number of non-background slice assignments.
|
|
3868
|
+
apply_layout_from_slices: If true, apply CanvasPanel slot layout from slice bbox coordinates.
|
|
3869
|
+
layout_scale: Uniform multiplier for bbox-based placement/sizing (useful for DPI/canvas differences).
|
|
3870
|
+
auto_create_buttons_if_missing: If true, auto-add missing slice targets (Button/Image), including button widgets for button-like slices.
|
|
3871
|
+
Returns:
|
|
3872
|
+
JSON response with assignment/failure details.
|
|
3873
|
+
"""
|
|
3874
|
+
try:
|
|
3875
|
+
manifest_obj = json.loads(slice_manifest) if isinstance(slice_manifest, str) else slice_manifest
|
|
3876
|
+
except json.JSONDecodeError:
|
|
3877
|
+
return "Failed to parse slice_manifest JSON: invalid format"
|
|
3878
|
+
|
|
3879
|
+
provisioning_result = None
|
|
3880
|
+
if auto_create_buttons_if_missing and isinstance(manifest_obj, dict):
|
|
3881
|
+
provisioning_result = _ensure_slice_target_widgets_for_manifest(
|
|
3882
|
+
user_widget_path=user_widget_path,
|
|
3883
|
+
manifest=manifest_obj,
|
|
3884
|
+
max_auto_components=max_component_assignments,
|
|
3885
|
+
)
|
|
3886
|
+
|
|
3887
|
+
command = {
|
|
3888
|
+
"type": "apply_ui_slices_to_widget",
|
|
3889
|
+
"user_widget_path": user_widget_path,
|
|
3890
|
+
"slice_manifest": manifest_obj,
|
|
3891
|
+
"assign_background": assign_background,
|
|
3892
|
+
"assign_components": assign_components,
|
|
3893
|
+
"max_component_assignments": max_component_assignments,
|
|
3894
|
+
"apply_layout_from_slices": apply_layout_from_slices,
|
|
3895
|
+
"layout_scale": layout_scale
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
response = _ensure_dict_response(send_to_unreal(command))
|
|
3899
|
+
if response.get("success"):
|
|
3900
|
+
if isinstance(provisioning_result, dict):
|
|
3901
|
+
response["button_widget_provisioning"] = provisioning_result
|
|
3902
|
+
response["target_widget_provisioning"] = provisioning_result
|
|
3903
|
+
return json.dumps(response)
|
|
3904
|
+
error_message = response.get("error", "Unknown error")
|
|
3905
|
+
raw_suffix = f" Raw response: {response.get('raw')}" if "raw" in response else ""
|
|
3906
|
+
return f"Failed to apply UI slices: {error_message}{raw_suffix}"
|
|
3907
|
+
|
|
3908
|
+
|
|
3909
|
+
@mcp.tool()
|
|
3910
|
+
def auto_slice_and_apply_ui_mockup(
|
|
3911
|
+
user_widget_path: str,
|
|
3912
|
+
source_image_path: str,
|
|
3913
|
+
destination_folder: str = "/Game/UI/AutoSlices",
|
|
3914
|
+
base_name: str = "UI_Mockup",
|
|
3915
|
+
max_slices: int = 16,
|
|
3916
|
+
min_component_area_ratio: float = 0.0025,
|
|
3917
|
+
background_tolerance: int = 28,
|
|
3918
|
+
slice_mode: str = "auto",
|
|
3919
|
+
include_full_image: bool = True,
|
|
3920
|
+
enable_chroma_key: bool = True,
|
|
3921
|
+
chroma_tolerance: int = 24,
|
|
3922
|
+
assign_background: bool = True,
|
|
3923
|
+
assign_components: bool = True,
|
|
3924
|
+
max_component_assignments: int = 12,
|
|
3925
|
+
apply_layout_from_slices: bool = True,
|
|
3926
|
+
layout_scale: float = 1.0,
|
|
3927
|
+
enable_host_fallback: bool = True,
|
|
3928
|
+
auto_create_buttons_if_missing: bool = True
|
|
3929
|
+
) -> str:
|
|
3930
|
+
"""
|
|
3931
|
+
Full one-shot pipeline: slice/import textures from a flattened mockup image and auto-assign them to widget images.
|
|
3932
|
+
Args:
|
|
3933
|
+
user_widget_path: Target widget blueprint path.
|
|
3934
|
+
source_image_path: Local image path for the mockup.
|
|
3935
|
+
destination_folder: Unreal content folder where generated texture assets will be imported.
|
|
3936
|
+
base_name: Asset-name prefix for imported textures.
|
|
3937
|
+
max_slices: Maximum number of slice components to generate.
|
|
3938
|
+
min_component_area_ratio: Minimum connected-component area ratio retained as a slice.
|
|
3939
|
+
background_tolerance: Color tolerance for background subtraction in flattened images.
|
|
3940
|
+
slice_mode: Segmentation mode: "auto" (default), "connected", or "aggressive".
|
|
3941
|
+
include_full_image: Import full image as fallback texture.
|
|
3942
|
+
enable_chroma_key: If true, attempt simple corner-color chroma keying on each extracted slice.
|
|
3943
|
+
chroma_tolerance: RGB tolerance used by chroma-keying (higher removes more near-matching background).
|
|
3944
|
+
assign_background: Auto-assign background-like slice to background-like widget.
|
|
3945
|
+
assign_components: Auto-assign remaining slices to image placeholders.
|
|
3946
|
+
max_component_assignments: Maximum number of component assignments.
|
|
3947
|
+
apply_layout_from_slices: Apply CanvasPanel slot layout from slice bbox coordinates.
|
|
3948
|
+
layout_scale: Uniform multiplier for bbox-based placement/sizing.
|
|
3949
|
+
enable_host_fallback: If Unreal-side Pillow is missing, slice on MCP host and then apply manifest.
|
|
3950
|
+
auto_create_buttons_if_missing: If true, auto-add missing slice targets (Button/Image), including button widgets for button-like slices.
|
|
3951
|
+
Returns:
|
|
3952
|
+
Full JSON with both slice and apply results.
|
|
3953
|
+
"""
|
|
3954
|
+
slice_response = _run_auto_slice_with_fallback(
|
|
3955
|
+
source_image_path=source_image_path,
|
|
3956
|
+
destination_folder=destination_folder,
|
|
3957
|
+
base_name=base_name,
|
|
3958
|
+
max_slices=max_slices,
|
|
3959
|
+
min_component_area_ratio=min_component_area_ratio,
|
|
3960
|
+
background_tolerance=background_tolerance,
|
|
3961
|
+
slice_mode=slice_mode,
|
|
3962
|
+
include_full_image=include_full_image,
|
|
3963
|
+
enable_chroma_key=enable_chroma_key,
|
|
3964
|
+
chroma_tolerance=chroma_tolerance,
|
|
3965
|
+
enable_host_fallback=enable_host_fallback,
|
|
3966
|
+
)
|
|
3967
|
+
if not slice_response.get("success"):
|
|
3968
|
+
error_message = slice_response.get("error", "Unknown error")
|
|
3969
|
+
raw_suffix = f" Raw response: {slice_response.get('raw')}" if "raw" in slice_response else ""
|
|
3970
|
+
return f"Failed auto-slice/apply pipeline: {error_message}{raw_suffix}"
|
|
3971
|
+
|
|
3972
|
+
provisioning_result = None
|
|
3973
|
+
if auto_create_buttons_if_missing:
|
|
3974
|
+
manifest_obj = slice_response.get("manifest")
|
|
3975
|
+
if isinstance(manifest_obj, dict):
|
|
3976
|
+
provisioning_result = _ensure_slice_target_widgets_for_manifest(
|
|
3977
|
+
user_widget_path=user_widget_path,
|
|
3978
|
+
manifest=manifest_obj,
|
|
3979
|
+
max_auto_components=max_component_assignments,
|
|
3980
|
+
)
|
|
3981
|
+
|
|
3982
|
+
apply_command = {
|
|
3983
|
+
"type": "apply_ui_slices_to_widget",
|
|
3984
|
+
"user_widget_path": user_widget_path,
|
|
3985
|
+
"slice_manifest": slice_response.get("manifest"),
|
|
3986
|
+
"assign_background": assign_background,
|
|
3987
|
+
"assign_components": assign_components,
|
|
3988
|
+
"max_component_assignments": max_component_assignments,
|
|
3989
|
+
"apply_layout_from_slices": apply_layout_from_slices,
|
|
3990
|
+
"layout_scale": layout_scale,
|
|
3991
|
+
}
|
|
3992
|
+
apply_response = _ensure_dict_response(send_to_unreal(apply_command))
|
|
3993
|
+
combined_response = {
|
|
3994
|
+
"success": bool(slice_response.get("success")) and bool(apply_response.get("success")),
|
|
3995
|
+
"message": "Auto-slice and apply pipeline completed.",
|
|
3996
|
+
"slice_result": slice_response,
|
|
3997
|
+
"apply_result": apply_response,
|
|
3998
|
+
"manifest": slice_response.get("manifest"),
|
|
3999
|
+
"host_fallback_used": bool(slice_response.get("host_fallback_used")),
|
|
4000
|
+
}
|
|
4001
|
+
if isinstance(provisioning_result, dict):
|
|
4002
|
+
combined_response["button_widget_provisioning"] = provisioning_result
|
|
4003
|
+
combined_response["target_widget_provisioning"] = provisioning_result
|
|
4004
|
+
if combined_response.get("success"):
|
|
4005
|
+
return json.dumps(combined_response)
|
|
4006
|
+
|
|
4007
|
+
apply_error = apply_response.get("error", "Unknown apply error")
|
|
4008
|
+
return f"Failed auto-slice/apply pipeline: {apply_error}"
|
|
4009
|
+
|
|
4010
|
+
|
|
4011
|
+
# Input
|
|
4012
|
+
@mcp.tool()
|
|
4013
|
+
def add_input_binding(action_name: str, key: str) -> str:
|
|
4014
|
+
"""
|
|
4015
|
+
Add an input action binding to Project Settings.
|
|
4016
|
+
Args:
|
|
4017
|
+
action_name: Name of the action (e.g., "Flap")
|
|
4018
|
+
key: Key to bind (e.g., "Space Bar")
|
|
4019
|
+
"""
|
|
4020
|
+
command = {"type": "add_input_binding", "action_name": action_name, "key": key}
|
|
4021
|
+
response = send_to_unreal(command)
|
|
4022
|
+
if response.get("success"):
|
|
4023
|
+
message = response.get("message", f"Added input binding {action_name} -> {key}")
|
|
4024
|
+
details = response.get("details")
|
|
4025
|
+
if details:
|
|
4026
|
+
return f"{message} ({details})"
|
|
4027
|
+
return message
|
|
4028
|
+
|
|
4029
|
+
error_message = response.get("error", "Unknown error")
|
|
4030
|
+
details = response.get("details")
|
|
4031
|
+
if details:
|
|
4032
|
+
return f"Failed: {error_message} | {details}"
|
|
4033
|
+
return f"Failed: {error_message}"
|
|
4034
|
+
|
|
4035
|
+
#
|
|
4036
|
+
# Actor Management Tools
|
|
4037
|
+
#
|
|
4038
|
+
|
|
4039
|
+
@mcp.tool()
|
|
4040
|
+
def find_actors_by_name(name_pattern: str, exact_match: bool = False) -> str:
|
|
4041
|
+
"""
|
|
4042
|
+
Find actors in the scene by name pattern.
|
|
4043
|
+
Args:
|
|
4044
|
+
name_pattern: Name pattern to search for (supports wildcards like "*")
|
|
4045
|
+
exact_match: If True, perform exact match instead of pattern matching
|
|
4046
|
+
Returns:
|
|
4047
|
+
JSON string with list of matching actors
|
|
4048
|
+
"""
|
|
4049
|
+
command = {
|
|
4050
|
+
"type": "find_actors_by_name",
|
|
4051
|
+
"name_pattern": name_pattern,
|
|
4052
|
+
"exact_match": exact_match
|
|
4053
|
+
}
|
|
4054
|
+
response = send_to_unreal(command)
|
|
4055
|
+
if response.get("success"):
|
|
4056
|
+
actors = response.get("actors", [])
|
|
4057
|
+
return json.dumps({"success": True, "actors": actors, "count": len(actors)})
|
|
4058
|
+
return json.dumps({"success": False, "error": response.get("error", "Unknown error")})
|
|
4059
|
+
|
|
4060
|
+
@mcp.tool()
|
|
4061
|
+
def delete_actor(actor_name: str) -> str:
|
|
4062
|
+
"""
|
|
4063
|
+
Delete an actor from the level.
|
|
4064
|
+
Args:
|
|
4065
|
+
actor_name: Name of the actor to delete
|
|
4066
|
+
Returns:
|
|
4067
|
+
Message indicating success or failure
|
|
4068
|
+
"""
|
|
4069
|
+
command = {"type": "delete_actor", "actor_name": actor_name}
|
|
4070
|
+
response = send_to_unreal(command)
|
|
4071
|
+
if response.get("success"):
|
|
4072
|
+
return response.get("message", f"Deleted actor: {actor_name}")
|
|
4073
|
+
return f"Failed to delete actor: {response.get('error', 'Unknown error')}"
|
|
4074
|
+
|
|
4075
|
+
@mcp.tool()
|
|
4076
|
+
def set_actor_transform(actor_name: str, location: Optional[List[float]] = None, rotation: Optional[List[float]] = None, scale: Optional[List[float]] = None) -> str:
|
|
4077
|
+
"""
|
|
4078
|
+
Set actor transform (position, rotation, scale) in one call.
|
|
4079
|
+
Args:
|
|
4080
|
+
actor_name: Name of the actor
|
|
4081
|
+
location: [X, Y, Z] coordinates (optional)
|
|
4082
|
+
rotation: [Pitch, Yaw, Roll] in degrees (optional)
|
|
4083
|
+
scale: [X, Y, Z] scale factors (optional)
|
|
4084
|
+
Returns:
|
|
4085
|
+
Message indicating success or failure
|
|
4086
|
+
"""
|
|
4087
|
+
command = {
|
|
4088
|
+
"type": "set_actor_transform",
|
|
4089
|
+
"actor_name": actor_name,
|
|
4090
|
+
"location": location,
|
|
4091
|
+
"rotation": rotation,
|
|
4092
|
+
"scale": scale
|
|
4093
|
+
}
|
|
4094
|
+
response = send_to_unreal(command)
|
|
4095
|
+
if response.get("success"):
|
|
4096
|
+
return response.get("message", f"Set transform for {actor_name}")
|
|
4097
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4098
|
+
|
|
4099
|
+
@mcp.tool()
|
|
4100
|
+
def get_actor_material_info(actor_name: str) -> str:
|
|
4101
|
+
"""
|
|
4102
|
+
Get material information from an actor.
|
|
4103
|
+
Args:
|
|
4104
|
+
actor_name: Name of the actor
|
|
4105
|
+
Returns:
|
|
4106
|
+
JSON string with material information
|
|
4107
|
+
"""
|
|
4108
|
+
command = {"type": "get_actor_material_info", "actor_name": actor_name}
|
|
4109
|
+
response = send_to_unreal(command)
|
|
4110
|
+
return json.dumps(response) if response else json.dumps({"success": False, "error": "No response"})
|
|
4111
|
+
|
|
4112
|
+
#
|
|
4113
|
+
# Material Management Tools
|
|
4114
|
+
#
|
|
4115
|
+
|
|
4116
|
+
@mcp.tool()
|
|
4117
|
+
def get_available_materials(folder_path: str = "/Game/Materials", recursive: bool = True) -> str:
|
|
4118
|
+
"""
|
|
4119
|
+
Get all available materials in the project.
|
|
4120
|
+
Args:
|
|
4121
|
+
folder_path: Folder path to search in (default: "/Game/Materials")
|
|
4122
|
+
recursive: Whether to search recursively (default: True)
|
|
4123
|
+
Returns:
|
|
4124
|
+
JSON string with list of materials
|
|
4125
|
+
"""
|
|
4126
|
+
command = {
|
|
4127
|
+
"type": "get_available_materials",
|
|
4128
|
+
"folder_path": folder_path,
|
|
4129
|
+
"recursive": recursive
|
|
4130
|
+
}
|
|
4131
|
+
response = send_to_unreal(command)
|
|
4132
|
+
return json.dumps(response) if response else json.dumps({"success": False, "error": "No response"})
|
|
4133
|
+
|
|
4134
|
+
@mcp.tool()
|
|
4135
|
+
def apply_material_to_actor(actor_name: str, material_path: str, material_slot: int = 0) -> str:
|
|
4136
|
+
"""
|
|
4137
|
+
Apply a material to a scene actor.
|
|
4138
|
+
Args:
|
|
4139
|
+
actor_name: Name of the actor
|
|
4140
|
+
material_path: Path to the material (e.g., "/Game/Materials/MyMaterial")
|
|
4141
|
+
material_slot: Material slot index (default: 0)
|
|
4142
|
+
Returns:
|
|
4143
|
+
Message indicating success or failure
|
|
4144
|
+
"""
|
|
4145
|
+
command = {
|
|
4146
|
+
"type": "apply_material_to_actor",
|
|
4147
|
+
"actor_name": actor_name,
|
|
4148
|
+
"material_path": material_path,
|
|
4149
|
+
"material_slot": material_slot
|
|
4150
|
+
}
|
|
4151
|
+
response = send_to_unreal(command)
|
|
4152
|
+
if response.get("success"):
|
|
4153
|
+
return response.get("message", f"Applied material to {actor_name}")
|
|
4154
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4155
|
+
|
|
4156
|
+
@mcp.tool()
|
|
4157
|
+
def apply_material_to_blueprint(blueprint_path: str, component_name: str, material_path: str, material_slot: int = 0) -> str:
|
|
4158
|
+
"""
|
|
4159
|
+
Apply a material to a Blueprint component.
|
|
4160
|
+
Args:
|
|
4161
|
+
blueprint_path: Path to the Blueprint
|
|
4162
|
+
component_name: Name of the component
|
|
4163
|
+
material_path: Path to the material
|
|
4164
|
+
material_slot: Material slot index (default: 0)
|
|
4165
|
+
Returns:
|
|
4166
|
+
Message indicating success or failure
|
|
4167
|
+
"""
|
|
4168
|
+
command = {
|
|
4169
|
+
"type": "apply_material_to_blueprint",
|
|
4170
|
+
"blueprint_path": blueprint_path,
|
|
4171
|
+
"component_name": component_name,
|
|
4172
|
+
"material_path": material_path,
|
|
4173
|
+
"material_slot": material_slot
|
|
4174
|
+
}
|
|
4175
|
+
response = send_to_unreal(command)
|
|
4176
|
+
if response.get("success"):
|
|
4177
|
+
return response.get("message", f"Applied material to {component_name}")
|
|
4178
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4179
|
+
|
|
4180
|
+
@mcp.tool()
|
|
4181
|
+
def set_mesh_material_color(material_path: str, color: List[float], property_name: str = "BaseColor") -> str:
|
|
4182
|
+
"""
|
|
4183
|
+
Set material color properties on a mesh material.
|
|
4184
|
+
Args:
|
|
4185
|
+
material_path: Path to the material to modify
|
|
4186
|
+
color: [R, G, B] color values (0-1)
|
|
4187
|
+
property_name: Material property name (default: "BaseColor")
|
|
4188
|
+
Returns:
|
|
4189
|
+
Message indicating success or failure
|
|
4190
|
+
"""
|
|
4191
|
+
normalized_color = _coerce_numeric_list(color)
|
|
4192
|
+
if len(normalized_color) == 3:
|
|
4193
|
+
color = normalized_color
|
|
4194
|
+
|
|
4195
|
+
command = {
|
|
4196
|
+
"type": "set_mesh_material_color",
|
|
4197
|
+
"material_path": material_path,
|
|
4198
|
+
"color": color,
|
|
4199
|
+
"property_name": property_name
|
|
4200
|
+
}
|
|
4201
|
+
response = send_to_unreal(command)
|
|
4202
|
+
if response.get("success"):
|
|
4203
|
+
return response.get("message", f"Set color for {material_path}")
|
|
4204
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4205
|
+
|
|
4206
|
+
#
|
|
4207
|
+
# Physics Tools
|
|
4208
|
+
#
|
|
4209
|
+
|
|
4210
|
+
@mcp.tool()
|
|
4211
|
+
def spawn_physics_blueprint_actor(blueprint_path: str, location: Optional[List[float]] = None, rotation: Optional[List[float]] = None, scale: Optional[List[float]] = None, actor_label: Optional[str] = None, simulate_physics: bool = True) -> str:
|
|
4212
|
+
"""
|
|
4213
|
+
Spawn a Blueprint actor with physics enabled.
|
|
4214
|
+
Args:
|
|
4215
|
+
blueprint_path: Path to the Blueprint asset
|
|
4216
|
+
location: [X, Y, Z] coordinates (optional)
|
|
4217
|
+
rotation: [Pitch, Yaw, Roll] in degrees (optional)
|
|
4218
|
+
scale: [X, Y, Z] scale factors (optional)
|
|
4219
|
+
actor_label: Optional custom name for the actor
|
|
4220
|
+
simulate_physics: Enable physics simulation (default: True)
|
|
4221
|
+
Returns:
|
|
4222
|
+
Message indicating success or failure
|
|
4223
|
+
"""
|
|
4224
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4225
|
+
rotation = _ensure_vector(rotation, [0.0, 0.0, 0.0])
|
|
4226
|
+
scale = _ensure_vector(scale, [1.0, 1.0, 1.0])
|
|
4227
|
+
|
|
4228
|
+
command = {
|
|
4229
|
+
"type": "spawn_physics_blueprint_actor",
|
|
4230
|
+
"blueprint_path": blueprint_path,
|
|
4231
|
+
"location": location,
|
|
4232
|
+
"rotation": rotation,
|
|
4233
|
+
"scale": scale,
|
|
4234
|
+
"actor_label": actor_label,
|
|
4235
|
+
"simulate_physics": simulate_physics
|
|
4236
|
+
}
|
|
4237
|
+
response = send_to_unreal(command)
|
|
4238
|
+
if response.get("success"):
|
|
4239
|
+
actor_name = response.get("actor_name", "actor")
|
|
4240
|
+
return f"Successfully spawned physics-enabled {blueprint_path} as {actor_name}"
|
|
4241
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4242
|
+
|
|
4243
|
+
@mcp.tool()
|
|
4244
|
+
def set_physics_properties(actor_name: str, mass: Optional[float] = None, linear_damping: Optional[float] = None, angular_damping: Optional[float] = None, enable_gravity: Optional[bool] = None, simulate_physics: Optional[bool] = None, collision_enabled: Optional[bool] = None) -> str:
|
|
4245
|
+
"""
|
|
4246
|
+
Set physics properties on an actor.
|
|
4247
|
+
Args:
|
|
4248
|
+
actor_name: Name of the actor
|
|
4249
|
+
mass: Mass in kg (optional)
|
|
4250
|
+
linear_damping: Linear damping coefficient (optional)
|
|
4251
|
+
angular_damping: Angular damping coefficient (optional)
|
|
4252
|
+
enable_gravity: Enable gravity (optional)
|
|
4253
|
+
simulate_physics: Enable physics simulation (optional)
|
|
4254
|
+
collision_enabled: Enable collision (optional)
|
|
4255
|
+
Returns:
|
|
4256
|
+
Message indicating success or failure
|
|
4257
|
+
"""
|
|
4258
|
+
command = {
|
|
4259
|
+
"type": "set_physics_properties",
|
|
4260
|
+
"actor_name": actor_name
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
# Only include provided parameters
|
|
4264
|
+
if mass is not None:
|
|
4265
|
+
command["mass"] = mass
|
|
4266
|
+
if linear_damping is not None:
|
|
4267
|
+
command["linear_damping"] = linear_damping
|
|
4268
|
+
if angular_damping is not None:
|
|
4269
|
+
command["angular_damping"] = angular_damping
|
|
4270
|
+
if enable_gravity is not None:
|
|
4271
|
+
command["enable_gravity"] = enable_gravity
|
|
4272
|
+
if simulate_physics is not None:
|
|
4273
|
+
command["simulate_physics"] = simulate_physics
|
|
4274
|
+
if collision_enabled is not None:
|
|
4275
|
+
command["collision_enabled"] = collision_enabled
|
|
4276
|
+
|
|
4277
|
+
response = send_to_unreal(command)
|
|
4278
|
+
if response.get("success"):
|
|
4279
|
+
return response.get("message", f"Set physics properties for {actor_name}")
|
|
4280
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4281
|
+
|
|
4282
|
+
#
|
|
4283
|
+
# World Building Tools
|
|
4284
|
+
#
|
|
4285
|
+
|
|
4286
|
+
@mcp.tool()
|
|
4287
|
+
def create_town(town_size: str = "medium", architectural_style: str = "modern", building_density: float = 0.7, include_advanced_features: bool = False, center_location: Optional[List[float]] = None) -> str:
|
|
4288
|
+
"""
|
|
4289
|
+
Generate an entire town with buildings, streets, and infrastructure.
|
|
4290
|
+
Args:
|
|
4291
|
+
town_size: Size of the town ("small", "medium", "large", "massive")
|
|
4292
|
+
architectural_style: Style of buildings ("medieval", "modern", "futuristic")
|
|
4293
|
+
building_density: Density of buildings (0.0-1.0, default: 0.7)
|
|
4294
|
+
include_advanced_features: Include parks, fountains, etc. (default: False)
|
|
4295
|
+
center_location: [X, Y, Z] center location (optional)
|
|
4296
|
+
Returns:
|
|
4297
|
+
Message indicating success with town information
|
|
4298
|
+
"""
|
|
4299
|
+
center_location = _ensure_vector(center_location, [0.0, 0.0, 0.0])
|
|
4300
|
+
|
|
4301
|
+
command = {
|
|
4302
|
+
"type": "create_town",
|
|
4303
|
+
"town_size": town_size,
|
|
4304
|
+
"architectural_style": architectural_style,
|
|
4305
|
+
"building_density": building_density,
|
|
4306
|
+
"include_advanced_features": include_advanced_features,
|
|
4307
|
+
"center_location": center_location
|
|
4308
|
+
}
|
|
4309
|
+
response = send_to_unreal(command)
|
|
4310
|
+
if response.get("success"):
|
|
4311
|
+
count = response.get("count", 0)
|
|
4312
|
+
return f"Successfully created {town_size} town with {count} buildings"
|
|
4313
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4314
|
+
|
|
4315
|
+
@mcp.tool()
|
|
4316
|
+
def construct_house(house_style: str = "simple", width: float = 800.0, height: float = 400.0, depth: float = 600.0, num_rooms: int = 4, location: Optional[List[float]] = None) -> str:
|
|
4317
|
+
"""
|
|
4318
|
+
Build a multi-room house with windows, doors, and roof.
|
|
4319
|
+
Args:
|
|
4320
|
+
house_style: Style of house ("simple", "modern", "victorian")
|
|
4321
|
+
width: Width of the house (default: 800)
|
|
4322
|
+
height: Height of the house (default: 400)
|
|
4323
|
+
depth: Depth of the house (default: 600)
|
|
4324
|
+
num_rooms: Number of rooms (default: 4)
|
|
4325
|
+
location: [X, Y, Z] location (optional)
|
|
4326
|
+
Returns:
|
|
4327
|
+
Message indicating success with house information
|
|
4328
|
+
"""
|
|
4329
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4330
|
+
|
|
4331
|
+
command = {
|
|
4332
|
+
"type": "construct_house",
|
|
4333
|
+
"house_style": house_style,
|
|
4334
|
+
"width": width,
|
|
4335
|
+
"height": height,
|
|
4336
|
+
"depth": depth,
|
|
4337
|
+
"num_rooms": num_rooms,
|
|
4338
|
+
"location": location
|
|
4339
|
+
}
|
|
4340
|
+
response = send_to_unreal(command)
|
|
4341
|
+
if response.get("success"):
|
|
4342
|
+
count = response.get("count", 0)
|
|
4343
|
+
return f"Successfully constructed {house_style} house with {num_rooms} rooms ({count} parts)"
|
|
4344
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4345
|
+
|
|
4346
|
+
@mcp.tool()
|
|
4347
|
+
def construct_mansion(width: float = 1500.0, height: float = 900.0, depth: float = 1200.0, num_wings: int = 2, num_towers: int = 4, location: Optional[List[float]] = None) -> str:
|
|
4348
|
+
"""
|
|
4349
|
+
Create a mansion complex with wings and towers.
|
|
4350
|
+
Args:
|
|
4351
|
+
width: Width of main building (default: 1500)
|
|
4352
|
+
height: Height of main building (default: 900)
|
|
4353
|
+
depth: Depth of main building (default: 1200)
|
|
4354
|
+
num_wings: Number of wings (default: 2)
|
|
4355
|
+
num_towers: Number of towers (default: 4)
|
|
4356
|
+
location: [X, Y, Z] location (optional)
|
|
4357
|
+
Returns:
|
|
4358
|
+
Message indicating success with mansion information
|
|
4359
|
+
"""
|
|
4360
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4361
|
+
|
|
4362
|
+
command = {
|
|
4363
|
+
"type": "construct_mansion",
|
|
4364
|
+
"width": width,
|
|
4365
|
+
"height": height,
|
|
4366
|
+
"depth": depth,
|
|
4367
|
+
"num_wings": num_wings,
|
|
4368
|
+
"num_towers": num_towers,
|
|
4369
|
+
"location": location
|
|
4370
|
+
}
|
|
4371
|
+
response = send_to_unreal(command)
|
|
4372
|
+
if response.get("success"):
|
|
4373
|
+
count = response.get("count", 0)
|
|
4374
|
+
return f"Successfully constructed mansion with {num_wings} wings and {num_towers} towers ({count} parts)"
|
|
4375
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4376
|
+
|
|
4377
|
+
@mcp.tool()
|
|
4378
|
+
def create_tower(tower_style: str = "medieval", height: float = 1000.0, radius: float = 200.0, location: Optional[List[float]] = None) -> str:
|
|
4379
|
+
"""
|
|
4380
|
+
Create a tower structure.
|
|
4381
|
+
Args:
|
|
4382
|
+
tower_style: Style of tower ("medieval", "modern", "futuristic")
|
|
4383
|
+
height: Height of the tower (default: 1000)
|
|
4384
|
+
radius: Radius of the tower (default: 200)
|
|
4385
|
+
location: [X, Y, Z] location (optional)
|
|
4386
|
+
Returns:
|
|
4387
|
+
Message indicating success
|
|
4388
|
+
"""
|
|
4389
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4390
|
+
|
|
4391
|
+
command = {
|
|
4392
|
+
"type": "create_tower",
|
|
4393
|
+
"tower_style": tower_style,
|
|
4394
|
+
"height": height,
|
|
4395
|
+
"radius": radius,
|
|
4396
|
+
"location": location
|
|
4397
|
+
}
|
|
4398
|
+
response = send_to_unreal(command)
|
|
4399
|
+
if response.get("success"):
|
|
4400
|
+
return f"Successfully created {tower_style} tower"
|
|
4401
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4402
|
+
|
|
4403
|
+
@mcp.tool()
|
|
4404
|
+
def create_arch(width: float = 400.0, height: float = 600.0, depth: float = 200.0, location: Optional[List[float]] = None) -> str:
|
|
4405
|
+
"""
|
|
4406
|
+
Create an architectural arch.
|
|
4407
|
+
Args:
|
|
4408
|
+
width: Width of the arch (default: 400)
|
|
4409
|
+
height: Height of the arch (default: 600)
|
|
4410
|
+
depth: Depth of the arch (default: 200)
|
|
4411
|
+
location: [X, Y, Z] location (optional)
|
|
4412
|
+
Returns:
|
|
4413
|
+
Message indicating success
|
|
4414
|
+
"""
|
|
4415
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4416
|
+
|
|
4417
|
+
command = {
|
|
4418
|
+
"type": "create_arch",
|
|
4419
|
+
"width": width,
|
|
4420
|
+
"height": height,
|
|
4421
|
+
"depth": depth,
|
|
4422
|
+
"location": location
|
|
4423
|
+
}
|
|
4424
|
+
response = send_to_unreal(command)
|
|
4425
|
+
if response.get("success"):
|
|
4426
|
+
count = response.get("count", 0)
|
|
4427
|
+
return f"Successfully created arch ({count} parts)"
|
|
4428
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4429
|
+
|
|
4430
|
+
@mcp.tool()
|
|
4431
|
+
def create_staircase(num_steps: int = 10, step_width: float = 200.0, step_depth: float = 50.0, step_height: float = 20.0, location: Optional[List[float]] = None) -> str:
|
|
4432
|
+
"""
|
|
4433
|
+
Create a staircase.
|
|
4434
|
+
Args:
|
|
4435
|
+
num_steps: Number of steps (default: 10)
|
|
4436
|
+
step_width: Width of each step (default: 200)
|
|
4437
|
+
step_depth: Depth of each step (default: 50)
|
|
4438
|
+
step_height: Height of each step (default: 20)
|
|
4439
|
+
location: [X, Y, Z] starting location (optional)
|
|
4440
|
+
Returns:
|
|
4441
|
+
Message indicating success
|
|
4442
|
+
"""
|
|
4443
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4444
|
+
|
|
4445
|
+
command = {
|
|
4446
|
+
"type": "create_staircase",
|
|
4447
|
+
"num_steps": num_steps,
|
|
4448
|
+
"step_width": step_width,
|
|
4449
|
+
"step_depth": step_depth,
|
|
4450
|
+
"step_height": step_height,
|
|
4451
|
+
"location": location
|
|
4452
|
+
}
|
|
4453
|
+
response = send_to_unreal(command)
|
|
4454
|
+
if response.get("success"):
|
|
4455
|
+
count = response.get("count", 0)
|
|
4456
|
+
return f"Successfully created staircase with {num_steps} steps"
|
|
4457
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4458
|
+
|
|
4459
|
+
@mcp.tool()
|
|
4460
|
+
def create_castle_fortress(size: str = "medium", num_towers: int = 4, wall_height: float = 800.0, location: Optional[List[float]] = None) -> str:
|
|
4461
|
+
"""
|
|
4462
|
+
Build a medieval fortress with walls and towers.
|
|
4463
|
+
Args:
|
|
4464
|
+
size: Size of the fortress ("small", "medium", "large")
|
|
4465
|
+
num_towers: Number of corner towers (default: 4)
|
|
4466
|
+
wall_height: Height of walls (default: 800)
|
|
4467
|
+
location: [X, Y, Z] center location (optional)
|
|
4468
|
+
Returns:
|
|
4469
|
+
Message indicating success with fortress information
|
|
4470
|
+
"""
|
|
4471
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4472
|
+
|
|
4473
|
+
command = {
|
|
4474
|
+
"type": "create_castle_fortress",
|
|
4475
|
+
"size": size,
|
|
4476
|
+
"num_towers": num_towers,
|
|
4477
|
+
"wall_height": wall_height,
|
|
4478
|
+
"location": location
|
|
4479
|
+
}
|
|
4480
|
+
response = send_to_unreal(command)
|
|
4481
|
+
if response.get("success"):
|
|
4482
|
+
count = response.get("count", 0)
|
|
4483
|
+
return f"Successfully created {size} fortress with {num_towers} towers ({count} parts)"
|
|
4484
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4485
|
+
|
|
4486
|
+
@mcp.tool()
|
|
4487
|
+
def create_suspension_bridge(length: float = 2000.0, width: float = 400.0, height: float = 200.0, location: Optional[List[float]] = None) -> str:
|
|
4488
|
+
"""
|
|
4489
|
+
Create a suspension bridge.
|
|
4490
|
+
Args:
|
|
4491
|
+
length: Length of the bridge (default: 2000)
|
|
4492
|
+
width: Width of the bridge (default: 400)
|
|
4493
|
+
height: Height above ground (default: 200)
|
|
4494
|
+
location: [X, Y, Z] starting location (optional)
|
|
4495
|
+
Returns:
|
|
4496
|
+
Message indicating success
|
|
4497
|
+
"""
|
|
4498
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4499
|
+
|
|
4500
|
+
command = {
|
|
4501
|
+
"type": "create_suspension_bridge",
|
|
4502
|
+
"length": length,
|
|
4503
|
+
"width": width,
|
|
4504
|
+
"height": height,
|
|
4505
|
+
"location": location
|
|
4506
|
+
}
|
|
4507
|
+
response = send_to_unreal(command)
|
|
4508
|
+
if response.get("success"):
|
|
4509
|
+
count = response.get("count", 0)
|
|
4510
|
+
return f"Successfully created suspension bridge ({count} parts)"
|
|
4511
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4512
|
+
|
|
4513
|
+
@mcp.tool()
|
|
4514
|
+
def create_aqueduct(length: float = 1500.0, height: float = 500.0, num_arches: int = 5, location: Optional[List[float]] = None) -> str:
|
|
4515
|
+
"""
|
|
4516
|
+
Create an aqueduct structure.
|
|
4517
|
+
Args:
|
|
4518
|
+
length: Length of the aqueduct (default: 1500)
|
|
4519
|
+
height: Height of supports (default: 500)
|
|
4520
|
+
num_arches: Number of arches (default: 5)
|
|
4521
|
+
location: [X, Y, Z] starting location (optional)
|
|
4522
|
+
Returns:
|
|
4523
|
+
Message indicating success
|
|
4524
|
+
"""
|
|
4525
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4526
|
+
|
|
4527
|
+
command = {
|
|
4528
|
+
"type": "create_aqueduct",
|
|
4529
|
+
"length": length,
|
|
4530
|
+
"height": height,
|
|
4531
|
+
"num_arches": num_arches,
|
|
4532
|
+
"location": location
|
|
4533
|
+
}
|
|
4534
|
+
response = send_to_unreal(command)
|
|
4535
|
+
if response.get("success"):
|
|
4536
|
+
count = response.get("count", 0)
|
|
4537
|
+
return f"Successfully created aqueduct with {num_arches} arches ({count} parts)"
|
|
4538
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4539
|
+
|
|
4540
|
+
@mcp.tool()
|
|
4541
|
+
def create_maze(rows: int = 15, cols: int = 15, cell_size: float = 250.0, wall_height: float = 4.0, location: Optional[List[float]] = None) -> str:
|
|
4542
|
+
"""
|
|
4543
|
+
Generate a solvable maze using recursive backtracking.
|
|
4544
|
+
Args:
|
|
4545
|
+
rows: Number of rows in the maze (default: 15)
|
|
4546
|
+
cols: Number of columns in the maze (default: 15)
|
|
4547
|
+
cell_size: Size of each cell (default: 250)
|
|
4548
|
+
wall_height: Height of walls (default: 4)
|
|
4549
|
+
location: [X, Y, Z] starting location (optional)
|
|
4550
|
+
Returns:
|
|
4551
|
+
Message indicating success with maze information
|
|
4552
|
+
"""
|
|
4553
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4554
|
+
|
|
4555
|
+
command = {
|
|
4556
|
+
"type": "create_maze",
|
|
4557
|
+
"rows": rows,
|
|
4558
|
+
"cols": cols,
|
|
4559
|
+
"cell_size": cell_size,
|
|
4560
|
+
"wall_height": wall_height,
|
|
4561
|
+
"location": location
|
|
4562
|
+
}
|
|
4563
|
+
response = send_to_unreal(command)
|
|
4564
|
+
if response.get("success"):
|
|
4565
|
+
count = response.get("count", 0)
|
|
4566
|
+
return f"Successfully created {rows}x{cols} maze with {count} walls"
|
|
4567
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4568
|
+
|
|
4569
|
+
@mcp.tool()
|
|
4570
|
+
def create_pyramid(base_size: float = 1000.0, height: float = 800.0, location: Optional[List[float]] = None) -> str:
|
|
4571
|
+
"""
|
|
4572
|
+
Create a pyramid structure.
|
|
4573
|
+
Args:
|
|
4574
|
+
base_size: Size of the base (default: 1000)
|
|
4575
|
+
height: Height of the pyramid (default: 800)
|
|
4576
|
+
location: [X, Y, Z] center location (optional)
|
|
4577
|
+
Returns:
|
|
4578
|
+
Message indicating success
|
|
4579
|
+
"""
|
|
4580
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4581
|
+
|
|
4582
|
+
command = {
|
|
4583
|
+
"type": "create_pyramid",
|
|
4584
|
+
"base_size": base_size,
|
|
4585
|
+
"height": height,
|
|
4586
|
+
"location": location
|
|
4587
|
+
}
|
|
4588
|
+
response = send_to_unreal(command)
|
|
4589
|
+
if response.get("success"):
|
|
4590
|
+
return f"Successfully created pyramid"
|
|
4591
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4592
|
+
|
|
4593
|
+
@mcp.tool()
|
|
4594
|
+
def create_wall(length: float = 1000.0, height: float = 400.0, thickness: float = 50.0, location: Optional[List[float]] = None, rotation: Optional[List[float]] = None) -> str:
|
|
4595
|
+
"""
|
|
4596
|
+
Create a wall structure.
|
|
4597
|
+
Args:
|
|
4598
|
+
length: Length of the wall (default: 1000)
|
|
4599
|
+
height: Height of the wall (default: 400)
|
|
4600
|
+
thickness: Thickness of the wall (default: 50)
|
|
4601
|
+
location: [X, Y, Z] center location (optional)
|
|
4602
|
+
rotation: [Pitch, Yaw, Roll] rotation in degrees (optional)
|
|
4603
|
+
Returns:
|
|
4604
|
+
Message indicating success
|
|
4605
|
+
"""
|
|
4606
|
+
location = _ensure_vector(location, [0.0, 0.0, 0.0])
|
|
4607
|
+
rotation = _ensure_vector(rotation, [0.0, 0.0, 0.0])
|
|
4608
|
+
|
|
4609
|
+
command = {
|
|
4610
|
+
"type": "create_wall",
|
|
4611
|
+
"length": length,
|
|
4612
|
+
"height": height,
|
|
4613
|
+
"thickness": thickness,
|
|
4614
|
+
"location": location,
|
|
4615
|
+
"rotation": rotation
|
|
4616
|
+
}
|
|
4617
|
+
response = send_to_unreal(command)
|
|
4618
|
+
if response.get("success"):
|
|
4619
|
+
return f"Successfully created wall"
|
|
4620
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4621
|
+
|
|
4622
|
+
#
|
|
4623
|
+
# Blueprint Enhancement
|
|
4624
|
+
#
|
|
4625
|
+
|
|
4626
|
+
@mcp.tool()
|
|
4627
|
+
def set_static_mesh_properties(blueprint_path: str, component_name: str, static_mesh_path: Optional[str] = None, collision_enabled: Optional[bool] = None, cast_shadow: Optional[bool] = None, receive_shadow: Optional[bool] = None) -> str:
|
|
4628
|
+
"""
|
|
4629
|
+
Set static mesh properties on a Blueprint component.
|
|
4630
|
+
Args:
|
|
4631
|
+
blueprint_path: Path to the Blueprint
|
|
4632
|
+
component_name: Name of the StaticMeshComponent
|
|
4633
|
+
static_mesh_path: Path to the static mesh to set (optional)
|
|
4634
|
+
collision_enabled: Enable collision (optional)
|
|
4635
|
+
cast_shadow: Cast shadows (optional)
|
|
4636
|
+
receive_shadow: Receive shadows (optional)
|
|
4637
|
+
Returns:
|
|
4638
|
+
Message indicating success or failure
|
|
4639
|
+
"""
|
|
4640
|
+
command = {
|
|
4641
|
+
"type": "set_static_mesh_properties",
|
|
4642
|
+
"blueprint_path": blueprint_path,
|
|
4643
|
+
"component_name": component_name
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
if static_mesh_path is not None:
|
|
4647
|
+
command["static_mesh_path"] = static_mesh_path
|
|
4648
|
+
if collision_enabled is not None:
|
|
4649
|
+
command["collision_enabled"] = collision_enabled
|
|
4650
|
+
if cast_shadow is not None:
|
|
4651
|
+
command["cast_shadow"] = cast_shadow
|
|
4652
|
+
if receive_shadow is not None:
|
|
4653
|
+
command["receive_shadow"] = receive_shadow
|
|
4654
|
+
|
|
4655
|
+
response = send_to_unreal(command)
|
|
4656
|
+
if response.get("success"):
|
|
4657
|
+
props_set = response.get("properties_set", [])
|
|
4658
|
+
return f"Successfully set properties for {component_name}: {', '.join(props_set)}"
|
|
4659
|
+
return f"Failed: {response.get('error', 'Unknown error')}"
|
|
4660
|
+
|
|
4661
|
+
# Safety check for potentially destructive actions
|
|
4662
|
+
def is_potentially_destructive(script: str) -> bool:
|
|
4663
|
+
"""
|
|
4664
|
+
Check if the script contains potentially destructive actions like deleting or saving files.
|
|
4665
|
+
Returns True if such actions are detected and not explicitly requested.
|
|
4666
|
+
"""
|
|
4667
|
+
destructive_keywords = [
|
|
4668
|
+
r'unreal\.EditorAssetLibrary\.delete_asset',
|
|
4669
|
+
r'unreal\.EditorLevelLibrary\.destroy_actor',
|
|
4670
|
+
r'unreal\.save_package',
|
|
4671
|
+
r'os\.remove',
|
|
4672
|
+
r'shutil\.rmtree',
|
|
4673
|
+
r'file\.write',
|
|
4674
|
+
r'unreal\.EditorAssetLibrary\.save_asset'
|
|
4675
|
+
]
|
|
4676
|
+
for keyword in destructive_keywords:
|
|
4677
|
+
if re.search(keyword, script, re.IGNORECASE):
|
|
4678
|
+
return True
|
|
4679
|
+
return False
|
|
4680
|
+
|
|
4681
|
+
if __name__ == "__main__":
|
|
4682
|
+
import traceback
|
|
4683
|
+
try:
|
|
4684
|
+
print("Server starting...", file=sys.stderr)
|
|
4685
|
+
mcp.run("stdio")
|
|
4686
|
+
except Exception as e:
|
|
4687
|
+
print(f"Server crashed with error: {e}", file=sys.stderr)
|
|
4688
|
+
traceback.print_exc(file=sys.stderr)
|
|
4689
|
+
raise
|
|
4690
|
+
|
|
4691
|
+
# Safety check for potentially destructive actions
|
|
4692
|
+
def is_potentially_destructive(script: str) -> bool:
|
|
4693
|
+
"""
|
|
4694
|
+
Check if the script contains potentially destructive actions like deleting or saving files.
|
|
4695
|
+
Returns True if such actions are detected and not explicitly requested.
|
|
4696
|
+
"""
|
|
4697
|
+
destructive_keywords = [
|
|
4698
|
+
r'unreal\.EditorAssetLibrary\.delete_asset',
|
|
4699
|
+
r'unreal\.EditorLevelLibrary\.destroy_actor',
|
|
4700
|
+
r'unreal\.save_package',
|
|
4701
|
+
r'os\.remove',
|
|
4702
|
+
r'shutil\.rmtree',
|
|
4703
|
+
r'file\.write',
|
|
4704
|
+
r'unreal\.EditorAssetLibrary\.save_asset'
|
|
4705
|
+
]
|
|
4706
|
+
|
|
4707
|
+
for keyword in destructive_keywords:
|
|
4708
|
+
if re.search(keyword, script, re.IGNORECASE):
|
|
4709
|
+
return True
|
|
4710
|
+
return False
|
|
4711
|
+
|
|
4712
|
+
if __name__ == "__main__":
|
|
4713
|
+
import traceback
|
|
4714
|
+
try:
|
|
4715
|
+
print("Server starting...", file=sys.stderr)
|
|
4716
|
+
mcp.run("stdio")
|
|
4717
|
+
except Exception as e:
|
|
4718
|
+
print(f"Server crashed with error: {e}", file=sys.stderr)
|
|
4719
|
+
traceback.print_exc(file=sys.stderr)
|
|
4720
|
+
raise
|