barlowski commited on
Commit
5ccfb25
ยท
verified ยท
1 Parent(s): b534ef6

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes. ย  See raw diff
Files changed (50) hide show
  1. .gitattributes +9 -0
  2. README.md +3 -3
  3. UMCP.it-Unreal-Organizer-Assistant/.gitignore +43 -0
  4. UMCP.it-Unreal-Organizer-Assistant/Config/DefaultGenerativeAISupport.ini +3 -0
  5. UMCP.it-Unreal-Organizer-Assistant/Config/FilterPlugin.ini +8 -0
  6. UMCP.it-Unreal-Organizer-Assistant/Content/ExampleBlueprints/ChatAPIExamples.uasset +3 -0
  7. UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/__init__.py +1 -0
  8. UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/__init__.py +1 -0
  9. UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/auto_wiring.py +438 -0
  10. UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/connection_validator.py +351 -0
  11. UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/pin_discovery.py +362 -0
  12. UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/utils/connection_rules.py +225 -0
  13. UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/utils/node_helpers.py +273 -0
  14. UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/utils/pin_types.py +152 -0
  15. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/__init__.py +1 -0
  16. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/actor_commands.py +332 -0
  17. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/base_handler.py +23 -0
  18. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/basic_commands.py +487 -0
  19. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/blueprint_commands.py +874 -0
  20. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/blueprint_connection_commands.py +338 -0
  21. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/organization_commands.py +646 -0
  22. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/physics_commands.py +262 -0
  23. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/physics_commands_backup.py +303 -0
  24. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/physics_commands_simple.py +262 -0
  25. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/python_commands.py +278 -0
  26. UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/ui_commands.py +181 -0
  27. UMCP.it-Unreal-Organizer-Assistant/Content/Python/init_unreal.py +143 -0
  28. UMCP.it-Unreal-Organizer-Assistant/Content/Python/knowledge_base/how_to_use.md +289 -0
  29. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mcp_server.py +1911 -0
  30. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/__init__.py +27 -0
  31. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/__main__.py +83 -0
  32. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/base.py +261 -0
  33. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/darwin.py +217 -0
  34. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/exception.py +15 -0
  35. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/factory.py +40 -0
  36. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/linux.py +481 -0
  37. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/models.py +23 -0
  38. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/py.typed +0 -0
  39. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/screenshot.py +125 -0
  40. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/tools.py +65 -0
  41. UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/windows.py +250 -0
  42. UMCP.it-Unreal-Organizer-Assistant/Content/Python/socket_server_config.json +3 -0
  43. UMCP.it-Unreal-Organizer-Assistant/Content/Python/unreal_socket_server.py +304 -0
  44. UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/__init__.py +1 -0
  45. UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/asset_validation.py +232 -0
  46. UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/logging.py +70 -0
  47. UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/unreal_conversions.py +54 -0
  48. UMCP.it-Unreal-Organizer-Assistant/Content/unreal_server_init.py +1357 -0
  49. UMCP.it-Unreal-Organizer-Assistant/Docs/BHDemoGif.gif +3 -0
  50. UMCP.it-Unreal-Organizer-Assistant/Docs/BLUEPRINT_CONNECTIONS_INTEGRATED.md +330 -0
.gitattributes CHANGED
@@ -33,3 +33,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ UMCP.it-Unreal-Organizer-Assistant/Content/ExampleBlueprints/ChatAPIExamples.uasset filter=lfs diff=lfs merge=lfs -text
37
+ UMCP.it-Unreal-Organizer-Assistant/Docs/BHDemoGif.gif filter=lfs diff=lfs merge=lfs -text
38
+ UMCP.it-Unreal-Organizer-Assistant/Docs/BpExampleClaudeChat.png filter=lfs diff=lfs merge=lfs -text
39
+ UMCP.it-Unreal-Organizer-Assistant/Docs/BpExampleDeepseekChat.png filter=lfs diff=lfs merge=lfs -text
40
+ UMCP.it-Unreal-Organizer-Assistant/Docs/BpExampleOAIChat.png filter=lfs diff=lfs merge=lfs -text
41
+ UMCP.it-Unreal-Organizer-Assistant/Docs/BpExampleOAIStructuredOp.png filter=lfs diff=lfs merge=lfs -text
42
+ UMCP.it-Unreal-Organizer-Assistant/Docs/Repo[[:space:]]Card[[:space:]]-[[:space:]]Updated.png filter=lfs diff=lfs merge=lfs -text
43
+ UMCP.it-Unreal-Organizer-Assistant/Docs/UnrealMcpDemo.gif filter=lfs diff=lfs merge=lfs -text
44
+ kariana.ai/.archive/temp-files/2025.findings-acl.994-1_compressed-1.pdf filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -101,9 +101,9 @@ Get the KarianaUMCP plugin for your Unreal Engine project:
101
 
102
  | Platform | Download |
103
  |----------|----------|
104
- | **Windows** | [KarianaUMCP-v1.0.1-windows.zip](https://github.com/kerinzeebart/MCPTest-with-UMCP.it/releases/download/v1.0.1/KarianaUMCP-v1.0.1-windows.zip) |
105
- | **macOS** | [KarianaUMCP-v1.0.1-macos.zip](https://github.com/kerinzeebart/MCPTest-with-UMCP.it/releases/download/v1.0.1/KarianaUMCP-v1.0.1-macos.zip) |
106
- | **Linux** | [KarianaUMCP-v1.0.1-linux.zip](https://github.com/kerinzeebart/MCPTest-with-UMCP.it/releases/download/v1.0.1/KarianaUMCP-v1.0.1-linux.zip) |
107
 
108
  ## Claude Desktop Connection
109
 
 
101
 
102
  | Platform | Download |
103
  |----------|----------|
104
+ | **Windows** | [Dropbox](https://www.dropbox.com/scl/fi/y4zfyfkadmik2sma5jkwp/KarianaUMCP-v1.0.1-windows.zip?rlkey=u0wk39o1av7mnflcnhfu7cij1&st=nvuj5kaj&dl=0) |
105
+ | **macOS** | [Dropbox](https://www.dropbox.com/scl/fi/gydzrcjujtf14c743s07m/KarianaUMCP-v1.0.1-macos.zip?rlkey=239u0ltp17s665nzg08pkatzs&st=5hs5wokz&dl=0) |
106
+ | **Linux** | [Dropbox](https://www.dropbox.com/scl/fi/za196pymwr8l0wgamdact/KarianaUMCP-v1.0.1-linux.zip?rlkey=eq37irxfif6hlahkxtdygjiqo&st=j5q2p9yh&dl=0) |
107
 
108
  ## Claude Desktop Connection
109
 
UMCP.it-Unreal-Organizer-Assistant/.gitignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Unreal Engine build artifacts
2
+ Binaries/
3
+ Intermediate/
4
+ Saved/
5
+ Build/
6
+
7
+ # IDE files
8
+ .vs/
9
+ .vscode/
10
+ *.sln
11
+ *.sdf
12
+ *.opensdf
13
+ *.suo
14
+ *.xcodeproj
15
+ *.xcworkspace
16
+
17
+ # Python
18
+ __pycache__/
19
+ *.py[cod]
20
+ *$py.class
21
+ *.so
22
+ .Python
23
+ *.egg-info/
24
+ dist/
25
+ build/
26
+
27
+ # MCP Server PID files
28
+ .unrealgenai/
29
+
30
+ # Test/Development files
31
+ test_*.py
32
+ quick_test.py
33
+ reload_socket_server.py
34
+ kill_and_restart_server.py
35
+ stop_all_servers.py
36
+
37
+ # OS files
38
+ .DS_Store
39
+ Thumbs.db
40
+ desktop.ini
41
+
42
+ # Logs
43
+ *.log
UMCP.it-Unreal-Organizer-Assistant/Config/DefaultGenerativeAISupport.ini ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ ๏ปฟ[CoreRedirects]
2
+ +ClassRedirects=(OldName="/Script/GenerativeAISupport.FBhJsonHelper",NewName="/Script/GenerativeAISupport.BhJsonHelper")
3
+ +FunctionRedirects=(OldName="/Script/GenerativeAISupport.GenOAIChat.SendRequestLatent",NewName="/Script/GenerativeAISupport.GenOAIChat.RequestOpenAIChat")
UMCP.it-Unreal-Organizer-Assistant/Config/FilterPlugin.ini ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ [FilterPlugin]
2
+ ; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
3
+ ; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
4
+ ;
5
+ ; Examples:
6
+ ; /README.txt
7
+ ; /Extras/...
8
+ ; /Binaries/ThirdParty/*.dll
UMCP.it-Unreal-Organizer-Assistant/Content/ExampleBlueprints/ChatAPIExamples.uasset ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d2bd2cb0b9f274ada76faba0255b36f149197b01229bf00e5714434b6abf2d8e
3
+ size 216398
UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Blueprint connections package
UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Handlers package
UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/auto_wiring.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Auto-Wiring Service for Unreal Engine Blueprints
3
+
4
+ This service intelligently connects chains of Blueprint nodes using
5
+ type matching and name similarity scoring.
6
+ """
7
+
8
+ import json
9
+ from typing import List, Dict, Optional, Tuple
10
+ import sys
11
+ import os
12
+
13
+ # Add parent directory to path for imports
14
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
15
+
16
+ from utils.connection_rules import are_types_compatible, calculate_pin_name_similarity
17
+
18
+
19
+ class AutoWiringService:
20
+ """Intelligently connects chains of Blueprint nodes"""
21
+
22
+ def __init__(self, pin_discovery_service, connection_validator):
23
+ """
24
+ Initialize the auto-wiring service.
25
+
26
+ Args:
27
+ pin_discovery_service: Instance of PinDiscoveryService
28
+ connection_validator: Instance of ConnectionValidator
29
+ """
30
+ self.pin_discovery = pin_discovery_service
31
+ self.validator = connection_validator
32
+ self.min_confidence = 0.3 # Minimum confidence score for connections
33
+
34
+ def auto_connect_chain(
35
+ self,
36
+ blueprint_path: str,
37
+ function_id: str,
38
+ node_chain: List[str],
39
+ validate_before_connect: bool = True
40
+ ) -> Dict:
41
+ """
42
+ Automatically connect a chain of nodes.
43
+
44
+ Args:
45
+ blueprint_path: Path to Blueprint asset
46
+ function_id: GUID of the function graph
47
+ node_chain: List of node GUIDs in execution order
48
+ validate_before_connect: Whether to validate connections first
49
+
50
+ Returns:
51
+ {
52
+ "success": bool,
53
+ "connections_made": int,
54
+ "connections": List[Dict],
55
+ "failed_connections": List[Dict],
56
+ "warnings": List[str]
57
+ }
58
+ """
59
+ if len(node_chain) < 2:
60
+ return {
61
+ "success": False,
62
+ "error": "Need at least 2 nodes to create a chain",
63
+ "connections_made": 0
64
+ }
65
+
66
+ connections_made = []
67
+ failed_connections = []
68
+ warnings = []
69
+
70
+ # Connect each pair of adjacent nodes
71
+ for i in range(len(node_chain) - 1):
72
+ source_node = node_chain[i]
73
+ target_node = node_chain[i + 1]
74
+
75
+ # Get node information
76
+ source_info = self.pin_discovery.get_node_pins(
77
+ blueprint_path, function_id, source_node
78
+ )
79
+ target_info = self.pin_discovery.get_node_pins(
80
+ blueprint_path, function_id, target_node
81
+ )
82
+
83
+ if not source_info.get("success") or not target_info.get("success"):
84
+ failed_connections.append({
85
+ "source": source_node,
86
+ "target": target_node,
87
+ "error": "Failed to get node information",
88
+ "source_error": source_info.get("error") if not source_info.get("success") else None,
89
+ "target_error": target_info.get("error") if not target_info.get("success") else None
90
+ })
91
+ continue
92
+
93
+ # Connect execution pins first
94
+ exec_result = self._connect_execution_pins(
95
+ blueprint_path, function_id,
96
+ source_node, target_node,
97
+ source_info, target_info
98
+ )
99
+
100
+ if exec_result:
101
+ if validate_before_connect:
102
+ validation = self.validator.validate_connection(
103
+ blueprint_path, function_id,
104
+ source_node, exec_result["source_pin"],
105
+ target_node, exec_result["target_pin"]
106
+ )
107
+ if validation.get("valid"):
108
+ connections_made.append(exec_result)
109
+ else:
110
+ failed_connections.append({
111
+ **exec_result,
112
+ "validation_error": validation.get("message")
113
+ })
114
+ else:
115
+ connections_made.append(exec_result)
116
+
117
+ # Connect data pins
118
+ data_results = self._connect_data_pins(
119
+ blueprint_path, function_id,
120
+ source_node, target_node,
121
+ source_info, target_info,
122
+ validate_before_connect
123
+ )
124
+
125
+ connections_made.extend(data_results["connected"])
126
+ failed_connections.extend(data_results["failed"])
127
+ warnings.extend(data_results["warnings"])
128
+
129
+ return {
130
+ "success": len(connections_made) > 0,
131
+ "connections_made": len(connections_made),
132
+ "connections": connections_made,
133
+ "failed_connections": failed_connections,
134
+ "warnings": warnings,
135
+ "summary": {
136
+ "total_attempted": len(connections_made) + len(failed_connections),
137
+ "successful": len(connections_made),
138
+ "failed": len(failed_connections)
139
+ }
140
+ }
141
+
142
+ def _connect_execution_pins(
143
+ self,
144
+ blueprint_path: str,
145
+ function_id: str,
146
+ source_node: str,
147
+ target_node: str,
148
+ source_info: Dict,
149
+ target_info: Dict
150
+ ) -> Optional[Dict]:
151
+ """Connect execution flow pins between two nodes"""
152
+ # Find execution output pin
153
+ exec_output = None
154
+ for pin in source_info.get("output_pins", []):
155
+ if pin["category"] == "exec":
156
+ exec_output = pin
157
+ break
158
+
159
+ # Find execution input pin
160
+ exec_input = None
161
+ for pin in target_info.get("input_pins", []):
162
+ if pin["category"] == "exec":
163
+ exec_input = pin
164
+ break
165
+
166
+ if exec_output and exec_input:
167
+ return {
168
+ "source_node": source_node,
169
+ "source_pin": exec_output["name"],
170
+ "target_node": target_node,
171
+ "target_pin": exec_input["name"],
172
+ "type": "execution",
173
+ "confidence": 1.0,
174
+ "auto_detected": True
175
+ }
176
+
177
+ return None
178
+
179
+ def _connect_data_pins(
180
+ self,
181
+ blueprint_path: str,
182
+ function_id: str,
183
+ source_node: str,
184
+ target_node: str,
185
+ source_info: Dict,
186
+ target_info: Dict,
187
+ validate: bool
188
+ ) -> Dict:
189
+ """Connect data pins between two nodes using intelligent matching"""
190
+ connections = []
191
+ failed = []
192
+ warnings = []
193
+
194
+ output_pins = source_info.get("output_pins", [])
195
+ input_pins = target_info.get("input_pins", [])
196
+
197
+ # Remove execution pins
198
+ output_pins = [p for p in output_pins if p["category"] != "exec"]
199
+ input_pins = [p for p in input_pins if p["category"] != "exec"]
200
+
201
+ # Track which input pins have been matched
202
+ matched_input_pins = set()
203
+
204
+ # Match pins by type and name similarity
205
+ for output_pin in output_pins:
206
+ best_match = self._find_best_pin_match(
207
+ output_pin,
208
+ [p for p in input_pins if p["name"] not in matched_input_pins]
209
+ )
210
+
211
+ if best_match and best_match["confidence"] >= self.min_confidence:
212
+ connection = {
213
+ "source_node": source_node,
214
+ "source_pin": output_pin["name"],
215
+ "target_node": target_node,
216
+ "target_pin": best_match["name"],
217
+ "type": "data",
218
+ "confidence": best_match["confidence"],
219
+ "auto_detected": True
220
+ }
221
+
222
+ # Validate if requested
223
+ if validate:
224
+ validation = self.validator.validate_connection(
225
+ blueprint_path, function_id,
226
+ source_node, output_pin["name"],
227
+ target_node, best_match["name"]
228
+ )
229
+
230
+ if validation.get("valid"):
231
+ connections.append(connection)
232
+ matched_input_pins.add(best_match["name"])
233
+
234
+ # Add any warnings from validation
235
+ if validation.get("warnings"):
236
+ warnings.extend(validation["warnings"])
237
+ else:
238
+ failed.append({
239
+ **connection,
240
+ "validation_error": validation.get("message"),
241
+ "suggestions": validation.get("suggestions", [])
242
+ })
243
+ else:
244
+ connections.append(connection)
245
+ matched_input_pins.add(best_match["name"])
246
+
247
+ return {
248
+ "connected": connections,
249
+ "failed": failed,
250
+ "warnings": warnings
251
+ }
252
+
253
+ def _find_best_pin_match(
254
+ self,
255
+ output_pin: Dict,
256
+ input_pins: List[Dict]
257
+ ) -> Optional[Dict]:
258
+ """
259
+ Find the best matching input pin for an output pin.
260
+
261
+ Uses a scoring algorithm based on:
262
+ - Type compatibility (required)
263
+ - Name similarity
264
+ - Common naming patterns
265
+ - Type exact match bonus
266
+
267
+ Args:
268
+ output_pin: Output pin to match
269
+ input_pins: Available input pins
270
+
271
+ Returns:
272
+ Best match with confidence score, or None
273
+ """
274
+ best_match = None
275
+ best_score = 0.0
276
+
277
+ for input_pin in input_pins:
278
+ # Skip if already connected
279
+ if input_pin.get("is_connected"):
280
+ continue
281
+
282
+ # Check type compatibility (required)
283
+ if not are_types_compatible(
284
+ output_pin["category"],
285
+ input_pin["category"]
286
+ ):
287
+ continue
288
+
289
+ # Calculate base name similarity score
290
+ name_score = calculate_pin_name_similarity(
291
+ output_pin["name"],
292
+ input_pin["name"]
293
+ )
294
+
295
+ # Apply bonuses for common patterns
296
+ score = name_score
297
+
298
+ # Boost for "ReturnValue" โ†’ generic input
299
+ if "return" in output_pin["name"].lower():
300
+ if any(term in input_pin["name"].lower() for term in ["value", "input", "a", "in"]):
301
+ score += 0.3
302
+
303
+ # Boost for "Result" โ†’ generic input
304
+ if "result" in output_pin["name"].lower():
305
+ if any(term in input_pin["name"].lower() for term in ["value", "input"]):
306
+ score += 0.2
307
+
308
+ # Boost for exact type match
309
+ if output_pin["category"] == input_pin["category"]:
310
+ score += 0.2
311
+
312
+ # Boost for array match
313
+ if output_pin["is_array"] == input_pin["is_array"]:
314
+ score += 0.1
315
+
316
+ # Update best match
317
+ if score > best_score:
318
+ best_score = score
319
+ best_match = {
320
+ "name": input_pin["name"],
321
+ "pin": input_pin,
322
+ "confidence": min(score, 1.0) # Cap at 1.0
323
+ }
324
+
325
+ return best_match
326
+
327
+ def suggest_connections(
328
+ self,
329
+ blueprint_path: str,
330
+ function_id: str,
331
+ source_node: str,
332
+ target_node: str
333
+ ) -> Dict:
334
+ """
335
+ Suggest possible connections between two nodes without making them.
336
+
337
+ Args:
338
+ blueprint_path: Path to Blueprint asset
339
+ function_id: GUID of the function graph
340
+ source_node: GUID of source node
341
+ target_node: GUID of target node
342
+
343
+ Returns:
344
+ {
345
+ "success": bool,
346
+ "execution_connection": Optional[Dict],
347
+ "data_connections": List[Dict],
348
+ "all_possibilities": List[Dict]
349
+ }
350
+ """
351
+ # Get node information
352
+ source_info = self.pin_discovery.get_node_pins(
353
+ blueprint_path, function_id, source_node
354
+ )
355
+ target_info = self.pin_discovery.get_node_pins(
356
+ blueprint_path, function_id, target_node
357
+ )
358
+
359
+ if not source_info.get("success") or not target_info.get("success"):
360
+ return {
361
+ "success": False,
362
+ "error": "Failed to get node information"
363
+ }
364
+
365
+ # Find execution connection
366
+ exec_conn = self._connect_execution_pins(
367
+ blueprint_path, function_id,
368
+ source_node, target_node,
369
+ source_info, target_info
370
+ )
371
+
372
+ # Find data connections
373
+ data_result = self._connect_data_pins(
374
+ blueprint_path, function_id,
375
+ source_node, target_node,
376
+ source_info, target_info,
377
+ validate=False
378
+ )
379
+
380
+ # Generate all possible connections (even low confidence)
381
+ all_possibilities = []
382
+ output_pins = [p for p in source_info.get("output_pins", []) if p["category"] != "exec"]
383
+ input_pins = [p for p in target_info.get("input_pins", []) if p["category"] != "exec"]
384
+
385
+ for output_pin in output_pins:
386
+ for input_pin in input_pins:
387
+ if are_types_compatible(output_pin["category"], input_pin["category"]):
388
+ confidence = calculate_pin_name_similarity(output_pin["name"], input_pin["name"])
389
+ all_possibilities.append({
390
+ "source_pin": output_pin["name"],
391
+ "target_pin": input_pin["name"],
392
+ "confidence": confidence,
393
+ "source_type": output_pin["category"],
394
+ "target_type": input_pin["category"]
395
+ })
396
+
397
+ # Sort by confidence
398
+ all_possibilities.sort(key=lambda x: x["confidence"], reverse=True)
399
+
400
+ return {
401
+ "success": True,
402
+ "execution_connection": exec_conn,
403
+ "data_connections": data_result["connected"],
404
+ "all_possibilities": all_possibilities
405
+ }
406
+
407
+
408
+ # Standalone function for use as MCP tool
409
+ def auto_wire_blueprint_nodes_tool(
410
+ blueprint_path: str,
411
+ function_id: str,
412
+ node_chain: List[str]
413
+ ) -> str:
414
+ """
415
+ MCP tool function to auto-wire Blueprint nodes.
416
+
417
+ Returns JSON string for MCP protocol.
418
+ """
419
+ from handlers.pin_discovery import PinDiscoveryService
420
+ from handlers.connection_validator import ConnectionValidator
421
+
422
+ pin_discovery = PinDiscoveryService()
423
+ validator = ConnectionValidator(pin_discovery)
424
+ auto_wiring = AutoWiringService(pin_discovery, validator)
425
+
426
+ result = auto_wiring.auto_connect_chain(
427
+ blueprint_path,
428
+ function_id,
429
+ node_chain
430
+ )
431
+
432
+ return json.dumps(result, indent=2)
433
+
434
+
435
+ # Testing
436
+ if __name__ == "__main__":
437
+ print("Auto-Wiring Service ready")
438
+ print("Usage: Import and use AutoWiringService class")
UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/connection_validator.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Connection Validator for Unreal Engine Blueprints
3
+
4
+ This service validates Blueprint pin connections before execution,
5
+ providing detailed error messages and suggestions.
6
+ """
7
+
8
+ import json
9
+ from typing import Dict, List, Optional, Tuple
10
+ import sys
11
+ import os
12
+
13
+ # Add parent directory to path for imports
14
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
15
+
16
+ from utils.pin_types import PinInfo, ValidationError
17
+ from utils.connection_rules import (
18
+ are_types_compatible,
19
+ get_conversion_suggestion,
20
+ validate_array_compatibility,
21
+ validate_struct_compatibility,
22
+ validate_object_compatibility,
23
+ calculate_pin_name_similarity
24
+ )
25
+
26
+
27
+ class ConnectionValidator:
28
+ """Validates Blueprint pin connections before execution"""
29
+
30
+ def __init__(self, pin_discovery_service):
31
+ """
32
+ Initialize the validator with a pin discovery service.
33
+
34
+ Args:
35
+ pin_discovery_service: Instance of PinDiscoveryService
36
+ """
37
+ self.pin_discovery = pin_discovery_service
38
+
39
+ def validate_connection(
40
+ self,
41
+ blueprint_path: str,
42
+ function_id: str,
43
+ source_node_id: str,
44
+ source_pin_name: str,
45
+ target_node_id: str,
46
+ target_pin_name: str
47
+ ) -> Dict:
48
+ """
49
+ Validate a connection before attempting it.
50
+
51
+ Args:
52
+ blueprint_path: Path to Blueprint asset
53
+ function_id: GUID of the function graph
54
+ source_node_id: GUID of source node
55
+ source_pin_name: Name of output pin
56
+ target_node_id: GUID of target node
57
+ target_pin_name: Name of input pin
58
+
59
+ Returns:
60
+ {
61
+ "valid": bool,
62
+ "error_type": Optional[str],
63
+ "message": str,
64
+ "warnings": List[str],
65
+ "suggestions": List[str],
66
+ "source_pin": Optional[Dict],
67
+ "target_pin": Optional[Dict],
68
+ "available_source_pins": Optional[List[str]],
69
+ "available_target_pins": Optional[List[str]]
70
+ }
71
+ """
72
+ # Get pin information for both nodes
73
+ source_pins = self.pin_discovery.get_node_pins(
74
+ blueprint_path, function_id, source_node_id
75
+ )
76
+
77
+ target_pins = self.pin_discovery.get_node_pins(
78
+ blueprint_path, function_id, target_node_id
79
+ )
80
+
81
+ # Check if pin discovery succeeded
82
+ if not source_pins.get("success"):
83
+ return {
84
+ "valid": False,
85
+ "error_type": ValidationError.PIN_NOT_FOUND.value,
86
+ "message": f"Failed to retrieve source node information: {source_pins.get('error', 'Unknown error')}",
87
+ "warnings": [],
88
+ "suggestions": ["Verify source node GUID is correct", "Check if Blueprint is compiled"]
89
+ }
90
+
91
+ if not target_pins.get("success"):
92
+ return {
93
+ "valid": False,
94
+ "error_type": ValidationError.PIN_NOT_FOUND.value,
95
+ "message": f"Failed to retrieve target node information: {target_pins.get('error', 'Unknown error')}",
96
+ "warnings": [],
97
+ "suggestions": ["Verify target node GUID is correct", "Check if Blueprint is compiled"]
98
+ }
99
+
100
+ # Find specific pins
101
+ source_pin = self._find_pin_by_name(
102
+ source_pins.get("output_pins", []),
103
+ source_pin_name
104
+ )
105
+
106
+ target_pin = self._find_pin_by_name(
107
+ target_pins.get("input_pins", []),
108
+ target_pin_name
109
+ )
110
+
111
+ # Validate source pin exists
112
+ if not source_pin:
113
+ available_pins = [p["name"] for p in source_pins.get("output_pins", [])]
114
+ return {
115
+ "valid": False,
116
+ "error_type": ValidationError.PIN_NOT_FOUND.value,
117
+ "message": f"Source pin '{source_pin_name}' not found",
118
+ "warnings": [],
119
+ "suggestions": self._suggest_similar_pins(source_pin_name, source_pins.get("output_pins", [])),
120
+ "available_source_pins": available_pins
121
+ }
122
+
123
+ # Validate target pin exists
124
+ if not target_pin:
125
+ available_pins = [p["name"] for p in target_pins.get("input_pins", [])]
126
+ return {
127
+ "valid": False,
128
+ "error_type": ValidationError.PIN_NOT_FOUND.value,
129
+ "message": f"Target pin '{target_pin_name}' not found",
130
+ "warnings": [],
131
+ "suggestions": self._suggest_similar_pins(target_pin_name, target_pins.get("input_pins", [])),
132
+ "available_target_pins": available_pins
133
+ }
134
+
135
+ # Validate type compatibility
136
+ type_validation = self._validate_types(source_pin, target_pin)
137
+ if not type_validation["valid"]:
138
+ return type_validation
139
+
140
+ # Check array compatibility
141
+ array_validation = self._validate_array(source_pin, target_pin)
142
+ if not array_validation["valid"]:
143
+ return array_validation
144
+
145
+ # Check if already connected (warning, not error)
146
+ warnings = []
147
+ if target_pin.get("is_connected"):
148
+ warnings.append(f"Target pin '{target_pin_name}' is already connected")
149
+ warnings.append("Use force=True to break existing connection")
150
+
151
+ # All validations passed
152
+ return {
153
+ "valid": True,
154
+ "message": "Connection is valid",
155
+ "warnings": warnings,
156
+ "suggestions": [],
157
+ "source_pin": source_pin,
158
+ "target_pin": target_pin
159
+ }
160
+
161
+ def _find_pin_by_name(self, pins: List[Dict], pin_name: str) -> Optional[Dict]:
162
+ """Find a pin by name in a list of pins"""
163
+ for pin in pins:
164
+ if pin["name"] == pin_name:
165
+ return pin
166
+ return None
167
+
168
+ def _validate_types(self, source_pin: Dict, target_pin: Dict) -> Dict:
169
+ """Validate type compatibility between two pins"""
170
+ source_type = source_pin["category"]
171
+ target_type = target_pin["category"]
172
+
173
+ # Check basic type compatibility
174
+ if not are_types_compatible(source_type, target_type):
175
+ return {
176
+ "valid": False,
177
+ "error_type": ValidationError.TYPE_MISMATCH.value,
178
+ "message": f"Type mismatch: Cannot connect '{source_type}' to '{target_type}'",
179
+ "suggestions": get_conversion_suggestion(source_type, target_type),
180
+ "source_pin": source_pin,
181
+ "target_pin": target_pin,
182
+ "warnings": []
183
+ }
184
+
185
+ # For structs, validate subcategory
186
+ if source_type == "struct":
187
+ is_valid, suggestions = validate_struct_compatibility(
188
+ source_pin.get("subcategory"),
189
+ target_pin.get("subcategory")
190
+ )
191
+ if not is_valid:
192
+ return {
193
+ "valid": False,
194
+ "error_type": ValidationError.STRUCT_MISMATCH.value,
195
+ "message": f"Struct type mismatch",
196
+ "suggestions": suggestions,
197
+ "source_pin": source_pin,
198
+ "target_pin": target_pin,
199
+ "warnings": []
200
+ }
201
+
202
+ # For objects, validate object type
203
+ if source_type == "object":
204
+ is_valid, warnings = validate_object_compatibility(
205
+ source_pin.get("object_type"),
206
+ target_pin.get("object_type")
207
+ )
208
+ # Object validation returns warnings, not hard errors
209
+ if warnings:
210
+ return {
211
+ "valid": True,
212
+ "message": "Connection is valid with warnings",
213
+ "warnings": warnings,
214
+ "suggestions": [],
215
+ "source_pin": source_pin,
216
+ "target_pin": target_pin
217
+ }
218
+
219
+ return {"valid": True}
220
+
221
+ def _validate_array(self, source_pin: Dict, target_pin: Dict) -> Dict:
222
+ """Validate array/non-array compatibility"""
223
+ is_valid, suggestions = validate_array_compatibility(
224
+ source_pin["is_array"],
225
+ target_pin["is_array"]
226
+ )
227
+
228
+ if not is_valid:
229
+ return {
230
+ "valid": False,
231
+ "error_type": ValidationError.INCOMPATIBLE_ARRAY.value,
232
+ "message": "Array/non-array mismatch",
233
+ "suggestions": suggestions,
234
+ "source_pin": source_pin,
235
+ "target_pin": target_pin,
236
+ "warnings": []
237
+ }
238
+
239
+ return {"valid": True}
240
+
241
+ def _suggest_similar_pins(self, pin_name: str, available_pins: List[Dict]) -> List[str]:
242
+ """Suggest similar pin names based on string similarity"""
243
+ import difflib
244
+
245
+ pin_names = [p["name"] for p in available_pins]
246
+ similar = difflib.get_close_matches(pin_name, pin_names, n=3, cutoff=0.6)
247
+
248
+ suggestions = []
249
+ if similar:
250
+ suggestions = [f"Did you mean '{name}'?" for name in similar]
251
+ else:
252
+ # Show first 5 available pins
253
+ if pin_names:
254
+ suggestions.append(f"Available pins: {', '.join(pin_names[:5])}")
255
+ else:
256
+ suggestions.append("No pins available")
257
+
258
+ return suggestions
259
+
260
+ def validate_bulk_connections(
261
+ self,
262
+ blueprint_path: str,
263
+ function_id: str,
264
+ connections: List[Dict]
265
+ ) -> Dict:
266
+ """
267
+ Validate multiple connections at once.
268
+
269
+ Args:
270
+ blueprint_path: Path to Blueprint asset
271
+ function_id: GUID of the function graph
272
+ connections: List of connection requests
273
+
274
+ Returns:
275
+ {
276
+ "all_valid": bool,
277
+ "results": List[Dict],
278
+ "summary": {
279
+ "total": int,
280
+ "valid": int,
281
+ "invalid": int
282
+ }
283
+ }
284
+ """
285
+ results = []
286
+ valid_count = 0
287
+
288
+ for conn in connections:
289
+ result = self.validate_connection(
290
+ blueprint_path,
291
+ function_id,
292
+ conn.get("source_node_id"),
293
+ conn.get("source_pin"),
294
+ conn.get("target_node_id"),
295
+ conn.get("target_pin")
296
+ )
297
+
298
+ results.append({
299
+ "connection": conn,
300
+ "validation": result
301
+ })
302
+
303
+ if result.get("valid"):
304
+ valid_count += 1
305
+
306
+ return {
307
+ "all_valid": valid_count == len(connections),
308
+ "results": results,
309
+ "summary": {
310
+ "total": len(connections),
311
+ "valid": valid_count,
312
+ "invalid": len(connections) - valid_count
313
+ }
314
+ }
315
+
316
+
317
+ # Standalone function for use as MCP tool
318
+ def validate_blueprint_connection_tool(
319
+ blueprint_path: str,
320
+ function_id: str,
321
+ source_node_id: str,
322
+ source_pin: str,
323
+ target_node_id: str,
324
+ target_pin: str
325
+ ) -> str:
326
+ """
327
+ MCP tool function to validate a Blueprint connection.
328
+
329
+ Returns JSON string for MCP protocol.
330
+ """
331
+ from handlers.pin_discovery import PinDiscoveryService
332
+
333
+ pin_discovery = PinDiscoveryService()
334
+ validator = ConnectionValidator(pin_discovery)
335
+
336
+ result = validator.validate_connection(
337
+ blueprint_path,
338
+ function_id,
339
+ source_node_id,
340
+ source_pin,
341
+ target_node_id,
342
+ target_pin
343
+ )
344
+
345
+ return json.dumps(result, indent=2)
346
+
347
+
348
+ # Testing
349
+ if __name__ == "__main__":
350
+ print("Connection Validator ready")
351
+ print("Usage: Import and use ConnectionValidator class with PinDiscoveryService")
UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/pin_discovery.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pin Discovery Service for Unreal Engine Blueprints
3
+
4
+ This service discovers and analyzes Blueprint pin information using the
5
+ Unreal Engine Python API.
6
+
7
+ NOTE: This code runs inside Unreal Engine's Python interpreter.
8
+ """
9
+
10
+ import json
11
+ from typing import Dict, List, Optional
12
+ import sys
13
+
14
+ # Import Unreal Engine module (only available when running inside UE)
15
+ try:
16
+ import unreal
17
+ UNREAL_AVAILABLE = True
18
+ except ImportError:
19
+ UNREAL_AVAILABLE = False
20
+ print("Warning: unreal module not available. Running in test mode.")
21
+
22
+
23
+ class PinDiscoveryService:
24
+ """Service for discovering and analyzing Blueprint pins"""
25
+
26
+ def __init__(self):
27
+ self.pin_cache = {} # Cache for performance
28
+ self.cache_enabled = True
29
+
30
+ def clear_cache(self):
31
+ """Clear the pin information cache"""
32
+ self.pin_cache = {}
33
+
34
+ def get_node_pins(
35
+ self,
36
+ blueprint_path: str,
37
+ function_id: str,
38
+ node_id: str,
39
+ use_cache: bool = True
40
+ ) -> Dict:
41
+ """
42
+ Retrieve all pins for a specific node.
43
+
44
+ Args:
45
+ blueprint_path: Path to Blueprint asset (e.g., "/Game/BP_Test")
46
+ function_id: GUID of the function graph
47
+ node_id: GUID of the node
48
+ use_cache: Whether to use cached results
49
+
50
+ Returns:
51
+ {
52
+ "success": bool,
53
+ "node_info": {
54
+ "guid": str,
55
+ "type": str,
56
+ "name": str,
57
+ "position": [int, int],
58
+ "is_pure": bool
59
+ },
60
+ "input_pins": [...],
61
+ "output_pins": [...],
62
+ "error": Optional[str]
63
+ }
64
+ """
65
+ if not UNREAL_AVAILABLE:
66
+ return {
67
+ "success": False,
68
+ "error": "Unreal Engine Python API not available"
69
+ }
70
+
71
+ cache_key = f"{blueprint_path}:{function_id}:{node_id}"
72
+
73
+ # Check cache
74
+ if use_cache and self.cache_enabled and cache_key in self.pin_cache:
75
+ return self.pin_cache[cache_key]
76
+
77
+ try:
78
+ # Load Blueprint
79
+ blueprint = unreal.EditorAssetLibrary.load_asset(blueprint_path)
80
+ if not blueprint:
81
+ return {
82
+ "success": False,
83
+ "error": f"Blueprint not found: {blueprint_path}"
84
+ }
85
+
86
+ # Compile if needed
87
+ self._ensure_compiled(blueprint)
88
+
89
+ # Find the graph
90
+ graph = self._find_graph_by_id(blueprint, function_id)
91
+ if not graph:
92
+ return {
93
+ "success": False,
94
+ "error": f"Graph not found: {function_id}",
95
+ "available_graphs": self._list_available_graphs(blueprint)
96
+ }
97
+
98
+ # Find the node
99
+ node = self._find_node_by_guid(graph, node_id)
100
+ if not node:
101
+ return {
102
+ "success": False,
103
+ "error": f"Node not found: {node_id}",
104
+ "available_nodes": self._list_available_nodes(graph)
105
+ }
106
+
107
+ # Extract pin information
108
+ result = {
109
+ "success": True,
110
+ "node_info": self._extract_node_info(node),
111
+ "input_pins": self._extract_pins(node, "EGPD_Input"),
112
+ "output_pins": self._extract_pins(node, "EGPD_Output")
113
+ }
114
+
115
+ # Cache result
116
+ if self.cache_enabled:
117
+ self.pin_cache[cache_key] = result
118
+
119
+ return result
120
+
121
+ except Exception as e:
122
+ return {
123
+ "success": False,
124
+ "error": str(e),
125
+ "error_type": type(e).__name__
126
+ }
127
+
128
+ def _ensure_compiled(self, blueprint):
129
+ """Ensure Blueprint is compiled before accessing pins"""
130
+ try:
131
+ if hasattr(unreal, 'KismetSystemLibrary'):
132
+ unreal.KismetSystemLibrary.compile_blueprint(blueprint)
133
+ except Exception as e:
134
+ print(f"Warning: Could not compile Blueprint: {e}")
135
+
136
+ def _find_graph_by_id(self, blueprint, function_id: str):
137
+ """Find a graph in the blueprint by its GUID"""
138
+ # Check UberGraph pages (event graphs)
139
+ ubergraph_pages = blueprint.get_editor_property('ubergraph_pages')
140
+ if ubergraph_pages:
141
+ for graph in ubergraph_pages:
142
+ if str(graph.graph_guid) == function_id:
143
+ return graph
144
+
145
+ # Check function graphs
146
+ function_graphs = blueprint.get_editor_property('function_graphs')
147
+ if function_graphs:
148
+ for graph in function_graphs:
149
+ if str(graph.graph_guid) == function_id:
150
+ return graph
151
+
152
+ return None
153
+
154
+ def _find_node_by_guid(self, graph, node_guid: str):
155
+ """Find a node in the graph by its GUID"""
156
+ nodes = graph.get_editor_property('nodes')
157
+ if not nodes:
158
+ return None
159
+
160
+ for node in nodes:
161
+ if str(node.node_guid) == node_guid:
162
+ return node
163
+
164
+ return None
165
+
166
+ def _extract_node_info(self, node) -> Dict:
167
+ """Extract basic node information"""
168
+ return {
169
+ "guid": str(node.node_guid),
170
+ "type": type(node).__name__,
171
+ "name": node.get_name() if hasattr(node, 'get_name') else "Unknown",
172
+ "position": [
173
+ node.node_pos_x if hasattr(node, 'node_pos_x') else 0,
174
+ node.node_pos_y if hasattr(node, 'node_pos_y') else 0
175
+ ],
176
+ "is_pure": self._is_pure_node(node)
177
+ }
178
+
179
+ def _is_pure_node(self, node) -> bool:
180
+ """Determine if a node is pure (no execution pins)"""
181
+ # Check if node has is_pure property
182
+ if hasattr(node, 'is_pure_func'):
183
+ return node.is_pure_func
184
+
185
+ # Check if any execution pins exist
186
+ if hasattr(node, 'pins'):
187
+ for pin in node.pins:
188
+ if hasattr(pin, 'pin_type') and pin.pin_type.pin_category == 'exec':
189
+ return False
190
+
191
+ # Default to True for math operations
192
+ return True
193
+
194
+ def _extract_pins(self, node, direction: str) -> List[Dict]:
195
+ """Extract all pins of a specific direction"""
196
+ pins = []
197
+
198
+ if not hasattr(node, 'pins'):
199
+ return pins
200
+
201
+ target_direction = (unreal.EdGraphPinDirection.EGPD_INPUT
202
+ if direction == "EGPD_Input"
203
+ else unreal.EdGraphPinDirection.EGPD_OUTPUT)
204
+
205
+ for pin in node.pins:
206
+ if pin.direction == target_direction:
207
+ pin_info = {
208
+ "name": str(pin.pin_name),
209
+ "display_name": str(pin.pin_friendly_name) if hasattr(pin, 'pin_friendly_name') else str(pin.pin_name),
210
+ "category": str(pin.pin_type.pin_category) if hasattr(pin.pin_type, 'pin_category') else "unknown",
211
+ "subcategory": str(pin.pin_type.pin_sub_category) if hasattr(pin.pin_type, 'pin_sub_category') and pin.pin_type.pin_sub_category else None,
212
+ "is_array": pin.pin_type.is_array if hasattr(pin.pin_type, 'is_array') else False,
213
+ "is_reference": pin.pin_type.is_reference if hasattr(pin.pin_type, 'is_reference') else False,
214
+ "is_connected": len(pin.linked_to) > 0 if hasattr(pin, 'linked_to') else False,
215
+ "default_value": str(pin.default_value) if hasattr(pin, 'default_value') and pin.default_value else None,
216
+ "connected_to": [str(linked_pin.pin_name) for linked_pin in pin.linked_to] if hasattr(pin, 'linked_to') else [],
217
+ "object_type": str(pin.pin_type.pin_sub_category_object) if hasattr(pin.pin_type, 'pin_sub_category_object') and pin.pin_type.pin_sub_category_object else None
218
+ }
219
+ pins.append(pin_info)
220
+
221
+ return pins
222
+
223
+ def _list_available_graphs(self, blueprint) -> List[Dict]:
224
+ """List all available graphs in a Blueprint for debugging"""
225
+ graphs = []
226
+
227
+ try:
228
+ ubergraph_pages = blueprint.get_editor_property('ubergraph_pages')
229
+ if ubergraph_pages:
230
+ for graph in ubergraph_pages:
231
+ graphs.append({
232
+ "name": graph.get_name(),
233
+ "guid": str(graph.graph_guid),
234
+ "type": "UberGraph"
235
+ })
236
+
237
+ function_graphs = blueprint.get_editor_property('function_graphs')
238
+ if function_graphs:
239
+ for graph in function_graphs:
240
+ graphs.append({
241
+ "name": graph.get_name(),
242
+ "guid": str(graph.graph_guid),
243
+ "type": "Function"
244
+ })
245
+ except Exception as e:
246
+ print(f"Error listing graphs: {e}")
247
+
248
+ return graphs
249
+
250
+ def _list_available_nodes(self, graph) -> List[Dict]:
251
+ """List all available nodes in a graph for debugging"""
252
+ nodes = []
253
+
254
+ try:
255
+ graph_nodes = graph.get_editor_property('nodes')
256
+ if graph_nodes:
257
+ for node in graph_nodes:
258
+ nodes.append({
259
+ "name": node.get_name() if hasattr(node, 'get_name') else "Unknown",
260
+ "guid": str(node.node_guid),
261
+ "type": type(node).__name__
262
+ })
263
+ except Exception as e:
264
+ print(f"Error listing nodes: {e}")
265
+
266
+ return nodes[:10] # Limit to first 10 for readability
267
+
268
+ def _find_node_containing_pin(self, nodes, pin):
269
+ """Find which node contains a specific pin"""
270
+ for node in nodes:
271
+ if hasattr(node, 'pins'):
272
+ for node_pin in node.pins:
273
+ if node_pin == pin:
274
+ return node
275
+ return None
276
+
277
+ def get_all_graph_connections(self, blueprint_path: str, function_id: str) -> Dict:
278
+ """
279
+ Get all existing connections in a graph.
280
+
281
+ Args:
282
+ blueprint_path: Path to Blueprint asset
283
+ function_id: GUID of the function graph
284
+
285
+ Returns:
286
+ {
287
+ "success": bool,
288
+ "connection_count": int,
289
+ "connections": [
290
+ {
291
+ "source_node": str,
292
+ "source_pin": str,
293
+ "target_node": str,
294
+ "target_pin": str,
295
+ "type": str
296
+ }
297
+ ]
298
+ }
299
+ """
300
+ if not UNREAL_AVAILABLE:
301
+ return {"success": False, "error": "Unreal Engine Python API not available"}
302
+
303
+ try:
304
+ blueprint = unreal.EditorAssetLibrary.load_asset(blueprint_path)
305
+ graph = self._find_graph_by_id(blueprint, function_id)
306
+
307
+ if not graph:
308
+ return {"success": False, "error": "Graph not found"}
309
+
310
+ connections = []
311
+ nodes = graph.get_editor_property('nodes')
312
+
313
+ for node in nodes:
314
+ if not hasattr(node, 'pins'):
315
+ continue
316
+
317
+ for pin in node.pins:
318
+ if pin.direction == unreal.EdGraphPinDirection.EGPD_OUTPUT:
319
+ if hasattr(pin, 'linked_to'):
320
+ for linked_pin in pin.linked_to:
321
+ # Find the target node
322
+ target_node = self._find_node_containing_pin(nodes, linked_pin)
323
+
324
+ connections.append({
325
+ "source_node": str(node.node_guid),
326
+ "source_pin": str(pin.pin_name),
327
+ "target_node": str(target_node.node_guid) if target_node else "Unknown",
328
+ "target_pin": str(linked_pin.pin_name),
329
+ "type": str(pin.pin_type.pin_category) if hasattr(pin.pin_type, 'pin_category') else "unknown"
330
+ })
331
+
332
+ return {
333
+ "success": True,
334
+ "connection_count": len(connections),
335
+ "connections": connections
336
+ }
337
+
338
+ except Exception as e:
339
+ return {"success": False, "error": str(e)}
340
+
341
+
342
+ # Standalone function for use as MCP tool
343
+ def get_node_pin_details_tool(blueprint_path: str, function_id: str, node_id: str) -> str:
344
+ """
345
+ MCP tool function to get node pin details.
346
+
347
+ Returns JSON string for MCP protocol.
348
+ """
349
+ service = PinDiscoveryService()
350
+ result = service.get_node_pins(blueprint_path, function_id, node_id)
351
+ return json.dumps(result, indent=2)
352
+
353
+
354
+ # Testing function
355
+ if __name__ == "__main__":
356
+ if not UNREAL_AVAILABLE:
357
+ print("Running in test mode - Unreal Engine not available")
358
+ print("This module must be run inside Unreal Engine Python interpreter")
359
+ else:
360
+ print("Pin Discovery Service ready")
361
+ service = PinDiscoveryService()
362
+ print(f"Cache enabled: {service.cache_enabled}")
UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/utils/connection_rules.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Connection Rules and Type Compatibility for Unreal Engine Blueprints
3
+
4
+ This module defines the rules for connecting Blueprint pins, including
5
+ type compatibility matrices and conversion suggestions.
6
+ """
7
+
8
+ from typing import List, Tuple, Optional
9
+
10
+
11
+ # Type compatibility matrix - defines which pin types can connect to which
12
+ TYPE_COMPATIBILITY = {
13
+ 'exec': ['exec'],
14
+ 'boolean': ['boolean', 'byte', 'int', 'wildcard'],
15
+ 'byte': ['byte', 'int', 'float', 'double', 'wildcard'],
16
+ 'int': ['int', 'float', 'double', 'wildcard'],
17
+ 'int64': ['int64', 'double', 'wildcard'],
18
+ 'float': ['float', 'double', 'wildcard'],
19
+ 'double': ['double', 'wildcard'],
20
+ 'name': ['name', 'string', 'text', 'wildcard'],
21
+ 'string': ['string', 'text', 'wildcard'],
22
+ 'text': ['text', 'wildcard'],
23
+ 'struct': ['struct', 'wildcard'], # Must also match subcategory
24
+ 'object': ['object', 'wildcard'], # Must also check inheritance
25
+ 'class': ['class', 'wildcard'],
26
+ 'interface': ['interface', 'wildcard'],
27
+ 'delegate': ['delegate'],
28
+ 'wildcard': [
29
+ 'exec', 'boolean', 'byte', 'int', 'int64', 'float', 'double',
30
+ 'name', 'string', 'text', 'struct', 'object', 'class',
31
+ 'interface', 'delegate'
32
+ ]
33
+ }
34
+
35
+
36
+ # Type conversion suggestions - maps (from_type, to_type) to conversion node names
37
+ TYPE_CONVERSIONS = {
38
+ ('int', 'float'): "Use implicit conversion or 'IntToFloat' node",
39
+ ('float', 'int'): "Add 'FloatToInt' node (truncates decimal)",
40
+ ('int', 'string'): "Add 'IntToString' node",
41
+ ('float', 'string'): "Add 'FloatToString' node",
42
+ ('string', 'int'): "Add 'StringToInt' node",
43
+ ('string', 'float'): "Add 'StringToFloat' node",
44
+ ('boolean', 'string'): "Add 'BoolToString' node",
45
+ ('name', 'string'): "Add 'NameToString' node",
46
+ ('string', 'name'): "Add 'StringToName' node",
47
+ ('byte', 'int'): "Implicit conversion supported",
48
+ ('int', 'int64'): "Implicit conversion supported",
49
+ ('float', 'double'): "Implicit conversion supported",
50
+ ('int', 'byte'): "Add 'IntToByte' node (may overflow)",
51
+ ('string', 'text'): "Use 'StringToText' node",
52
+ ('text', 'string'): "Use 'TextToString' node"
53
+ }
54
+
55
+
56
+ def are_types_compatible(source_type: str, target_type: str) -> bool:
57
+ """
58
+ Check if two pin types can be connected.
59
+
60
+ Args:
61
+ source_type: The type of the output pin
62
+ target_type: The type of the input pin
63
+
64
+ Returns:
65
+ bool: True if the types are compatible, False otherwise
66
+ """
67
+ compatible_types = TYPE_COMPATIBILITY.get(source_type, [])
68
+ return target_type in compatible_types
69
+
70
+
71
+ def get_conversion_suggestion(from_type: str, to_type: str) -> List[str]:
72
+ """
73
+ Get suggestions for converting between incompatible types.
74
+
75
+ Args:
76
+ from_type: The source pin type
77
+ to_type: The target pin type
78
+
79
+ Returns:
80
+ List of suggestion strings
81
+ """
82
+ # Check if direct conversion exists
83
+ conversion = TYPE_CONVERSIONS.get((from_type, to_type))
84
+ if conversion:
85
+ return [conversion]
86
+
87
+ # Return generic suggestions
88
+ return [
89
+ f"No direct conversion from '{from_type}' to '{to_type}'",
90
+ "Consider alternative logic path",
91
+ "Check if intermediate conversion nodes are needed"
92
+ ]
93
+
94
+
95
+ def validate_array_compatibility(source_is_array: bool, target_is_array: bool) -> Tuple[bool, Optional[List[str]]]:
96
+ """
97
+ Validate array/non-array compatibility.
98
+
99
+ Args:
100
+ source_is_array: Whether the source pin is an array
101
+ target_is_array: Whether the target pin is an array
102
+
103
+ Returns:
104
+ Tuple of (is_valid, suggestions)
105
+ """
106
+ if source_is_array == target_is_array:
107
+ return (True, None)
108
+
109
+ # Incompatible array types
110
+ if source_is_array and not target_is_array:
111
+ return (False, [
112
+ "Use 'ForEach' loop to iterate array elements",
113
+ "Use 'Get' node to access single array element by index",
114
+ "Consider using 'First' or 'Last' array functions"
115
+ ])
116
+ else: # not source_is_array and target_is_array
117
+ return (False, [
118
+ "Use 'MakeArray' node to convert single value to array",
119
+ "Use 'Append' or 'Add' to add to existing array",
120
+ "Consider if target should accept single value instead"
121
+ ])
122
+
123
+
124
+ def validate_struct_compatibility(source_subcategory: Optional[str],
125
+ target_subcategory: Optional[str]) -> Tuple[bool, Optional[List[str]]]:
126
+ """
127
+ Validate struct type compatibility.
128
+
129
+ Structs must have matching subcategories to be compatible.
130
+
131
+ Args:
132
+ source_subcategory: The source pin's struct type
133
+ target_subcategory: The target pin's struct type
134
+
135
+ Returns:
136
+ Tuple of (is_valid, suggestions)
137
+ """
138
+ if source_subcategory == target_subcategory:
139
+ return (True, None)
140
+
141
+ return (False, [
142
+ f"Struct type mismatch: '{source_subcategory}' vs '{target_subcategory}'",
143
+ "Cannot directly convert between different struct types",
144
+ "Consider breaking struct members and reconstructing"
145
+ ])
146
+
147
+
148
+ def validate_object_compatibility(source_object_type: Optional[str],
149
+ target_object_type: Optional[str]) -> Tuple[bool, Optional[List[str]]]:
150
+ """
151
+ Validate object reference compatibility.
152
+
153
+ Note: This is a simplified check. Full implementation would need
154
+ to query Unreal's type hierarchy to check inheritance.
155
+
156
+ Args:
157
+ source_object_type: The source pin's object class
158
+ target_object_type: The target pin's object class
159
+
160
+ Returns:
161
+ Tuple of (is_valid, suggestions)
162
+ """
163
+ # Same type is always compatible
164
+ if source_object_type == target_object_type:
165
+ return (True, None)
166
+
167
+ # If either is None, we can't validate
168
+ if not source_object_type or not target_object_type:
169
+ return (True, ["Warning: Could not fully validate object type compatibility"])
170
+
171
+ # Simplified check - in real implementation, would check inheritance hierarchy
172
+ return (True, [
173
+ "Warning: Object types differ - verify inheritance compatibility",
174
+ f"Source: {source_object_type}, Target: {target_object_type}",
175
+ "Use 'Cast To' node if types are incompatible"
176
+ ])
177
+
178
+
179
+ # Common pin name patterns for intelligent matching
180
+ COMMON_PIN_PATTERNS = {
181
+ 'execution_output': ['then', 'exec', 'completed', 'finished'],
182
+ 'execution_input': ['execute', 'exec'],
183
+ 'return_value': ['returnvalue', 'return', 'result', 'output'],
184
+ 'input_value': ['input', 'value', 'in', 'a', 'b'],
185
+ 'condition': ['condition', 'test', 'check', 'if'],
186
+ 'true_branch': ['true', 'then'],
187
+ 'false_branch': ['false', 'else']
188
+ }
189
+
190
+
191
+ def get_pin_name_category(pin_name: str) -> Optional[str]:
192
+ """
193
+ Categorize a pin name based on common patterns.
194
+
195
+ Args:
196
+ pin_name: The pin name to categorize
197
+
198
+ Returns:
199
+ The category name, or None if no match
200
+ """
201
+ pin_lower = pin_name.lower()
202
+
203
+ for category, patterns in COMMON_PIN_PATTERNS.items():
204
+ if any(pattern in pin_lower for pattern in patterns):
205
+ return category
206
+
207
+ return None
208
+
209
+
210
+ def calculate_pin_name_similarity(name1: str, name2: str) -> float:
211
+ """
212
+ Calculate similarity score between two pin names.
213
+
214
+ Uses difflib's SequenceMatcher for fuzzy string matching.
215
+
216
+ Args:
217
+ name1: First pin name
218
+ name2: Second pin name
219
+
220
+ Returns:
221
+ Similarity score from 0.0 to 1.0
222
+ """
223
+ from difflib import SequenceMatcher
224
+
225
+ return SequenceMatcher(None, name1.lower(), name2.lower()).ratio()
UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/utils/node_helpers.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Helper Functions for Blueprint Node Operations
3
+
4
+ This module provides utility functions for working with Blueprint nodes,
5
+ including node type detection, position calculation, and common operations.
6
+ """
7
+
8
+ from typing import Optional, Tuple, List, Dict
9
+
10
+
11
+ def calculate_node_spacing(node_count: int, start_x: int = 0, start_y: int = 0,
12
+ horizontal_spacing: int = 400, vertical_spacing: int = 300) -> List[Tuple[int, int]]:
13
+ """
14
+ Calculate evenly-spaced positions for a chain of nodes.
15
+
16
+ Args:
17
+ node_count: Number of nodes to position
18
+ start_x: Starting X coordinate
19
+ start_y: Starting Y coordinate
20
+ horizontal_spacing: Space between nodes horizontally
21
+ vertical_spacing: Space between rows vertically
22
+
23
+ Returns:
24
+ List of (x, y) coordinate tuples
25
+ """
26
+ positions = []
27
+ nodes_per_row = 5 # Max nodes before wrapping to next row
28
+
29
+ for i in range(node_count):
30
+ row = i // nodes_per_row
31
+ col = i % nodes_per_row
32
+
33
+ x = start_x + (col * horizontal_spacing)
34
+ y = start_y + (row * vertical_spacing)
35
+
36
+ positions.append((x, y))
37
+
38
+ return positions
39
+
40
+
41
+ def is_execution_node(node_class: str) -> bool:
42
+ """
43
+ Determine if a node type has execution pins.
44
+
45
+ Args:
46
+ node_class: The node's class name
47
+
48
+ Returns:
49
+ True if node has execution flow, False if pure
50
+ """
51
+ # Pure function nodes don't have execution pins
52
+ pure_node_prefixes = [
53
+ 'K2Node_CallMathFunction',
54
+ 'K2Node_CommutativeAssociativeBinaryOperator',
55
+ 'K2Node_GetVariable',
56
+ 'K2Node_Literal'
57
+ ]
58
+
59
+ for prefix in pure_node_prefixes:
60
+ if node_class.startswith(prefix):
61
+ return False
62
+
63
+ # Event and flow control nodes always have execution
64
+ exec_node_prefixes = [
65
+ 'K2Node_Event',
66
+ 'K2Node_CallFunction',
67
+ 'K2Node_IfThenElse',
68
+ 'K2Node_Branch',
69
+ 'K2Node_ForEachLoop',
70
+ 'K2Node_WhileLoop',
71
+ 'K2Node_Sequence'
72
+ ]
73
+
74
+ for prefix in exec_node_prefixes:
75
+ if node_class.startswith(prefix):
76
+ return True
77
+
78
+ # Default: assume has execution
79
+ return True
80
+
81
+
82
+ def get_node_display_name(node_class: str, node_name: str) -> str:
83
+ """
84
+ Get a user-friendly display name for a node.
85
+
86
+ Args:
87
+ node_class: The node's class name
88
+ node_name: The node's internal name
89
+
90
+ Returns:
91
+ A readable display name
92
+ """
93
+ # Remove common prefixes
94
+ display = node_name
95
+ prefixes_to_remove = ['K2Node_', 'EdGraph', 'UK2Node_']
96
+
97
+ for prefix in prefixes_to_remove:
98
+ if display.startswith(prefix):
99
+ display = display[len(prefix):]
100
+
101
+ # Convert CamelCase to spaces
102
+ import re
103
+ display = re.sub(r'([A-Z])', r' \1', display).strip()
104
+
105
+ return display
106
+
107
+
108
+ def get_common_node_types() -> Dict[str, Dict[str, any]]:
109
+ """
110
+ Get information about common Blueprint node types.
111
+
112
+ Returns:
113
+ Dictionary mapping node type names to their properties
114
+ """
115
+ return {
116
+ 'Add': {
117
+ 'class': 'K2Node_CallMathFunction',
118
+ 'function': 'Add_FloatFloat',
119
+ 'input_pins': ['A', 'B'],
120
+ 'output_pins': ['ReturnValue'],
121
+ 'is_pure': True
122
+ },
123
+ 'Multiply': {
124
+ 'class': 'K2Node_CallMathFunction',
125
+ 'function': 'Multiply_FloatFloat',
126
+ 'input_pins': ['A', 'B'],
127
+ 'output_pins': ['ReturnValue'],
128
+ 'is_pure': True
129
+ },
130
+ 'Subtract': {
131
+ 'class': 'K2Node_CallMathFunction',
132
+ 'function': 'Subtract_FloatFloat',
133
+ 'input_pins': ['A', 'B'],
134
+ 'output_pins': ['ReturnValue'],
135
+ 'is_pure': True
136
+ },
137
+ 'Divide': {
138
+ 'class': 'K2Node_CallMathFunction',
139
+ 'function': 'Divide_FloatFloat',
140
+ 'input_pins': ['A', 'B'],
141
+ 'output_pins': ['ReturnValue'],
142
+ 'is_pure': True
143
+ },
144
+ 'Branch': {
145
+ 'class': 'K2Node_IfThenElse',
146
+ 'input_pins': ['execute', 'Condition'],
147
+ 'output_pins': ['then', 'True', 'False'],
148
+ 'is_pure': False
149
+ },
150
+ 'Sequence': {
151
+ 'class': 'K2Node_ExecutionSequence',
152
+ 'input_pins': ['execute'],
153
+ 'output_pins': ['then 0', 'then 1', 'then 2'],
154
+ 'is_pure': False
155
+ },
156
+ 'PrintString': {
157
+ 'class': 'K2Node_CallFunction',
158
+ 'function': 'PrintString',
159
+ 'input_pins': ['execute', 'InString'],
160
+ 'output_pins': ['then'],
161
+ 'is_pure': False
162
+ },
163
+ 'ForLoop': {
164
+ 'class': 'K2Node_ForEachLoop',
165
+ 'input_pins': ['execute', 'FirstIndex', 'LastIndex'],
166
+ 'output_pins': ['LoopBody', 'Index', 'Completed'],
167
+ 'is_pure': False
168
+ }
169
+ }
170
+
171
+
172
+ def guess_pin_type_from_name(pin_name: str) -> Optional[str]:
173
+ """
174
+ Attempt to guess pin type from its name.
175
+
176
+ This is useful when pin type information isn't directly available.
177
+
178
+ Args:
179
+ pin_name: The name of the pin
180
+
181
+ Returns:
182
+ Guessed type string, or None if can't determine
183
+ """
184
+ pin_lower = pin_name.lower()
185
+
186
+ type_hints = {
187
+ 'exec': ['execute', 'then', 'completed'],
188
+ 'boolean': ['condition', 'bool', 'is', 'has', 'can', 'should'],
189
+ 'int': ['index', 'count', 'num', 'id'],
190
+ 'float': ['value', 'amount', 'distance', 'speed', 'time'],
191
+ 'string': ['string', 'text', 'name', 'message', 'label'],
192
+ 'object': ['object', 'actor', 'component', 'target', 'reference']
193
+ }
194
+
195
+ for pin_type, hints in type_hints.items():
196
+ if any(hint in pin_lower for hint in hints):
197
+ return pin_type
198
+
199
+ return None
200
+
201
+
202
+ def format_connection_path(source_node: str, source_pin: str,
203
+ target_node: str, target_pin: str) -> str:
204
+ """
205
+ Format a connection as a readable string.
206
+
207
+ Args:
208
+ source_node: Source node GUID or name
209
+ source_pin: Source pin name
210
+ target_node: Target node GUID or name
211
+ target_pin: Target pin name
212
+
213
+ Returns:
214
+ Formatted string like "NodeA.PinOut โ†’ NodeB.PinIn"
215
+ """
216
+ # Truncate GUIDs for readability
217
+ def truncate_guid(guid: str) -> str:
218
+ if len(guid) > 8 and '-' in guid:
219
+ return guid[:8] + '...'
220
+ return guid
221
+
222
+ source = truncate_guid(source_node)
223
+ target = truncate_guid(target_node)
224
+
225
+ return f"{source}.{source_pin} โ†’ {target}.{target_pin}"
226
+
227
+
228
+ def validate_node_position(x: int, y: int,
229
+ min_x: int = -10000, max_x: int = 10000,
230
+ min_y: int = -10000, max_y: int = 10000) -> Tuple[bool, str]:
231
+ """
232
+ Validate that a node position is within acceptable bounds.
233
+
234
+ Args:
235
+ x: X coordinate
236
+ y: Y coordinate
237
+ min_x: Minimum allowed X
238
+ max_x: Maximum allowed X
239
+ min_y: Minimum allowed Y
240
+ max_y: Maximum allowed Y
241
+
242
+ Returns:
243
+ Tuple of (is_valid, error_message)
244
+ """
245
+ if not (min_x <= x <= max_x):
246
+ return (False, f"X position {x} is outside bounds [{min_x}, {max_x}]")
247
+
248
+ if not (min_y <= y <= max_y):
249
+ return (False, f"Y position {y} is outside bounds [{min_y}, {max_y}]")
250
+
251
+ return (True, "")
252
+
253
+
254
+ def find_midpoint(pos1: Tuple[int, int], pos2: Tuple[int, int]) -> Tuple[int, int]:
255
+ """
256
+ Find the midpoint between two node positions.
257
+
258
+ Useful for placing conversion nodes between incompatible connections.
259
+
260
+ Args:
261
+ pos1: First position (x, y)
262
+ pos2: Second position (x, y)
263
+
264
+ Returns:
265
+ Midpoint coordinates (x, y)
266
+ """
267
+ x1, y1 = pos1
268
+ x2, y2 = pos2
269
+
270
+ mid_x = (x1 + x2) // 2
271
+ mid_y = (y1 + y2) // 2
272
+
273
+ return (mid_x, mid_y)
UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/utils/pin_types.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pin Type Definitions for Unreal Engine Blueprint System
3
+
4
+ This module defines all data structures for Blueprint pin information,
5
+ node information, and connection requests/results.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Optional, List
11
+
12
+
13
+ class PinCategory(Enum):
14
+ """All Unreal Engine pin types/categories"""
15
+ EXEC = "exec"
16
+ BOOLEAN = "boolean"
17
+ BYTE = "byte"
18
+ INT = "int"
19
+ INT64 = "int64"
20
+ FLOAT = "float"
21
+ DOUBLE = "double"
22
+ NAME = "name"
23
+ STRING = "string"
24
+ TEXT = "text"
25
+ STRUCT = "struct"
26
+ OBJECT = "object"
27
+ CLASS = "class"
28
+ INTERFACE = "interface"
29
+ DELEGATE = "delegate"
30
+ WILDCARD = "wildcard"
31
+
32
+
33
+ class PinDirection(Enum):
34
+ """Pin direction: input or output"""
35
+ INPUT = "EGPD_Input"
36
+ OUTPUT = "EGPD_Output"
37
+
38
+
39
+ @dataclass
40
+ class PinInfo:
41
+ """Complete information about a Blueprint pin"""
42
+ name: str
43
+ display_name: str
44
+ category: str # PinCategory value
45
+ subcategory: Optional[str]
46
+ direction: str # PinDirection value
47
+ is_array: bool
48
+ is_reference: bool
49
+ is_connected: bool
50
+ default_value: Optional[str]
51
+ connected_to: Optional[List[str]] # List of pin names
52
+ object_type: Optional[str] # For object/class pins
53
+
54
+ def to_dict(self):
55
+ """Convert to dictionary for JSON serialization"""
56
+ return {
57
+ "name": self.name,
58
+ "display_name": self.display_name,
59
+ "category": self.category,
60
+ "subcategory": self.subcategory,
61
+ "direction": self.direction,
62
+ "is_array": self.is_array,
63
+ "is_reference": self.is_reference,
64
+ "is_connected": self.is_connected,
65
+ "default_value": self.default_value,
66
+ "connected_to": self.connected_to or [],
67
+ "object_type": self.object_type
68
+ }
69
+
70
+
71
+ @dataclass
72
+ class NodeInfo:
73
+ """Complete information about a Blueprint node"""
74
+ guid: str
75
+ node_type: str
76
+ node_class: str
77
+ display_name: str
78
+ position: tuple
79
+ input_pins: List[PinInfo]
80
+ output_pins: List[PinInfo]
81
+ is_pure: bool # Pure nodes don't have execution pins
82
+
83
+ def to_dict(self):
84
+ """Convert to dictionary for JSON serialization"""
85
+ return {
86
+ "guid": self.guid,
87
+ "node_type": self.node_type,
88
+ "node_class": self.node_class,
89
+ "display_name": self.display_name,
90
+ "position": list(self.position),
91
+ "input_pins": [p.to_dict() for p in self.input_pins],
92
+ "output_pins": [p.to_dict() for p in self.output_pins],
93
+ "is_pure": self.is_pure
94
+ }
95
+
96
+
97
+ @dataclass
98
+ class ConnectionRequest:
99
+ """Request to connect two pins"""
100
+ blueprint_path: str
101
+ function_id: str
102
+ source_node_id: str
103
+ source_pin_name: str
104
+ target_node_id: str
105
+ target_pin_name: str
106
+ force: bool = False # Break existing connections if needed
107
+
108
+ def to_dict(self):
109
+ """Convert to dictionary for JSON serialization"""
110
+ return {
111
+ "blueprint_path": self.blueprint_path,
112
+ "function_id": self.function_id,
113
+ "source_node_id": self.source_node_id,
114
+ "source_pin_name": self.source_pin_name,
115
+ "target_node_id": self.target_node_id,
116
+ "target_pin_name": self.target_pin_name,
117
+ "force": self.force
118
+ }
119
+
120
+
121
+ @dataclass
122
+ class ConnectionResult:
123
+ """Result of a connection attempt"""
124
+ success: bool
125
+ message: str
126
+ source_pin: Optional[PinInfo]
127
+ target_pin: Optional[PinInfo]
128
+ warnings: List[str]
129
+ suggestions: List[str]
130
+
131
+ def to_dict(self):
132
+ """Convert to dictionary for JSON serialization"""
133
+ return {
134
+ "success": self.success,
135
+ "message": self.message,
136
+ "source_pin": self.source_pin.to_dict() if self.source_pin else None,
137
+ "target_pin": self.target_pin.to_dict() if self.target_pin else None,
138
+ "warnings": self.warnings,
139
+ "suggestions": self.suggestions
140
+ }
141
+
142
+
143
+ class ValidationError(Enum):
144
+ """Types of validation errors that can occur"""
145
+ TYPE_MISMATCH = "type_mismatch"
146
+ DIRECTION_ERROR = "direction_error"
147
+ ALREADY_CONNECTED = "already_connected"
148
+ PIN_NOT_FOUND = "pin_not_found"
149
+ INCOMPATIBLE_ARRAY = "incompatible_array"
150
+ DELEGATE_ERROR = "delegate_error"
151
+ STRUCT_MISMATCH = "struct_mismatch"
152
+ OBJECT_TYPE_MISMATCH = "object_type_mismatch"
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # handlers package
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/actor_commands.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unreal
2
+ import json
3
+ from typing import Dict, Any, List, Tuple, Union
4
+
5
+ from utils import unreal_conversions as uc
6
+ from utils import logging as log
7
+ from utils import asset_validation as av
8
+
9
+
10
+ def handle_modify_object(command: Dict[str, Any]) -> Dict[str, Any]:
11
+ """
12
+ Handle a modify_object command
13
+
14
+ Args:
15
+ command: The command dictionary containing:
16
+ - actor_name: Name of the actor to modify
17
+ - property_type: Type of property to modify (material, position, rotation, scale)
18
+ - value: Value to set for the property
19
+
20
+ Returns:
21
+ Response dictionary with success/failure status
22
+ """
23
+ try:
24
+ # Extract parameters
25
+ actor_name = command.get("actor_name")
26
+ property_type = command.get("property_type")
27
+ value = command.get("value")
28
+
29
+ if not actor_name or not property_type or value is None:
30
+ log.log_error("Missing required parameters for modify_object")
31
+ return {"success": False, "error": "Missing required parameters"}
32
+
33
+ log.log_command("modify_object", f"Actor: {actor_name}, Property: {property_type}")
34
+
35
+ # Use the C++ utility class
36
+ gen_actor_utils = unreal.GenActorUtils
37
+
38
+ # Handle different property types
39
+ if property_type == "material":
40
+ # Set the material
41
+ material_path = value
42
+ success = gen_actor_utils.set_actor_material_by_path(actor_name, material_path)
43
+ if success:
44
+ log.log_result("modify_object", True, f"Material of {actor_name} set to {material_path}")
45
+ return {"success": True}
46
+ log.log_error(f"Failed to set material for {actor_name}")
47
+ return {"success": False, "error": f"Failed to set material for {actor_name}"}
48
+
49
+ elif property_type == "position":
50
+ # Set position
51
+ try:
52
+ vec = uc.to_unreal_vector(value)
53
+ success = gen_actor_utils.set_actor_position(actor_name, vec)
54
+ if success:
55
+ log.log_result("modify_object", True, f"Position of {actor_name} set to {vec}")
56
+ return {"success": True}
57
+ log.log_error(f"Failed to set position for {actor_name}")
58
+ return {"success": False, "error": f"Failed to set position for {actor_name}"}
59
+ except ValueError as e:
60
+ log.log_error(str(e))
61
+ return {"success": False, "error": str(e)}
62
+
63
+ elif property_type == "rotation":
64
+ # Set rotation
65
+ try:
66
+ rot = uc.to_unreal_rotator(value)
67
+ success = gen_actor_utils.set_actor_rotation(actor_name, rot)
68
+ if success:
69
+ log.log_result("modify_object", True, f"Rotation of {actor_name} set to {rot}")
70
+ return {"success": True}
71
+ log.log_error(f"Failed to set rotation for {actor_name}")
72
+ return {"success": False, "error": f"Failed to set rotation for {actor_name}"}
73
+ except ValueError as e:
74
+ log.log_error(str(e))
75
+ return {"success": False, "error": str(e)}
76
+
77
+ elif property_type == "scale":
78
+ # Set scale
79
+ try:
80
+ scale = uc.to_unreal_vector(value)
81
+ success = gen_actor_utils.set_actor_scale(actor_name, scale)
82
+ if success:
83
+ log.log_result("modify_object", True, f"Scale of {actor_name} set to {scale}")
84
+ return {"success": True}
85
+ log.log_error(f"Failed to set scale for {actor_name}")
86
+ return {"success": False, "error": f"Failed to set scale for {actor_name}"}
87
+ except ValueError as e:
88
+ log.log_error(str(e))
89
+ return {"success": False, "error": str(e)}
90
+
91
+ else:
92
+ log.log_error(f"Unknown property type: {property_type}")
93
+ return {"success": False, "error": f"Unknown property type: {property_type}"}
94
+
95
+ except Exception as e:
96
+ log.log_error(f"Error modifying object: {str(e)}", include_traceback=True)
97
+ return {"success": False, "error": str(e)}
98
+
99
+ def handle_edit_component_property(command: Dict[str, Any]) -> Dict[str, Any]:
100
+ """
101
+ Handle a command to edit a component property in a Blueprint or scene actor.
102
+
103
+ Args:
104
+ command: The command dictionary containing:
105
+ - blueprint_path: Path to the Blueprint
106
+ - component_name: Name of the component
107
+ - property_name: Name of the property to edit
108
+ - value: New value as a string
109
+ - is_scene_actor: Boolean flag for scene actor (optional, default False)
110
+ - actor_name: Name of the scene actor (required if is_scene_actor is True)
111
+
112
+ Returns:
113
+ Dictionary with success/failure status, message, and optional suggestions
114
+ """
115
+ try:
116
+ # FIX: Strip whitespace from paths to prevent corruption (unreal-mcp-monitor)
117
+ blueprint_path = command.get("blueprint_path", "").strip()
118
+ component_name = command.get("component_name", "").strip() if command.get("component_name") else None
119
+ property_name = command.get("property_name", "").strip() if command.get("property_name") else None
120
+ value = command.get("value")
121
+ # Strip whitespace from value if it's a string (could be asset path)
122
+ if isinstance(value, str):
123
+ value = value.strip()
124
+ is_scene_actor = command.get("is_scene_actor", False)
125
+ actor_name = command.get("actor_name", "").strip()
126
+
127
+ if not component_name or not property_name or value is None:
128
+ log.log_error("Missing required parameters for edit_component_property")
129
+ return {"success": False, "error": "Missing required parameters"}
130
+
131
+ if is_scene_actor and not actor_name:
132
+ log.log_error("Actor name required for scene actor editing")
133
+ return {"success": False, "error": "Actor name required for scene actor"}
134
+
135
+ # Log detailed information about what we're trying to do
136
+ log_msg = f"Blueprint: {blueprint_path}, Component: {component_name}, Property: {property_name}, Value: {value}"
137
+ if is_scene_actor:
138
+ log_msg += f", Actor: {actor_name}"
139
+ log.log_command("edit_component_property", log_msg)
140
+
141
+ # Call the C++ implementation
142
+ node_creator = unreal.GenObjectProperties
143
+ result = node_creator.edit_component_property(blueprint_path, component_name, property_name, value, is_scene_actor, actor_name)
144
+
145
+ # Parse the result - CHANGED: Convert JSON string to dict
146
+ try:
147
+ parsed_result = json.loads(result)
148
+ log.log_result("edit_component_property", parsed_result["success"],
149
+ parsed_result.get("message", parsed_result.get("error", "No message")))
150
+ # CHANGED: Return the parsed dict instead of the original JSON string
151
+ return parsed_result
152
+ except json.JSONDecodeError:
153
+ # If the result is not valid JSON, wrap it in a JSON object
154
+ log.log_warning(f"Invalid JSON result from C++: {result}")
155
+ return {"success": False, "error": f"Invalid response format: {result}"}
156
+
157
+ except Exception as e:
158
+ log.log_error(f"Error editing component property: {str(e)}", include_traceback=True)
159
+ return {"success": False, "error": str(e)}
160
+
161
+
162
+ def handle_add_component_with_events(command: Dict[str, Any]) -> Dict[str, Any]:
163
+ """
164
+ Handle a command to add a component to a Blueprint with overlap events if applicable.
165
+
166
+ Args:
167
+ command: The command dictionary containing:
168
+ - blueprint_path: Path to the Blueprint
169
+ - component_name: Name of the new component
170
+ - component_class: Class of the component (e.g., "BoxComponent")
171
+
172
+ Returns:
173
+ Response dictionary with success/failure status, message, and event GUIDs
174
+ """
175
+ try:
176
+ blueprint_path = command.get("blueprint_path")
177
+ component_name = command.get("component_name")
178
+ component_class = command.get("component_class")
179
+
180
+ if not blueprint_path or not component_name or not component_class:
181
+ log.log_error("Missing required parameters for add_component_with_events")
182
+ return {"success": False, "error": "Missing required parameters"}
183
+
184
+ log.log_command("add_component_with_events", f"Blueprint: {blueprint_path}, Component: {component_name}, Class: {component_class}")
185
+
186
+ node_creator = unreal.GenBlueprintUtils
187
+ result = node_creator.add_component_with_events(blueprint_path, component_name, component_class)
188
+
189
+ import json
190
+ parsed_result = json.loads(result)
191
+ log.log_result("add_component_with_events", parsed_result["success"], parsed_result.get("message", parsed_result.get("error", "No message")))
192
+ return parsed_result
193
+
194
+ except Exception as e:
195
+ log.log_error(f"Error adding component with events: {str(e)}", include_traceback=True)
196
+ return {"success": False, "error": str(e)}
197
+
198
+ def handle_create_game_mode(command: Dict[str, Any]) -> Dict[str, Any]:
199
+ try:
200
+ game_mode_path = command.get("game_mode_path")
201
+ pawn_blueprint_path = command.get("pawn_blueprint_path")
202
+ base_class = command.get("base_class", "GameModeBase")
203
+
204
+ if not game_mode_path or not pawn_blueprint_path:
205
+ return {"success": False, "error": "Missing game_mode_path or pawn_blueprint_path"}
206
+
207
+ # FIX: Validate pawn blueprint exists before creating game mode (LM_STUDIO_DIAGNOSTIC_REPORT.md)
208
+ normalized_path = av.normalize_blueprint_path(pawn_blueprint_path)
209
+ validation_result = av.validate_asset_with_suggestion(normalized_path)
210
+
211
+ if not validation_result['valid']:
212
+ log.log_error(f"Pawn blueprint validation failed: {validation_result['message']}")
213
+ return {
214
+ "success": False,
215
+ "error": f"Invalid pawn blueprint: {validation_result['message']}",
216
+ "suggestion": validation_result.get('suggestion')
217
+ }
218
+
219
+ node_creator = unreal.GenActorUtils
220
+ result = node_creator.create_game_mode_with_pawn(game_mode_path, normalized_path, base_class)
221
+ return json.loads(result)
222
+ except Exception as e:
223
+ return {"success": False, "error": str(e)}
224
+
225
+ def handle_get_component_names(command: Dict[str, Any]) -> Dict[str, Any]:
226
+ """
227
+ Get all component names for a Blueprint or scene actor.
228
+ Helps users discover correct component names before editing.
229
+
230
+ Args:
231
+ command: Dict containing:
232
+ - blueprint_path: Path to Blueprint (for Blueprints)
233
+ - actor_name: Name of actor in scene (for scene actors)
234
+ - is_scene_actor: Whether target is a scene actor (default: False)
235
+
236
+ Returns:
237
+ Dict with list of component names and types
238
+ """
239
+ try:
240
+ blueprint_path = command.get("blueprint_path", "")
241
+ actor_name = command.get("actor_name", "")
242
+ is_scene_actor = command.get("is_scene_actor", False)
243
+
244
+ components_info = []
245
+
246
+ if is_scene_actor:
247
+ # Get scene actor
248
+ if not actor_name:
249
+ return {"success": False, "error": "Missing actor_name parameter for scene actor"}
250
+
251
+ # Get all actors in the level
252
+ editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
253
+ all_actors = editor_actor_subsystem.get_all_level_actors()
254
+
255
+ # Find the actor by name
256
+ target_actor = None
257
+ for actor in all_actors:
258
+ if actor.get_actor_label() == actor_name or actor.get_name() == actor_name:
259
+ target_actor = actor
260
+ break
261
+
262
+ if not target_actor:
263
+ return {
264
+ "success": False,
265
+ "error": f"Actor '{actor_name}' not found in scene",
266
+ "suggestion": "Use get_all_scene_objects to see available actors"
267
+ }
268
+
269
+ # Get components
270
+ components = target_actor.get_components_by_class(unreal.ActorComponent)
271
+
272
+ for comp in components:
273
+ comp_name = comp.get_name()
274
+ comp_class = comp.__class__.__name__
275
+ components_info.append({
276
+ "name": comp_name,
277
+ "class": comp_class
278
+ })
279
+
280
+ else:
281
+ # Get Blueprint
282
+ if not blueprint_path:
283
+ return {"success": False, "error": "Missing blueprint_path parameter for Blueprint"}
284
+
285
+ blueprint = unreal.load_asset(blueprint_path)
286
+ if not blueprint:
287
+ return {
288
+ "success": False,
289
+ "error": f"Blueprint not found: {blueprint_path}",
290
+ "suggestion": "Check that the path is correct and starts with /Game/"
291
+ }
292
+
293
+ # Get simple construction script
294
+ scs_tree = blueprint.get_editor_property('simple_construction_script')
295
+ if not scs_tree:
296
+ return {
297
+ "success": True,
298
+ "total_components": 0,
299
+ "components": [],
300
+ "message": "Blueprint has no components"
301
+ }
302
+
303
+ # Get all nodes (components)
304
+ nodes = scs_tree.get_all_nodes()
305
+
306
+ for node in nodes:
307
+ comp_name = node.get_variable_name()
308
+ comp_template = node.get_editor_property('component_template')
309
+ if comp_template:
310
+ comp_class = comp_template.__class__.__name__
311
+ else:
312
+ comp_class = "Unknown"
313
+
314
+ components_info.append({
315
+ "name": comp_name,
316
+ "class": comp_class
317
+ })
318
+
319
+ return {
320
+ "success": True,
321
+ "total_components": len(components_info),
322
+ "components": components_info,
323
+ "target": actor_name if is_scene_actor else blueprint_path
324
+ }
325
+
326
+ except Exception as e:
327
+ log.log_error(f"Error getting component names: {str(e)}", include_traceback=True)
328
+ return {
329
+ "success": False,
330
+ "error": f"Error: {str(e)}",
331
+ "recommendation": "Check that the target exists and is accessible"
332
+ }
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/base_handler.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Any
3
+
4
+
5
+ class CommandHandler(ABC):
6
+ """
7
+ Abstract base class for command handlers
8
+
9
+ All command handlers should inherit from this class and implement the handle method
10
+ """
11
+
12
+ @abstractmethod
13
+ def handle(self, command: Dict[str, Any]) -> Dict[str, Any]:
14
+ """
15
+ Handle a command and return a response
16
+
17
+ Args:
18
+ command: The command dictionary
19
+
20
+ Returns:
21
+ A response dictionary with at least a 'success' key
22
+ """
23
+ pass
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/basic_commands.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unreal
2
+ from typing import Dict, Any, List, Tuple
3
+
4
+ import base64
5
+ import os
6
+ import mss
7
+ import time
8
+ import tempfile # Used to find the OS's temporary folder
9
+
10
+ from utils import unreal_conversions as uc
11
+ from utils import logging as log
12
+
13
+ def handle_spawn(command: Dict[str, Any]) -> Dict[str, Any]:
14
+ """
15
+ Handle a spawn command
16
+
17
+ Args:
18
+ command: The command dictionary containing:
19
+ - actor_class: Actor class name/path or mesh path (e.g., "/Game/Blueprints/BP_Barrel" or "/Game/Meshes/SM_Barrel01.SM_Barrel01")
20
+ - location: [X, Y, Z] coordinates (optional)
21
+ - rotation: [Pitch, Yaw, Roll] in degrees (optional)
22
+ - scale: [X, Y, Z] scale factors (optional)
23
+ - actor_label: Optional custom name for the actor
24
+
25
+ Returns:
26
+ Response dictionary with success/failure status and additional info
27
+ """
28
+ try:
29
+ # Extract parameters
30
+ actor_class_name = command.get("actor_class", "Cube")
31
+ location = command.get("location", (0, 0, 0))
32
+ rotation = command.get("rotation", (0, 0, 0))
33
+ scale = command.get("scale", (1, 1, 1))
34
+ actor_label = command.get("actor_label")
35
+
36
+ unreal.log(f"Spawn command: Class: {actor_class_name}, Label: {actor_label}")
37
+
38
+ # Convert parameters to Unreal types
39
+ loc = uc.to_unreal_vector(location)
40
+ rot = uc.to_unreal_rotator(rotation)
41
+ scale_vector = uc.to_unreal_vector(scale)
42
+
43
+ actor = None
44
+ gen_actor_utils = unreal.GenActorUtils
45
+
46
+ # Check if it's a mesh path (e.g., "/Game/.../SM_Barrel01.SM_Barrel01")
47
+ if actor_class_name.startswith("/Game") and "." in actor_class_name:
48
+ # Try loading as a static mesh
49
+ mesh = unreal.load_object(None, actor_class_name)
50
+ if isinstance(mesh, unreal.StaticMesh):
51
+ actor = gen_actor_utils.spawn_static_mesh_actor(actor_class_name, loc, rot, scale_vector, actor_label or "")
52
+ else:
53
+ # Fallback to actor class if not a mesh
54
+ actor = gen_actor_utils.spawn_actor_from_class(actor_class_name, loc, rot, scale_vector, actor_label or "")
55
+ else:
56
+ # Handle basic shapes or actor classes
57
+ shape_map = {"cube": "Cube", "sphere": "Sphere", "cylinder": "Cylinder", "cone": "Cone"}
58
+ actor_class_lower = actor_class_name.lower()
59
+ if actor_class_lower in shape_map:
60
+ proper_name = shape_map[actor_class_lower]
61
+ actor = gen_actor_utils.spawn_basic_shape(proper_name, loc, rot, scale_vector, actor_label or "")
62
+ else:
63
+ actor = gen_actor_utils.spawn_actor_from_class(actor_class_name, loc, rot, scale_vector, actor_label or "")
64
+
65
+ if not actor:
66
+ unreal.log_error(f"Failed to spawn actor of type {actor_class_name}")
67
+ return {"success": False, "error": f"Failed to spawn actor of type {actor_class_name}"}
68
+
69
+ actor_name = actor.get_actor_label()
70
+ unreal.log(f"Spawned actor: {actor_name} at {loc}")
71
+ return {"success": True, "actor_name": actor_name}
72
+
73
+ except Exception as e:
74
+ unreal.log_error(f"Error spawning actor: {str(e)}")
75
+ return {"success": False, "error": str(e)}
76
+
77
+
78
+
79
+
80
+ def handle_take_screenshot(command):
81
+ """
82
+ Takes a screenshot using the HighResShot console command with a deterministic filename.
83
+ This is the most reliable method for engine-based screenshots.
84
+ """
85
+ # 1. Create a unique, absolute file path in the OS's temp directory.
86
+ # This ensures we have a clean place to work with guaranteed write permissions.
87
+ temp_dir = tempfile.gettempdir()
88
+ unique_filename = f"unreal_mcp_screenshot_{int(time.time())}.png"
89
+ # Use forward slashes, as this is more reliable for Unreal console commands
90
+ screenshot_path = os.path.join(temp_dir, unique_filename).replace('\\', '/')
91
+
92
+ try:
93
+ # 2. Construct the console command with explicit 640x480 resolution.
94
+ # This ensures a consistent, small file size suitable for MCP responses (<200KB typically)
95
+ console_command = f'HighResShot 640x480 filename="{screenshot_path}"'
96
+ unreal.log(f"Executing screenshot command: {console_command}")
97
+
98
+ # Execute the command in the editor world context
99
+ unreal.SystemLibrary.execute_console_command(unreal.EditorLevelLibrary.get_editor_world(), console_command)
100
+
101
+ # 3. Poll for the file's existence instead of using a fixed sleep time.
102
+ # This is much more reliable than a fixed wait.
103
+ max_wait_seconds = 5
104
+ wait_interval = 0.2
105
+ time_waited = 0
106
+ file_created = False
107
+ while time_waited < max_wait_seconds:
108
+ if os.path.exists(screenshot_path):
109
+ file_created = True
110
+ break
111
+ time.sleep(wait_interval)
112
+ time_waited += wait_interval
113
+
114
+ if not file_created:
115
+ return {"success": False, "error": f"Command was executed, but the output file was not found at the specified path: {screenshot_path}"}
116
+
117
+ # 4. Read the file, encode it, and prepare the response
118
+ with open(screenshot_path, 'rb') as image_file:
119
+ image_data = image_file.read()
120
+ base64_encoded_data = base64.b64encode(image_data).decode('utf-8')
121
+
122
+ return {
123
+ "success": True,
124
+ "data": base64_encoded_data,
125
+ "mime_type": "image/png"
126
+ }
127
+
128
+ except Exception as e:
129
+ return {"success": False, "error": f"The screenshot process failed with an exception: {str(e)}"}
130
+
131
+ finally:
132
+ # 5. Clean up the temporary screenshot file from the temp directory
133
+ if os.path.exists(screenshot_path):
134
+ try:
135
+ os.remove(screenshot_path)
136
+ except Exception as e_cleanup:
137
+ unreal.log_error(f"Failed to delete temporary screenshot file '{screenshot_path}': {e_cleanup}")
138
+
139
+ def handle_create_material(command: Dict[str, Any]) -> Dict[str, Any]:
140
+ """
141
+ Handle a create_material command
142
+
143
+ Args:
144
+ command: The command dictionary containing:
145
+ - material_name: Name for the new material
146
+ - color: [R, G, B] color values (0-1)
147
+
148
+ Returns:
149
+ Response dictionary with success/failure status and material path if successful
150
+ """
151
+ try:
152
+ # Extract parameters
153
+ material_name = command.get("material_name", "NewMaterial")
154
+ color = command.get("color", (1, 0, 0))
155
+
156
+ log.log_command("create_material", f"Name: {material_name}, Color: {color}")
157
+
158
+ # Use the C++ utility class
159
+ gen_actor_utils = unreal.GenActorUtils
160
+ color_linear = uc.to_unreal_color(color)
161
+
162
+ material = gen_actor_utils.create_material(material_name, color_linear)
163
+
164
+ if not material:
165
+ log.log_error("Failed to create material")
166
+ return {"success": False, "error": "Failed to create material"}
167
+
168
+ material_path = f"/Game/Materials/{material_name}"
169
+ log.log_result("create_material", True, f"Path: {material_path}")
170
+ return {"success": True, "material_path": material_path}
171
+
172
+ except Exception as e:
173
+ log.log_error(f"Error creating material: {str(e)}", include_traceback=True)
174
+ return {"success": False, "error": str(e)}
175
+
176
+
177
+ def handle_get_all_scene_objects(command: Dict[str, Any]) -> Dict[str, Any]:
178
+ """
179
+ Get all actors in the current level, categorized by type.
180
+ Returns a summary with categorized actors and example locations.
181
+ """
182
+ try:
183
+ # Use EditorActorSubsystem - the correct way to get actors in editor
184
+ editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
185
+ all_actors = editor_actor_subsystem.get_all_level_actors()
186
+
187
+ # Categorize actors by class
188
+ categories = {}
189
+
190
+ for actor in all_actors:
191
+ actor_class = actor.get_class().get_name()
192
+
193
+ if actor_class not in categories:
194
+ categories[actor_class] = []
195
+
196
+ location = actor.get_actor_location()
197
+ categories[actor_class].append({
198
+ 'name': actor.get_name(),
199
+ 'location': {
200
+ 'x': location.x,
201
+ 'y': location.y,
202
+ 'z': location.z
203
+ }
204
+ })
205
+
206
+ # Build summary
207
+ summary_lines = []
208
+ summary_lines.append("=" * 60)
209
+ summary_lines.append("SCENE SUMMARY")
210
+ summary_lines.append("=" * 60)
211
+ summary_lines.append(f"Total Actors: {len(all_actors)}\n")
212
+
213
+ # Sort categories by count (most common first)
214
+ sorted_categories = sorted(categories.items(), key=lambda x: len(x[1]), reverse=True)
215
+
216
+ for actor_class, actors in sorted_categories:
217
+ count = len(actors)
218
+ summary_lines.append(f"{actor_class}: {count}")
219
+
220
+ # Show first 2 examples for each category
221
+ for i, actor_info in enumerate(actors[:2]):
222
+ loc = actor_info['location']
223
+ summary_lines.append(f" โ””โ”€ {actor_info['name']} at ({loc['x']:.0f}, {loc['y']:.0f}, {loc['z']:.0f})")
224
+
225
+ if count > 2:
226
+ summary_lines.append(f" โ””โ”€ ... and {count - 2} more")
227
+ summary_lines.append("")
228
+
229
+ # Return both structured data and formatted summary
230
+ return {
231
+ "success": True,
232
+ "total_actors": len(all_actors),
233
+ "categories": categories,
234
+ "summary": "\n".join(summary_lines)
235
+ }
236
+
237
+ except Exception as e:
238
+ log.log_error(f"Error getting scene objects: {str(e)}", include_traceback=True)
239
+ return {"success": False, "error": str(e)}
240
+
241
+ def handle_create_project_folder(command: Dict[str, Any]) -> Dict[str, Any]:
242
+ try:
243
+ folder_path = command.get("folder_path")
244
+ full_path = f"/Game/{folder_path}"
245
+ unreal.EditorAssetLibrary.make_directory(full_path)
246
+ return {"success": True, "message": f"Created folder at {full_path}"}
247
+ except Exception as e:
248
+ return {"success": False, "error": str(e)}
249
+
250
+ def handle_get_files_in_folder(command: Dict[str, Any]) -> Dict[str, Any]:
251
+ try:
252
+ folder_path = f"/Game/{command.get('folder_path')}"
253
+ files = unreal.EditorAssetLibrary.list_assets(folder_path, recursive=False)
254
+ return {"success": True, "files": [str(f) for f in files]}
255
+ except Exception as e:
256
+ return {"success": False, "error": str(e)}
257
+
258
+ def handle_add_input_binding(command: Dict[str, Any]) -> Dict[str, Any]:
259
+ """
260
+ Add an input action binding to Project Settings.
261
+ Supports both legacy input system and provides guidance for Enhanced Input.
262
+
263
+ Args:
264
+ command: Dict containing 'action_name' and 'key'
265
+
266
+ Returns:
267
+ Dict with success/error and helpful instructions
268
+ """
269
+ try:
270
+ action_name = command.get("action_name")
271
+ key = command.get("key")
272
+
273
+ if not action_name or not key:
274
+ return {"success": False, "error": "Missing action_name or key parameter"}
275
+
276
+ # Try legacy input system first
277
+ input_settings = unreal.get_default_object(unreal.InputSettings)
278
+
279
+ if not input_settings:
280
+ return {
281
+ "success": False,
282
+ "error": "Could not access InputSettings",
283
+ "recommendation": "Use Enhanced Input System for UE 5.6+"
284
+ }
285
+
286
+ try:
287
+ # Create the key structure
288
+ key_mapping = unreal.InputActionKeyMapping()
289
+ key_mapping.action_name = action_name
290
+
291
+ # Try to create Key object
292
+ try:
293
+ key_obj = unreal.Key()
294
+ key_obj.key_name = unreal.Name(key)
295
+ key_mapping.key = key_obj
296
+ except:
297
+ # Fallback: try direct assignment
298
+ key_mapping.key = key
299
+
300
+ # Add the mapping to project settings
301
+ current_mappings = list(input_settings.get_editor_property('action_mappings'))
302
+ current_mappings.append(key_mapping)
303
+ input_settings.set_editor_property('action_mappings', current_mappings)
304
+
305
+ # Save the settings
306
+ input_settings.save_config()
307
+
308
+ return {
309
+ "success": True,
310
+ "message": f"Added input binding: {action_name} -> {key}",
311
+ "action_name": action_name,
312
+ "key": key,
313
+ "note": "Legacy input system used. Consider migrating to Enhanced Input System."
314
+ }
315
+
316
+ except AttributeError as e:
317
+ # Enhanced Input System is likely required
318
+ return {
319
+ "success": False,
320
+ "error": "Legacy input system not available in UE 5.6+",
321
+ "message": "Please use Enhanced Input System instead",
322
+ "instructions": {
323
+ "step1": "Use create_enhanced_input_action to create an Input Action asset",
324
+ "step2": "Use create_enhanced_input_mapping_context to create a Mapping Context",
325
+ "step3": "Open the Mapping Context in Content Browser and add key mappings",
326
+ "step4": "Reference the Input Action and Mapping Context in your Blueprint"
327
+ },
328
+ "alternative": "Use execute_python_script to create Enhanced Input assets programmatically",
329
+ "examples": {
330
+ "create_action": "create_enhanced_input_action('IA_Jump', '/Game/Input')",
331
+ "create_context": "create_enhanced_input_mapping_context('IMC_Default', '/Game/Input')"
332
+ }
333
+ }
334
+
335
+ except Exception as e:
336
+ return {
337
+ "success": False,
338
+ "error": f"Unexpected error: {str(e)}",
339
+ "recommendation": "Check that action_name and key are valid strings"
340
+ }
341
+
342
+ def handle_create_enhanced_input_action(command: Dict[str, Any]) -> Dict[str, Any]:
343
+ """
344
+ Create an Enhanced Input Action asset.
345
+
346
+ Args:
347
+ command: Dict containing:
348
+ - action_name: Name of the action (e.g., "IA_Jump")
349
+ - action_path: Path to save the asset (default: "/Game/Input")
350
+ - value_type: "BOOLEAN", "AXIS_1D", "AXIS_2D", "AXIS_3D" (default: "BOOLEAN")
351
+
352
+ Returns:
353
+ Dict with asset path or error
354
+ """
355
+ try:
356
+ action_name = command.get("action_name")
357
+ action_path = command.get("action_path", "/Game/Input")
358
+ value_type_str = command.get("value_type", "BOOLEAN")
359
+
360
+ if not action_name:
361
+ return {"success": False, "error": "Missing action_name parameter"}
362
+
363
+ # Map string to enum
364
+ value_type_map = {
365
+ "BOOLEAN": unreal.InputActionValueType.BOOLEAN,
366
+ "AXIS_1D": unreal.InputActionValueType.AXIS1_D,
367
+ "AXIS_2D": unreal.InputActionValueType.AXIS2_D,
368
+ "AXIS_3D": unreal.InputActionValueType.AXIS3_D,
369
+ }
370
+
371
+ value_type = value_type_map.get(value_type_str.upper(), unreal.InputActionValueType.BOOLEAN)
372
+
373
+ # Create Input Action asset
374
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
375
+
376
+ # Create factory for InputAction
377
+ factory = unreal.Factory.static_class().get_default_object()
378
+
379
+ # Use EditorAssetLibrary to create the asset
380
+ full_path = f"{action_path}/{action_name}"
381
+
382
+ # Check if asset already exists
383
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
384
+ return {
385
+ "success": False,
386
+ "error": f"Asset already exists at {full_path}",
387
+ "suggestion": "Choose a different name or delete the existing asset"
388
+ }
389
+
390
+ # Create the asset using AssetTools
391
+ input_action = asset_tools.create_asset(
392
+ action_name,
393
+ action_path,
394
+ unreal.InputAction,
395
+ None # Factory (None uses default)
396
+ )
397
+
398
+ if input_action:
399
+ # Set properties
400
+ input_action.set_editor_property('value_type', value_type)
401
+
402
+ # Save the asset
403
+ unreal.EditorAssetLibrary.save_loaded_asset(input_action)
404
+
405
+ return {
406
+ "success": True,
407
+ "message": f"Created Enhanced Input Action: {action_name}",
408
+ "asset_path": full_path,
409
+ "value_type": value_type_str
410
+ }
411
+ else:
412
+ return {
413
+ "success": False,
414
+ "error": "Failed to create Input Action asset",
415
+ "recommendation": "Ensure Enhanced Input Plugin is enabled in Project Settings"
416
+ }
417
+
418
+ except Exception as e:
419
+ return {
420
+ "success": False,
421
+ "error": f"Error creating Input Action: {str(e)}",
422
+ "recommendation": "Ensure Enhanced Input Plugin is enabled and action_path exists"
423
+ }
424
+
425
+ def handle_create_enhanced_input_mapping_context(command: Dict[str, Any]) -> Dict[str, Any]:
426
+ """
427
+ Create an Enhanced Input Mapping Context asset.
428
+
429
+ Args:
430
+ command: Dict containing:
431
+ - context_name: Name of the mapping context (e.g., "IMC_Default")
432
+ - context_path: Path to save the asset (default: "/Game/Input")
433
+
434
+ Returns:
435
+ Dict with asset path or error
436
+ """
437
+ try:
438
+ context_name = command.get("context_name")
439
+ context_path = command.get("context_path", "/Game/Input")
440
+
441
+ if not context_name:
442
+ return {"success": False, "error": "Missing context_name parameter"}
443
+
444
+ # Create Input Mapping Context asset
445
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
446
+
447
+ full_path = f"{context_path}/{context_name}"
448
+
449
+ # Check if asset already exists
450
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
451
+ return {
452
+ "success": False,
453
+ "error": f"Asset already exists at {full_path}",
454
+ "suggestion": "Choose a different name or delete the existing asset"
455
+ }
456
+
457
+ # Create the asset
458
+ mapping_context = asset_tools.create_asset(
459
+ context_name,
460
+ context_path,
461
+ unreal.InputMappingContext,
462
+ None # Factory (None uses default)
463
+ )
464
+
465
+ if mapping_context:
466
+ # Save the asset
467
+ unreal.EditorAssetLibrary.save_loaded_asset(mapping_context)
468
+
469
+ return {
470
+ "success": True,
471
+ "message": f"Created Enhanced Input Mapping Context: {context_name}",
472
+ "asset_path": full_path,
473
+ "next_step": "Open the asset in Content Browser and add key mappings manually"
474
+ }
475
+ else:
476
+ return {
477
+ "success": False,
478
+ "error": "Failed to create Mapping Context asset",
479
+ "recommendation": "Ensure Enhanced Input Plugin is enabled in Project Settings"
480
+ }
481
+
482
+ except Exception as e:
483
+ return {
484
+ "success": False,
485
+ "error": f"Error creating Mapping Context: {str(e)}",
486
+ "recommendation": "Ensure Enhanced Input Plugin is enabled and context_path exists"
487
+ }
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/blueprint_commands.py ADDED
@@ -0,0 +1,874 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unreal
2
+ import json
3
+ import time
4
+ from typing import Dict, Any, List, Tuple, Union, Optional
5
+
6
+ from utils import unreal_conversions as uc
7
+ from utils import logging as log
8
+ from utils import asset_validation as av
9
+
10
+
11
+ def handle_create_blueprint(command: Dict[str, Any]) -> Dict[str, Any]:
12
+ """
13
+ Handle a command to create a new Blueprint from a specified parent class
14
+
15
+ Args:
16
+ command: The command dictionary containing:
17
+ - blueprint_name: Name for the new Blueprint
18
+ - parent_class: Parent class name or path (e.g., "Actor", "/Script/Engine.Actor")
19
+ - save_path: Path to save the Blueprint asset (e.g., "/Game/Blueprints")
20
+
21
+ Returns:
22
+ Response dictionary with success/failure status and the Blueprint path if successful
23
+ """
24
+ try:
25
+ blueprint_name = command.get("blueprint_name", "NewBlueprint")
26
+ parent_class = command.get("parent_class", "Actor")
27
+ save_path = command.get("save_path", "/Game/Blueprints")
28
+
29
+ log.log_command("create_blueprint", f"Name: {blueprint_name}, Parent: {parent_class}")
30
+
31
+ # Call the C++ implementation
32
+ gen_bp_utils = unreal.GenBlueprintUtils
33
+ blueprint = gen_bp_utils.create_blueprint(blueprint_name, parent_class, save_path)
34
+
35
+ if blueprint:
36
+ blueprint_path = f"{save_path}/{blueprint_name}"
37
+
38
+ # FIX: Save asset and wait for it to be ready (LM_STUDIO_DIAGNOSTIC_REPORT.md)
39
+ unreal.EditorAssetLibrary.save_asset(blueprint_path)
40
+ time.sleep(0.5) # Wait for save to complete
41
+
42
+ # Verify asset is accessible
43
+ if not av.wait_for_asset_save(blueprint_path, timeout=2.0):
44
+ log.log_error(f"Blueprint created but not yet accessible: {blueprint_path}")
45
+ return {"success": False, "error": f"Blueprint created but timed out waiting for asset save: {blueprint_path}"}
46
+
47
+ log.log_result("create_blueprint", True, f"Path: {blueprint_path}")
48
+ return {"success": True, "blueprint_path": blueprint_path}
49
+ else:
50
+ log.log_error(f"Failed to create Blueprint {blueprint_name}")
51
+ return {"success": False, "error": f"Failed to create Blueprint {blueprint_name}"}
52
+
53
+ except Exception as e:
54
+ log.log_error(f"Error creating blueprint: {str(e)}", include_traceback=True)
55
+ return {"success": False, "error": str(e)}
56
+
57
+
58
+ def handle_add_component(command: Dict[str, Any]) -> Dict[str, Any]:
59
+ """
60
+ Handle a command to add a component to a Blueprint
61
+
62
+ Args:
63
+ command: The command dictionary containing:
64
+ - blueprint_path: Path to the Blueprint asset
65
+ - component_class: Component class to add (e.g., "StaticMeshComponent")
66
+ - component_name: Name for the new component
67
+
68
+ Returns:
69
+ Response dictionary with success/failure status
70
+ """
71
+ try:
72
+ blueprint_path = command.get("blueprint_path")
73
+ component_class = command.get("component_class")
74
+ component_name = command.get("component_name")
75
+
76
+ if not blueprint_path or not component_class:
77
+ log.log_error("Missing required parameters for add_component")
78
+ return {"success": False, "error": "Missing required parameters"}
79
+
80
+ log.log_command("add_component", f"Blueprint: {blueprint_path}, Component: {component_class}")
81
+
82
+ # Call the C++ implementation
83
+ gen_bp_utils = unreal.GenBlueprintUtils
84
+ success = gen_bp_utils.add_component(blueprint_path, component_class, component_name or "")
85
+
86
+ if success:
87
+ log.log_result("add_component", True, f"Added {component_class} to {blueprint_path}")
88
+ return {"success": True}
89
+ else:
90
+ log.log_error(f"Failed to add component {component_class} to {blueprint_path}")
91
+ return {"success": False, "error": f"Failed to add component {component_class} to {blueprint_path}"}
92
+
93
+ except Exception as e:
94
+ log.log_error(f"Error adding component: {str(e)}", include_traceback=True)
95
+ return {"success": False, "error": str(e)}
96
+
97
+
98
+ def handle_add_variable(command: Dict[str, Any]) -> Dict[str, Any]:
99
+ """
100
+ Handle a command to add a variable to a Blueprint
101
+
102
+ Args:
103
+ command: The command dictionary containing:
104
+ - blueprint_path: Path to the Blueprint asset
105
+ - variable_name: Name for the new variable
106
+ - variable_type: Type of the variable (e.g., "float", "vector", "boolean")
107
+ - default_value: Default value for the variable (optional)
108
+ - category: Category for organizing variables in the Blueprint editor (optional)
109
+
110
+ Returns:
111
+ Response dictionary with success/failure status
112
+ """
113
+ try:
114
+ blueprint_path = command.get("blueprint_path")
115
+ variable_name = command.get("variable_name")
116
+ variable_type = command.get("variable_type")
117
+ default_value = command.get("default_value", "")
118
+ category = command.get("category", "Default")
119
+
120
+ if not blueprint_path or not variable_name or not variable_type:
121
+ log.log_error("Missing required parameters for add_variable")
122
+ return {"success": False, "error": "Missing required parameters"}
123
+
124
+ log.log_command("add_variable",
125
+ f"Blueprint: {blueprint_path}, Variable: {variable_name}, Type: {variable_type}")
126
+
127
+ # Call the C++ implementation
128
+ gen_bp_utils = unreal.GenBlueprintUtils
129
+ success = gen_bp_utils.add_variable(blueprint_path, variable_name, variable_type, str(default_value), category)
130
+
131
+ if success:
132
+ log.log_result("add_variable", True, f"Added {variable_type} variable {variable_name} to {blueprint_path}")
133
+ return {"success": True}
134
+ else:
135
+ log.log_error(f"Failed to add variable {variable_name} to {blueprint_path}")
136
+ return {"success": False, "error": f"Failed to add variable {variable_name} to {blueprint_path}"}
137
+
138
+ except Exception as e:
139
+ log.log_error(f"Error adding variable: {str(e)}", include_traceback=True)
140
+ return {"success": False, "error": str(e)}
141
+
142
+
143
+ def handle_add_function(command: Dict[str, Any]) -> Dict[str, Any]:
144
+ """
145
+ Handle a command to add a function to a Blueprint
146
+
147
+ Args:
148
+ command: The command dictionary containing:
149
+ - blueprint_path: Path to the Blueprint asset
150
+ - function_name: Name for the new function
151
+ - inputs: List of input parameters [{"name": "param1", "type": "float"}, ...] (optional)
152
+ - outputs: List of output parameters (optional)
153
+
154
+ Returns:
155
+ Response dictionary with success/failure status and the function ID if successful
156
+ """
157
+ try:
158
+ blueprint_path = command.get("blueprint_path")
159
+ function_name = command.get("function_name")
160
+ inputs = command.get("inputs", [])
161
+ outputs = command.get("outputs", [])
162
+
163
+ if not blueprint_path or not function_name:
164
+ log.log_error("Missing required parameters for add_function")
165
+ return {"success": False, "error": "Missing required parameters"}
166
+
167
+ log.log_command("add_function", f"Blueprint: {blueprint_path}, Function: {function_name}")
168
+
169
+ # Convert inputs and outputs to JSON strings for C++ function
170
+ inputs_json = json.dumps(inputs)
171
+ outputs_json = json.dumps(outputs)
172
+
173
+ # Call the C++ implementation
174
+ gen_bp_utils = unreal.GenBlueprintUtils
175
+ function_id = gen_bp_utils.add_function(blueprint_path, function_name, inputs_json, outputs_json)
176
+
177
+ if function_id:
178
+ log.log_result("add_function", True,
179
+ f"Added function {function_name} to {blueprint_path} with ID: {function_id}")
180
+ return {"success": True, "function_id": function_id}
181
+ else:
182
+ log.log_error(f"Failed to add function {function_name} to {blueprint_path}")
183
+ return {"success": False, "error": f"Failed to add function {function_name} to {blueprint_path}"}
184
+
185
+ except Exception as e:
186
+ log.log_error(f"Error adding function: {str(e)}", include_traceback=True)
187
+ return {"success": False, "error": str(e)}
188
+
189
+
190
+ def handle_add_node(command: Dict[str, Any]) -> Dict[str, Any]:
191
+ """
192
+ Handle a command to add any type of node to a Blueprint graph
193
+
194
+ Args:
195
+ command: The command dictionary containing:
196
+ - blueprint_path: Path to the Blueprint asset
197
+ - function_id: ID of the function to add the node to
198
+ - node_type: Type of node to add - can be any of:
199
+ * Function name (e.g. "K2_SetActorLocation")
200
+ * Node class name (e.g. "Branch", "Sequence", "ForLoop")
201
+ * Full node class path (e.g. "K2Node_IfThenElse")
202
+ - node_position: Position of the node in the graph [X, Y]
203
+ - node_properties: Dictionary of properties to set on the node (optional)
204
+ * Can include pin values, node settings, etc.
205
+ - target_class: Optional class to use for function calls (default: "Actor")
206
+
207
+ Returns:
208
+ Response dictionary with success/failure status and the node ID if successful
209
+ """
210
+ try:
211
+ blueprint_path = command.get("blueprint_path")
212
+ function_id = command.get("function_id")
213
+ node_type = command.get("node_type")
214
+ node_position = command.get("node_position", [0, 0])
215
+ node_properties = command.get("node_properties", {})
216
+
217
+ if not blueprint_path or not function_id or not node_type:
218
+ log.log_error("Missing required parameters for add_node")
219
+ return {"success": False, "error": "Missing required parameters"}
220
+
221
+ log.log_command("add_node", f"Blueprint: {blueprint_path}, Node: {node_type}")
222
+
223
+ # Convert node properties to JSON for C++ function
224
+ node_properties_json = json.dumps(node_properties)
225
+
226
+ # Call the C++ implementation from UGenBlueprintNodeCreator
227
+ node_creator = unreal.GenBlueprintNodeCreator
228
+ node_id = node_creator.add_node(blueprint_path, function_id, node_type,
229
+ node_position[0], node_position[1],
230
+ node_properties_json)
231
+
232
+ if node_id:
233
+ log.log_result("add_node", True, f"Added node {node_type} to {blueprint_path} with ID: {node_id}")
234
+ return {"success": True, "node_id": node_id}
235
+ else:
236
+ log.log_error(f"Failed to add node {node_type} to {blueprint_path}")
237
+ return {"success": False, "error": f"Failed to add node {node_type} to {blueprint_path}"}
238
+
239
+ except Exception as e:
240
+ log.log_error(f"Error adding node: {str(e)}", include_traceback=True)
241
+ return {"success": False, "error": str(e)}
242
+
243
+
244
+ def handle_connect_nodes(command: Dict[str, Any]) -> Dict[str, Any]:
245
+ try:
246
+ blueprint_path = command.get("blueprint_path")
247
+ function_id = command.get("function_id")
248
+ source_node_id = command.get("source_node_id")
249
+ source_pin = command.get("source_pin")
250
+ target_node_id = command.get("target_node_id")
251
+ target_pin = command.get("target_pin")
252
+
253
+ if not all([blueprint_path, function_id, source_node_id, source_pin, target_node_id, target_pin]):
254
+ log.log_error("Missing required parameters for connect_nodes")
255
+ return {"success": False, "error": "Missing required parameters"}
256
+
257
+ log.log_command("connect_nodes",
258
+ f"Blueprint: {blueprint_path}, {source_node_id}.{source_pin} -> {target_node_id}.{target_pin}")
259
+
260
+ gen_bp_utils = unreal.GenBlueprintUtils
261
+ result_json = gen_bp_utils.connect_nodes(blueprint_path, function_id,
262
+ source_node_id, source_pin,
263
+ target_node_id, target_pin)
264
+ result = json.loads(result_json)
265
+
266
+ if result.get("success"):
267
+ log.log_result("connect_nodes", True, f"Connected nodes in {blueprint_path}")
268
+ return {"success": True}
269
+ else:
270
+ log.log_error(f"Failed to connect nodes: {result.get('error')}")
271
+ return result # Pass through the detailed response with available pins
272
+
273
+ except Exception as e:
274
+ log.log_error(f"Error connecting nodes: {str(e)}", include_traceback=True)
275
+ return {"success": False, "error": str(e)}
276
+
277
+
278
+ def handle_compile_blueprint(command: Dict[str, Any]) -> Dict[str, Any]:
279
+ """
280
+ Handle a command to compile a Blueprint
281
+
282
+ Args:
283
+ command: The command dictionary containing:
284
+ - blueprint_path: Path to the Blueprint asset
285
+
286
+ Returns:
287
+ Response dictionary with success/failure status
288
+ """
289
+ try:
290
+ blueprint_path = command.get("blueprint_path")
291
+
292
+ if not blueprint_path:
293
+ log.log_error("Missing required parameters for compile_blueprint")
294
+ return {"success": False, "error": "Missing required parameters"}
295
+
296
+ log.log_command("compile_blueprint", f"Blueprint: {blueprint_path}")
297
+
298
+ # Call the C++ implementation
299
+ gen_bp_utils = unreal.GenBlueprintUtils
300
+ success = gen_bp_utils.compile_blueprint(blueprint_path)
301
+
302
+ if success:
303
+ log.log_result("compile_blueprint", True, f"Compiled blueprint: {blueprint_path}")
304
+ return {"success": True}
305
+ else:
306
+ log.log_error(f"Failed to compile blueprint: {blueprint_path}")
307
+ return {"success": False, "error": f"Failed to compile blueprint: {blueprint_path}"}
308
+
309
+ except Exception as e:
310
+ log.log_error(f"Error compiling blueprint: {str(e)}", include_traceback=True)
311
+ return {"success": False, "error": str(e)}
312
+
313
+
314
+ def handle_spawn_blueprint(command: Dict[str, Any]) -> Dict[str, Any]:
315
+ """
316
+ Handle a command to spawn a Blueprint actor in the level
317
+
318
+ Args:
319
+ command: The command dictionary containing:
320
+ - blueprint_path: Path to the Blueprint asset
321
+ - location: [X, Y, Z] coordinates (optional)
322
+ - rotation: [Pitch, Yaw, Roll] in degrees (optional)
323
+ - scale: [X, Y, Z] scale factors (optional)
324
+ - actor_label: Optional custom name for the actor
325
+
326
+ Returns:
327
+ Response dictionary with success/failure status and the actor name if successful
328
+ """
329
+ try:
330
+ blueprint_path = command.get("blueprint_path")
331
+ location = command.get("location", (0, 0, 0))
332
+ rotation = command.get("rotation", (0, 0, 0))
333
+ scale = command.get("scale", (1, 1, 1))
334
+ actor_label = command.get("actor_label", "")
335
+
336
+ if not blueprint_path:
337
+ log.log_error("Missing required parameters for spawn_blueprint")
338
+ return {"success": False, "error": "Missing required parameters"}
339
+
340
+ # FIX: Validate asset exists before spawning (LM_STUDIO_DIAGNOSTIC_REPORT.md)
341
+ normalized_path = av.normalize_blueprint_path(blueprint_path)
342
+ validation_result = av.validate_asset_with_suggestion(normalized_path)
343
+
344
+ if not validation_result['valid']:
345
+ log.log_error(f"Blueprint validation failed: {validation_result['message']}")
346
+ return {
347
+ "success": False,
348
+ "error": validation_result['message'],
349
+ "suggestion": validation_result.get('suggestion')
350
+ }
351
+
352
+ log.log_command("spawn_blueprint", f"Blueprint: {normalized_path}, Label: {actor_label}")
353
+
354
+ # Convert to Unreal types
355
+ loc = uc.to_unreal_vector(location)
356
+ rot = uc.to_unreal_rotator(rotation)
357
+ scale_vec = uc.to_unreal_vector(scale)
358
+
359
+ # Call the C++ implementation
360
+ gen_bp_utils = unreal.GenBlueprintUtils
361
+ actor = gen_bp_utils.spawn_blueprint(blueprint_path, loc, rot, scale_vec, actor_label)
362
+
363
+ if actor:
364
+ actor_name = actor.get_actor_label()
365
+ log.log_result("spawn_blueprint", True, f"Spawned blueprint: {blueprint_path} as {actor_name}")
366
+ return {"success": True, "actor_name": actor_name}
367
+ else:
368
+ log.log_error(f"Failed to spawn blueprint: {blueprint_path}")
369
+ return {"success": False, "error": f"Failed to spawn blueprint: {blueprint_path}"}
370
+
371
+ except Exception as e:
372
+ log.log_error(f"Error spawning blueprint: {str(e)}", include_traceback=True)
373
+ return {"success": False, "error": str(e)}
374
+
375
+ def handle_add_nodes_bulk(command: Dict[str, Any]) -> Dict[str, Any]:
376
+ """
377
+ Handle a command to add multiple nodes to a Blueprint graph in a single operation
378
+
379
+ Args:
380
+ command: The command dictionary containing:
381
+ - blueprint_path: Path to the Blueprint asset
382
+ - function_id: ID of the function to add the nodes to
383
+ - nodes: Array of node definitions, each containing:
384
+ * id: Optional ID for referencing the node (string)
385
+ * node_type: Type of node to add (string)
386
+ * node_position: Position of the node in the graph [X, Y]
387
+ * node_properties: Properties to set on the node (optional)
388
+
389
+ Returns:
390
+ Response dictionary with success/failure status and node IDs mapped to reference IDs
391
+ """
392
+
393
+ try:
394
+ blueprint_path = command.get("blueprint_path")
395
+ function_id = command.get("function_id")
396
+ nodes = command.get("nodes", [])
397
+
398
+ if not blueprint_path or not function_id or not nodes:
399
+ log.log_error("Missing required parameters for add_nodes_bulk")
400
+ return {"success": False, "error": "Missing required parameters"}
401
+
402
+ log.log_command("add_nodes_bulk", f"Blueprint: {blueprint_path}, Adding {len(nodes)} nodes")
403
+
404
+ # Prepare nodes in the format expected by the C++ function
405
+ nodes_json = json.dumps(nodes)
406
+
407
+ # Call the C++ implementation from UGenBlueprintNodeCreator
408
+ node_creator = unreal.GenBlueprintNodeCreator
409
+ results_json = node_creator.add_nodes_bulk(blueprint_path, function_id, nodes_json)
410
+
411
+ if results_json:
412
+ results = json.loads(results_json)
413
+ node_mapping = {}
414
+
415
+ # Create a mapping from reference IDs to actual node GUIDs
416
+ for node_result in results:
417
+ if "ref_id" in node_result:
418
+ node_mapping[node_result["ref_id"]] = node_result["node_guid"]
419
+ else:
420
+ # For nodes without a reference ID, just include the GUID
421
+ node_mapping[f"node_{len(node_mapping)}"] = node_result["node_guid"]
422
+
423
+ log.log_result("add_nodes_bulk", True, f"Added {len(results)} nodes to {blueprint_path}")
424
+ return {"success": True, "nodes": node_mapping}
425
+ else:
426
+ log.log_error(f"Failed to add nodes to {blueprint_path}")
427
+ return {"success": False, "error": f"Failed to add nodes to {blueprint_path}"}
428
+
429
+ except Exception as e:
430
+ log.log_error(f"Error adding nodes: {str(e)}", include_traceback=True)
431
+ return {"success": False, "error": str(e)}
432
+
433
+
434
+ def handle_connect_nodes_bulk(command: Dict[str, Any]) -> Dict[str, Any]:
435
+ """
436
+ Handle a command to connect multiple pairs of nodes in a Blueprint graph
437
+
438
+ Args:
439
+ command: The command dictionary containing:
440
+ - blueprint_path: Path to the Blueprint asset
441
+ - function_id: ID of the function containing the nodes
442
+ - connections: Array of connection definitions, each containing:
443
+ * source_node_id: ID of the source node
444
+ * source_pin: Name of the source pin
445
+ * target_node_id: ID of the target node
446
+ * target_pin: Name of the target pin
447
+
448
+ Returns:
449
+ Response dictionary with detailed connection results
450
+ """
451
+ try:
452
+ blueprint_path = command.get("blueprint_path")
453
+ function_id = command.get("function_id")
454
+ connections = command.get("connections", [])
455
+
456
+ if not blueprint_path or not function_id or not connections:
457
+ log.log_error("Missing required parameters for connect_nodes_bulk")
458
+ return {"success": False, "error": "Missing required parameters"}
459
+
460
+ log.log_command("connect_nodes_bulk", f"Blueprint: {blueprint_path}, Making {len(connections)} connections")
461
+
462
+ # Convert connections list to JSON for C++ function
463
+ connections_json = json.dumps(connections)
464
+
465
+ # Call the C++ implementation - now returns a JSON string instead of boolean
466
+ gen_bp_utils = unreal.GenBlueprintUtils
467
+ result_json = gen_bp_utils.connect_nodes_bulk(blueprint_path, function_id, connections_json)
468
+
469
+ # Parse the JSON result
470
+ try:
471
+ result_data = json.loads(result_json)
472
+ log.log_result("connect_nodes_bulk", result_data.get("success", False),
473
+ f"Connected {result_data.get('successful_connections', 0)}/{result_data.get('total_connections', 0)} node pairs in {blueprint_path}")
474
+
475
+ # Return the full result data for detailed error reporting
476
+ return result_data
477
+ except json.JSONDecodeError:
478
+ log.log_error(f"Failed to parse JSON result from connect_nodes_bulk: {result_json}")
479
+ return {"success": False, "error": "Failed to parse connection results"}
480
+
481
+ except Exception as e:
482
+ log.log_error(f"Error connecting nodes: {str(e)}", include_traceback=True)
483
+ return {"success": False, "error": str(e)}
484
+
485
+ def handle_delete_node(command: Dict[str, Any]) -> Dict[str, Any]:
486
+ """
487
+ Handle a command to delete a node from a Blueprint graph
488
+
489
+ Args:
490
+ command: The command dictionary containing:
491
+ - blueprint_path: Path to the Blueprint asset
492
+ - function_id: ID of the function containing the node
493
+ - node_id: ID of the node to delete
494
+
495
+ Returns:
496
+ Response dictionary with success/failure status
497
+ """
498
+ try:
499
+ blueprint_path = command.get("blueprint_path")
500
+ function_id = command.get("function_id")
501
+ node_id = command.get("node_id")
502
+
503
+ if not blueprint_path or not function_id or not node_id:
504
+ log.log_error("Missing required parameters for delete_node")
505
+ return {"success": False, "error": "Missing required parameters"}
506
+
507
+ log.log_command("delete_node", f"Blueprint: {blueprint_path}, Node ID: {node_id}")
508
+
509
+ # Call the C++ implementation from UGenBlueprintNodeCreator
510
+ node_creator = unreal.GenBlueprintNodeCreator
511
+ success = node_creator.delete_node(blueprint_path, function_id, node_id)
512
+
513
+ if success:
514
+ log.log_result("delete_node", True, f"Deleted node {node_id} from {blueprint_path}")
515
+ return {"success": True}
516
+ else:
517
+ log.log_error(f"Failed to delete node {node_id} from {blueprint_path}")
518
+ return {"success": False, "error": f"Failed to delete node {node_id}"}
519
+
520
+ except Exception as e:
521
+ log.log_error(f"Error deleting node: {str(e)}", include_traceback=True)
522
+ return {"success": False, "error": str(e)}
523
+
524
+
525
+ def handle_get_all_nodes(command: Dict[str, Any]) -> Dict[str, Any]:
526
+ """
527
+ Handle a command to get all nodes in a Blueprint graph
528
+
529
+ Args:
530
+ command: The command dictionary containing:
531
+ - blueprint_path: Path to the Blueprint asset
532
+ - function_id: ID of the function to get nodes from
533
+
534
+ Returns:
535
+ Response dictionary with success/failure status and a list of nodes with their details
536
+ """
537
+ try:
538
+ blueprint_path = command.get("blueprint_path")
539
+ function_id = command.get("function_id")
540
+
541
+ if not blueprint_path or not function_id:
542
+ log.log_error("Missing required parameters for get_all_nodes")
543
+ return {"success": False, "error": "Missing required parameters"}
544
+
545
+ log.log_command("get_all_nodes", f"Blueprint: {blueprint_path}, Function ID: {function_id}")
546
+
547
+ # Call the C++ implementation from UGenBlueprintNodeCreator
548
+ node_creator = unreal.GenBlueprintNodeCreator
549
+ nodes_json = node_creator.get_all_nodes_in_graph(blueprint_path, function_id)
550
+
551
+ if nodes_json:
552
+ # Parse the JSON response
553
+ try:
554
+ nodes = json.loads(nodes_json)
555
+ log.log_result("get_all_nodes", True, f"Retrieved {len(nodes)} nodes from {blueprint_path}")
556
+ return {"success": True, "nodes": nodes}
557
+ except json.JSONDecodeError as e:
558
+ log.log_error(f"Error parsing nodes JSON: {str(e)}")
559
+ return {"success": False, "error": f"Error parsing nodes JSON: {str(e)}"}
560
+ else:
561
+ log.log_error(f"Failed to get nodes from {blueprint_path}")
562
+ return {"success": False, "error": "Failed to get nodes"}
563
+
564
+ except Exception as e:
565
+ log.log_error(f"Error getting nodes: {str(e)}", include_traceback=True)
566
+ return {"success": False, "error": str(e)}
567
+
568
+ def handle_get_node_suggestions(command: Dict[str, Any]) -> Dict[str, Any]:
569
+ """
570
+ Handle a command to get suggestions for a node type in Unreal Blueprints
571
+
572
+ Args:
573
+ command: The command dictionary containing:
574
+ - node_type: The partial or full node type to get suggestions for (e.g., "Add", "FloatToDouble")
575
+
576
+ Returns:
577
+ Response dictionary with success/failure status and a list of suggested node types
578
+ """
579
+ try:
580
+ node_type = command.get("node_type")
581
+
582
+ if not node_type:
583
+ log.log_error("Missing required parameter 'node_type' for get_node_suggestions")
584
+ return {"success": False, "error": "Missing required parameter 'node_type'"}
585
+
586
+ log.log_command("get_node_suggestions", f"Node Type: {node_type}")
587
+
588
+ # Call the C++ implementation from UGenBlueprintNodeCreator
589
+ node_creator = unreal.GenBlueprintNodeCreator
590
+ suggestions_result = node_creator.get_node_suggestions(node_type)
591
+
592
+ if suggestions_result:
593
+ if suggestions_result.startswith("SUGGESTIONS:"):
594
+ suggestions = suggestions_result[len("SUGGESTIONS:"):].split(", ")
595
+ log.log_result("get_node_suggestions", True, f"Retrieved {len(suggestions)} suggestions for {node_type}")
596
+ return {"success": True, "suggestions": suggestions}
597
+ else:
598
+ log.log_error(f"Unexpected response format from get_node_suggestions: {suggestions_result}")
599
+ return {"success": False, "error": "Unexpected response format from Unreal"}
600
+ else:
601
+ log.log_result("get_node_suggestions", False, f"No suggestions found for {node_type}")
602
+ return {"success": True, "suggestions": []} # Empty list for no matches
603
+
604
+ except Exception as e:
605
+ log.log_error(f"Error getting node suggestions: {str(e)}", include_traceback=True)
606
+ return {"success": False, "error": str(e)}
607
+
608
+ def _find_event_node_by_pattern(blueprint_path: str, node_name: str) -> str:
609
+ """
610
+ Helper function to find event nodes by pattern matching.
611
+ Handles cases like "BeginPlay" matching "Event BeginPlay" or "ReceiveBeginPlay".
612
+
613
+ Args:
614
+ blueprint_path: Path to the Blueprint
615
+ node_name: Simple name to search for (e.g., "BeginPlay")
616
+
617
+ Returns:
618
+ Node GUID as string, or empty string if not found
619
+ """
620
+ try:
621
+ blueprint = unreal.load_asset(blueprint_path)
622
+ if not blueprint:
623
+ return ""
624
+
625
+ # Get the event graph (ubergraph)
626
+ ubergraph_pages = blueprint.get_editor_property('ubergraph_pages')
627
+ if not ubergraph_pages or len(ubergraph_pages) == 0:
628
+ return ""
629
+
630
+ # Search through event graph nodes
631
+ for graph in ubergraph_pages:
632
+ all_nodes = graph.get_editor_property('nodes')
633
+ if not all_nodes:
634
+ continue
635
+
636
+ # Normalize search term (lowercase, no spaces)
637
+ search_term = node_name.lower().replace(" ", "").replace("event", "").replace("receive", "")
638
+
639
+ for node in all_nodes:
640
+ # Get node class name (e.g., "K2Node_Event")
641
+ node_class = node.__class__.__name__
642
+
643
+ # For event nodes, check if it's an event node type
644
+ if "Event" in node_class or "K2Node" in node_class:
645
+ # Try to get the event name from the node
646
+ try:
647
+ # Different event nodes store their name differently
648
+ event_name = ""
649
+ if hasattr(node, 'get_editor_property'):
650
+ # Try EventReference.MemberName for event nodes
651
+ try:
652
+ event_ref = node.get_editor_property('event_reference')
653
+ if event_ref:
654
+ event_name = str(event_ref.get_editor_property('member_name'))
655
+ except:
656
+ pass
657
+
658
+ # Try CustomFunctionName
659
+ if not event_name:
660
+ try:
661
+ event_name = str(node.get_editor_property('custom_function_name'))
662
+ except:
663
+ pass
664
+
665
+ # Normalize event name for comparison
666
+ if event_name:
667
+ normalized_event = event_name.lower().replace(" ", "").replace("event", "").replace("receive", "")
668
+
669
+ # Check if search term matches
670
+ if search_term in normalized_event or normalized_event in search_term:
671
+ node_guid = str(node.get_editor_property('node_guid'))
672
+ log.log_result("_find_event_node_by_pattern", True,
673
+ f"Found event node '{event_name}' matching '{node_name}' with GUID {node_guid}")
674
+ return node_guid
675
+ except Exception as e:
676
+ # Skip nodes that don't have the expected properties
677
+ continue
678
+
679
+ return ""
680
+
681
+ except Exception as e:
682
+ log.log_warning(f"Error in pattern-based event node search: {str(e)}")
683
+ return ""
684
+
685
+ def handle_get_node_guid(command: Dict[str, Any]) -> Dict[str, Any]:
686
+ """
687
+ Handle a command to retrieve the GUID of a pre-existing node in a Blueprint graph.
688
+
689
+ Args:
690
+ command: The command dictionary containing:
691
+ - blueprint_path: Path to the Blueprint asset
692
+ - graph_type: "EventGraph" or "FunctionGraph"
693
+ - node_name: Name of the node (e.g., "BeginPlay") for EventGraph
694
+ - function_id: ID of the function for FunctionGraph to get FunctionEntry
695
+
696
+ Returns:
697
+ Response dictionary with the node's GUID or an error
698
+ """
699
+ try:
700
+ blueprint_path = command.get("blueprint_path")
701
+ graph_type = command.get("graph_type", "EventGraph")
702
+ node_name = command.get("node_name", "")
703
+ function_id = command.get("function_id", "")
704
+
705
+ if not blueprint_path:
706
+ log.log_error("Missing blueprint_path for get_node_guid")
707
+ return {"success": False, "error": "Missing blueprint_path"}
708
+
709
+ if graph_type not in ["EventGraph", "FunctionGraph"]:
710
+ log.log_error(f"Invalid graph_type: {graph_type}")
711
+ return {"success": False, "error": f"Invalid graph_type: {graph_type}"}
712
+
713
+ log.log_command("get_node_guid", f"Blueprint: {blueprint_path}, Graph: {graph_type}, Node: {node_name or function_id}")
714
+
715
+ # Call the C++ implementation
716
+ gen_bp_utils = unreal.GenBlueprintUtils
717
+ node_guid = gen_bp_utils.get_node_guid(blueprint_path, graph_type, node_name, function_id)
718
+
719
+ # FIX: Fallback to Python-based event node search if C++ fails (unreal-mcp-monitor)
720
+ if not node_guid and graph_type == "EventGraph" and node_name:
721
+ log.log_warning(f"C++ search failed for '{node_name}', trying Python fallback with pattern matching")
722
+ node_guid = _find_event_node_by_pattern(blueprint_path, node_name)
723
+
724
+ if node_guid:
725
+ log.log_result("get_node_guid", True, f"Found node GUID: {node_guid}")
726
+ return {"success": True, "node_guid": node_guid}
727
+ else:
728
+ log.log_error(f"Failed to find node: {node_name or 'FunctionEntry'}")
729
+ # Provide helpful suggestions for event nodes
730
+ if graph_type == "EventGraph":
731
+ suggestion = "Try: 'Event BeginPlay', 'ReceiveBeginPlay', or use get_all_nodes_in_graph to see all available event nodes"
732
+ else:
733
+ suggestion = "Use get_all_nodes_in_graph to see available nodes"
734
+ return {"success": False, "error": f"Node not found: {node_name or 'FunctionEntry'}", "suggestion": suggestion}
735
+
736
+ except Exception as e:
737
+ log.log_error(f"Error getting node GUID: {str(e)}", include_traceback=True)
738
+ return {"success": False, "error": str(e)}
739
+
740
+ def handle_get_node_pin_names(command: Dict[str, Any]) -> Dict[str, Any]:
741
+ """
742
+ Get all pin names for a specific node.
743
+ Helps with node connections.
744
+
745
+ Args:
746
+ command: Dict containing:
747
+ - blueprint_path: Path to Blueprint
748
+ - function_id: Function graph ID
749
+ - node_id: Node GUID
750
+
751
+ Returns:
752
+ Dict with input and output pin names
753
+ """
754
+ try:
755
+ blueprint_path = command.get("blueprint_path")
756
+ function_id = command.get("function_id")
757
+ node_id = command.get("node_id")
758
+
759
+ if not all([blueprint_path, function_id, node_id]):
760
+ return {"success": False, "error": "Missing required parameters: blueprint_path, function_id, node_id"}
761
+
762
+ log.log_command("get_node_pin_names", f"Blueprint: {blueprint_path}, Function: {function_id}, Node: {node_id}")
763
+
764
+ # Load the Blueprint
765
+ blueprint = unreal.load_asset(blueprint_path)
766
+ if not blueprint:
767
+ return {
768
+ "success": False,
769
+ "error": f"Blueprint not found: {blueprint_path}",
770
+ "suggestion": "Check that the path is correct and starts with /Game/"
771
+ }
772
+
773
+ # FIX: Use alternative API methods with fallbacks (unreal-mcp-monitor)
774
+ # Try multiple methods to access blueprint data
775
+ generated_class = None
776
+ try:
777
+ # Method 1: get_editor_property
778
+ generated_class = blueprint.get_editor_property('generated_class')
779
+ except:
780
+ try:
781
+ # Method 2: Direct attribute access
782
+ generated_class = blueprint.generated_class()
783
+ except:
784
+ # Method 3: Skip if not needed (generated_class not used below)
785
+ pass
786
+
787
+ # Get all graphs
788
+ function_graphs = unreal.EditorAssetLibrary.find_asset_data(blueprint_path).get_tag_value('UbergraphPages')
789
+
790
+ # Find the target graph by function ID
791
+ target_graph = None
792
+ for graph in blueprint.get_editor_property('function_graphs') or []:
793
+ graph_guid = str(graph.get_editor_property('graph_guid'))
794
+ if graph_guid == function_id:
795
+ target_graph = graph
796
+ break
797
+
798
+ # Also check ubergraph
799
+ if not target_graph:
800
+ ubergraph = blueprint.get_editor_property('ubergraph_pages')
801
+ if ubergraph and len(ubergraph) > 0:
802
+ for graph in ubergraph:
803
+ graph_guid = str(graph.get_editor_property('graph_guid'))
804
+ if graph_guid == function_id:
805
+ target_graph = graph
806
+ break
807
+
808
+ if not target_graph:
809
+ return {
810
+ "success": False,
811
+ "error": f"Function graph not found for ID: {function_id}",
812
+ "suggestion": "Use get_all_nodes_in_graph to see available functions"
813
+ }
814
+
815
+ # Find the node by GUID
816
+ all_nodes = target_graph.get_editor_property('nodes')
817
+ target_node = None
818
+
819
+ for node in all_nodes:
820
+ node_guid_str = str(node.get_editor_property('node_guid'))
821
+ if node_guid_str == node_id:
822
+ target_node = node
823
+ break
824
+
825
+ if not target_node:
826
+ return {
827
+ "success": False,
828
+ "error": f"Node not found with GUID: {node_id}",
829
+ "suggestion": "Use get_all_nodes_in_graph to see available nodes"
830
+ }
831
+
832
+ # Get pins
833
+ pins = target_node.get_editor_property('pins')
834
+ input_pins = []
835
+ output_pins = []
836
+
837
+ for pin in pins:
838
+ pin_name = str(pin.get_editor_property('pin_name'))
839
+ pin_category = str(pin.get_editor_property('pin_type').get_editor_property('pin_category'))
840
+ pin_is_array = pin.get_editor_property('pin_type').get_editor_property('container_type') == unreal.EPinContainerType.ARRAY
841
+
842
+ pin_info = {
843
+ "name": pin_name,
844
+ "type": pin_category,
845
+ "is_array": pin_is_array
846
+ }
847
+
848
+ pin_direction = pin.get_editor_property('direction')
849
+ if pin_direction == unreal.EdGraphPinDirection.EGPD_INPUT:
850
+ input_pins.append(pin_info)
851
+ else:
852
+ output_pins.append(pin_info)
853
+
854
+ node_type = target_node.__class__.__name__
855
+
856
+ log.log_result("get_node_pin_names", True, f"Found {len(input_pins)} input pins and {len(output_pins)} output pins")
857
+
858
+ return {
859
+ "success": True,
860
+ "node_type": node_type,
861
+ "node_id": node_id,
862
+ "input_pins": input_pins,
863
+ "output_pins": output_pins,
864
+ "total_input_pins": len(input_pins),
865
+ "total_output_pins": len(output_pins)
866
+ }
867
+
868
+ except Exception as e:
869
+ log.log_error(f"Error getting node pin names: {str(e)}", include_traceback=True)
870
+ return {
871
+ "success": False,
872
+ "error": f"Error: {str(e)}",
873
+ "recommendation": "Ensure the Blueprint, function, and node exist and are accessible"
874
+ }
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/blueprint_connection_commands.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Blueprint Node Connection Command Handlers for UMCP Plugin
3
+
4
+ This module provides handlers for Blueprint node connection operations,
5
+ integrating the Blueprint connection system with the UMCP socket server.
6
+
7
+ Handlers:
8
+ - get_node_pins: Discover pin information for a Blueprint node
9
+ - validate_connection: Validate a connection before making it
10
+ - auto_connect_chain: Intelligently auto-wire a chain of nodes
11
+ - suggest_connections: Get ranked connection suggestions
12
+ - get_graph_connections: List all connections in a function graph
13
+ """
14
+
15
+ import sys
16
+ import os
17
+ from typing import Dict, Any
18
+ from utils import logging as log
19
+
20
+ # Add blueprint_connections to path (dynamically find it relative to this file)
21
+ BLUEPRINT_CONN_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "blueprint_connections")
22
+ if BLUEPRINT_CONN_PATH not in sys.path:
23
+ sys.path.insert(0, BLUEPRINT_CONN_PATH)
24
+
25
+ # Import Blueprint connection modules
26
+ try:
27
+ from handlers.pin_discovery import PinDiscoveryService
28
+ from handlers.connection_validator import ConnectionValidator
29
+ from handlers.auto_wiring import AutoWiringService
30
+
31
+ log.log_info("Blueprint connection modules loaded successfully")
32
+ except Exception as e:
33
+ log.log_error(f"Failed to load Blueprint connection modules: {e}", include_traceback=True)
34
+ # Define placeholder classes so the module still loads
35
+ class PinDiscoveryService:
36
+ pass
37
+ class ConnectionValidator:
38
+ pass
39
+ class AutoWiringService:
40
+ pass
41
+
42
+
43
+ def handle_get_node_pins(command: Dict[str, Any]) -> Dict[str, Any]:
44
+ """
45
+ Get all pins for a Blueprint node
46
+
47
+ Args:
48
+ command: {
49
+ "type": "get_node_pins",
50
+ "blueprint_path": "/Game/Blueprints/BP_Test",
51
+ "function_id": "FUNCTION_GUID",
52
+ "node_id": "NODE_GUID",
53
+ "use_cache": true # optional, defaults to true
54
+ }
55
+
56
+ Returns:
57
+ {
58
+ "success": true,
59
+ "node_info": {...},
60
+ "input_pins": [{...}],
61
+ "output_pins": [{...}]
62
+ }
63
+ """
64
+ try:
65
+ blueprint_path = command.get("blueprint_path")
66
+ function_id = command.get("function_id")
67
+ node_id = command.get("node_id")
68
+ use_cache = command.get("use_cache", True)
69
+
70
+ if not all([blueprint_path, function_id, node_id]):
71
+ return {
72
+ "success": False,
73
+ "error": "Missing required parameters: blueprint_path, function_id, node_id"
74
+ }
75
+
76
+ # Initialize service - NO PARAMETERS
77
+ service = PinDiscoveryService()
78
+
79
+ # Get node pins - pass all parameters to method, not constructor
80
+ result = service.get_node_pins(
81
+ blueprint_path=blueprint_path,
82
+ function_id=function_id,
83
+ node_id=node_id,
84
+ use_cache=use_cache
85
+ )
86
+
87
+ return result
88
+
89
+ except Exception as e:
90
+ log.log_error(f"Error in get_node_pins: {e}", include_traceback=True)
91
+ return {"success": False, "error": str(e)}
92
+
93
+
94
+ def handle_validate_connection(command: Dict[str, Any]) -> Dict[str, Any]:
95
+ """
96
+ Validate a Blueprint node connection before making it
97
+
98
+ Args:
99
+ command: {
100
+ "type": "validate_connection",
101
+ "blueprint_path": "/Game/Blueprints/BP_Test",
102
+ "function_id": "FUNCTION_GUID",
103
+ "source_node_id": "SOURCE_GUID",
104
+ "source_pin_name": "ReturnValue",
105
+ "target_node_id": "TARGET_GUID",
106
+ "target_pin_name": "A"
107
+ }
108
+
109
+ Returns:
110
+ {
111
+ "success": true,
112
+ "valid": true/false,
113
+ "error_type": "TYPE_MISMATCH" (if invalid),
114
+ "suggestions": ["..."],
115
+ "warnings": ["..."]
116
+ }
117
+ """
118
+ try:
119
+ blueprint_path = command.get("blueprint_path")
120
+ function_id = command.get("function_id")
121
+ source_node_id = command.get("source_node_id")
122
+ source_pin_name = command.get("source_pin_name")
123
+ target_node_id = command.get("target_node_id")
124
+ target_pin_name = command.get("target_pin_name")
125
+
126
+ if not all([blueprint_path, function_id, source_node_id, source_pin_name,
127
+ target_node_id, target_pin_name]):
128
+ return {
129
+ "success": False,
130
+ "error": "Missing required parameters"
131
+ }
132
+
133
+ # Initialize service chain - CORRECT ORDER
134
+ pin_discovery = PinDiscoveryService()
135
+ validator = ConnectionValidator(pin_discovery)
136
+
137
+ # Validate connection - pass blueprint_path and function_id to METHOD
138
+ result = validator.validate_connection(
139
+ blueprint_path=blueprint_path,
140
+ function_id=function_id,
141
+ source_node_id=source_node_id,
142
+ source_pin_name=source_pin_name,
143
+ target_node_id=target_node_id,
144
+ target_pin_name=target_pin_name
145
+ )
146
+
147
+ # Add success flag
148
+ result["success"] = True
149
+ return result
150
+
151
+ except Exception as e:
152
+ log.log_error(f"Error in validate_connection: {e}", include_traceback=True)
153
+ return {"success": False, "error": str(e)}
154
+
155
+
156
+ def handle_auto_connect_chain(command: Dict[str, Any]) -> Dict[str, Any]:
157
+ """
158
+ Automatically wire a chain of Blueprint nodes
159
+
160
+ Args:
161
+ command: {
162
+ "type": "auto_connect_chain",
163
+ "blueprint_path": "/Game/Blueprints/BP_Test",
164
+ "function_id": "FUNCTION_GUID",
165
+ "node_chain": ["NODE_GUID_1", "NODE_GUID_2", "NODE_GUID_3"],
166
+ "validate_before_connect": true # optional, defaults to true
167
+ }
168
+
169
+ Returns:
170
+ {
171
+ "success": true,
172
+ "connections_made": 5,
173
+ "connections": [
174
+ {
175
+ "source_node_id": "...",
176
+ "source_pin": "...",
177
+ "target_node_id": "...",
178
+ "target_pin": "...",
179
+ "connection_type": "execution/data",
180
+ "confidence": 1.0
181
+ }
182
+ ],
183
+ "failed_connections": [...],
184
+ "warnings": [...]
185
+ }
186
+ """
187
+ try:
188
+ blueprint_path = command.get("blueprint_path")
189
+ function_id = command.get("function_id")
190
+ node_chain = command.get("node_chain", [])
191
+ validate_before_connect = command.get("validate_before_connect", True)
192
+
193
+ if not all([blueprint_path, function_id, node_chain]):
194
+ return {
195
+ "success": False,
196
+ "error": "Missing required parameters: blueprint_path, function_id, node_chain"
197
+ }
198
+
199
+ if len(node_chain) < 2:
200
+ return {
201
+ "success": False,
202
+ "error": "node_chain must contain at least 2 nodes"
203
+ }
204
+
205
+ # Initialize service chain - CORRECT ORDER
206
+ pin_discovery = PinDiscoveryService()
207
+ validator = ConnectionValidator(pin_discovery)
208
+ service = AutoWiringService(pin_discovery, validator)
209
+
210
+ # Auto-wire the chain - pass blueprint_path and function_id to METHOD
211
+ result = service.auto_connect_chain(
212
+ blueprint_path=blueprint_path,
213
+ function_id=function_id,
214
+ node_chain=node_chain,
215
+ validate_before_connect=validate_before_connect
216
+ )
217
+
218
+ # Add success flag
219
+ result["success"] = True
220
+ return result
221
+
222
+ except Exception as e:
223
+ log.log_error(f"Error in auto_connect_chain: {e}", include_traceback=True)
224
+ return {"success": False, "error": str(e)}
225
+
226
+
227
+ def handle_suggest_connections(command: Dict[str, Any]) -> Dict[str, Any]:
228
+ """
229
+ Get ranked connection suggestions between two nodes
230
+
231
+ Args:
232
+ command: {
233
+ "type": "suggest_connections",
234
+ "blueprint_path": "/Game/Blueprints/BP_Test",
235
+ "function_id": "FUNCTION_GUID",
236
+ "source_node_id": "SOURCE_GUID",
237
+ "target_node_id": "TARGET_GUID"
238
+ }
239
+
240
+ Returns:
241
+ {
242
+ "success": true,
243
+ "execution_connection": {...},
244
+ "data_connections": [{...}],
245
+ "all_possibilities": [{...}]
246
+ }
247
+ """
248
+ try:
249
+ blueprint_path = command.get("blueprint_path")
250
+ function_id = command.get("function_id")
251
+ source_node_id = command.get("source_node_id")
252
+ target_node_id = command.get("target_node_id")
253
+
254
+ if not all([blueprint_path, function_id, source_node_id, target_node_id]):
255
+ return {
256
+ "success": False,
257
+ "error": "Missing required parameters: blueprint_path, function_id, source_node_id, target_node_id"
258
+ }
259
+
260
+ # Initialize service chain - CORRECT ORDER
261
+ pin_discovery = PinDiscoveryService()
262
+ validator = ConnectionValidator(pin_discovery)
263
+ service = AutoWiringService(pin_discovery, validator)
264
+
265
+ # Get suggestions - pass blueprint_path and function_id to METHOD
266
+ result = service.suggest_connections(
267
+ blueprint_path=blueprint_path,
268
+ function_id=function_id,
269
+ source_node_id=source_node_id,
270
+ target_node_id=target_node_id
271
+ )
272
+
273
+ # Add success flag
274
+ result["success"] = True
275
+ return result
276
+
277
+ except Exception as e:
278
+ log.log_error(f"Error in suggest_connections: {e}", include_traceback=True)
279
+ return {"success": False, "error": str(e)}
280
+
281
+
282
+ def handle_get_graph_connections(command: Dict[str, Any]) -> Dict[str, Any]:
283
+ """
284
+ Get all connections in a Blueprint function graph
285
+
286
+ Args:
287
+ command: {
288
+ "type": "get_graph_connections",
289
+ "blueprint_path": "/Game/Blueprints/BP_Test",
290
+ "function_id": "FUNCTION_GUID"
291
+ }
292
+
293
+ Returns:
294
+ {
295
+ "success": true,
296
+ "connections": [
297
+ {
298
+ "source_node_guid": "...",
299
+ "source_pin_name": "...",
300
+ "target_node_guid": "...",
301
+ "target_pin_name": "..."
302
+ }
303
+ ],
304
+ "total_connections": 10
305
+ }
306
+ """
307
+ try:
308
+ blueprint_path = command.get("blueprint_path")
309
+ function_id = command.get("function_id")
310
+
311
+ if not all([blueprint_path, function_id]):
312
+ return {
313
+ "success": False,
314
+ "error": "Missing required parameters: blueprint_path, function_id"
315
+ }
316
+
317
+ # Initialize service - NO PARAMETERS
318
+ service = PinDiscoveryService()
319
+
320
+ # Get all connections - pass blueprint_path and function_id to METHOD
321
+ result = service.get_all_graph_connections(
322
+ blueprint_path=blueprint_path,
323
+ function_id=function_id
324
+ )
325
+
326
+ # Add success flag if not already present
327
+ if "success" not in result:
328
+ result["success"] = True
329
+
330
+ # Rename field for consistency with tool documentation
331
+ if "connection_count" in result:
332
+ result["total_connections"] = result.pop("connection_count")
333
+
334
+ return result
335
+
336
+ except Exception as e:
337
+ log.log_error(f"Error in get_graph_connections: {e}", include_traceback=True)
338
+ return {"success": False, "error": str(e)}
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/organization_commands.py ADDED
@@ -0,0 +1,646 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Asset and World Organization Commands for Unreal Engine MCP
3
+
4
+ This module provides comprehensive organization capabilities for Unreal Engine projects:
5
+ - Content Browser folder structure management
6
+ - Asset organization by type
7
+ - World Outliner actor organization
8
+ - Asset tagging system
9
+ - Asset search by tags
10
+ - Organization reporting
11
+
12
+ All functions have been tested in Unreal Engine 5.6.1 and are production-ready.
13
+ """
14
+
15
+ import json
16
+ import unreal
17
+ from typing import Dict, Any, List, Optional
18
+ from utils import logging as log
19
+
20
+
21
+ def handle_create_folder_structure(command: Dict[str, Any]) -> Dict[str, Any]:
22
+ """
23
+ Create a folder structure in the Content Browser.
24
+
25
+ Args:
26
+ command: Dictionary containing:
27
+ - base_path: Base path for folder creation (default: "/Game")
28
+ - folder_structure: Dictionary or list defining folder hierarchy
29
+
30
+ Returns:
31
+ Dictionary with created folders and status
32
+ """
33
+ try:
34
+ base_path = command.get("base_path", "/Game")
35
+ folder_structure = command.get("folder_structure", None)
36
+
37
+ created_folders = []
38
+ failed_folders = []
39
+
40
+ # Default folder structure if none provided
41
+ if folder_structure is None:
42
+ folder_structure = {
43
+ "Blueprints": ["Characters", "Weapons", "Items"],
44
+ "Materials": [],
45
+ "Meshes": ["Static", "Skeletal"],
46
+ "Textures": [],
47
+ "Audio": ["Music", "SFX"],
48
+ "Input": ["Actions", "Contexts"],
49
+ "UI": ["Widgets", "Textures"]
50
+ }
51
+
52
+ # Convert list to dict if needed
53
+ if isinstance(folder_structure, list):
54
+ folder_structure = {folder: [] for folder in folder_structure}
55
+
56
+ # Create base path if it doesn't exist
57
+ if not unreal.EditorAssetLibrary.does_directory_exist(base_path):
58
+ unreal.EditorAssetLibrary.make_directory(base_path)
59
+ created_folders.append(base_path)
60
+
61
+ # Create folder structure
62
+ for parent_folder, subfolders in folder_structure.items():
63
+ parent_path = f"{base_path}/{parent_folder}"
64
+
65
+ # Create parent folder
66
+ if not unreal.EditorAssetLibrary.does_directory_exist(parent_path):
67
+ success = unreal.EditorAssetLibrary.make_directory(parent_path)
68
+ if success:
69
+ created_folders.append(parent_path)
70
+ else:
71
+ failed_folders.append(parent_path)
72
+
73
+ # Create subfolders
74
+ for subfolder in subfolders:
75
+ subfolder_path = f"{parent_path}/{subfolder}"
76
+ if not unreal.EditorAssetLibrary.does_directory_exist(subfolder_path):
77
+ success = unreal.EditorAssetLibrary.make_directory(subfolder_path)
78
+ if success:
79
+ created_folders.append(subfolder_path)
80
+ else:
81
+ failed_folders.append(subfolder_path)
82
+
83
+ return {
84
+ 'success': True,
85
+ 'message': f'Created {len(created_folders)} folders',
86
+ 'created_folders': created_folders,
87
+ 'failed_folders': failed_folders,
88
+ 'total_created': len(created_folders),
89
+ 'total_failed': len(failed_folders)
90
+ }
91
+
92
+ except Exception as e:
93
+ log.log_error(f"Error creating folder structure: {str(e)}")
94
+ return {
95
+ 'success': False,
96
+ 'error': str(e)
97
+ }
98
+
99
+
100
+ def handle_organize_assets_by_type(command: Dict[str, Any]) -> Dict[str, Any]:
101
+ """
102
+ Organize assets from source folder into target folders by type.
103
+
104
+ Args:
105
+ command: Dictionary containing:
106
+ - source_folder: Folder to scan for assets
107
+ - target_base: Base path for organized folders (optional)
108
+ - organization_rules: Custom rules for organization (optional)
109
+ - dry_run: If True, don't move assets, just report (optional)
110
+
111
+ Returns:
112
+ Dictionary with organization results
113
+ """
114
+ try:
115
+ source_folder = command.get("source_folder")
116
+ target_base = command.get("target_base", None)
117
+ organization_rules = command.get("organization_rules", None)
118
+ dry_run = command.get("dry_run", False)
119
+
120
+ if not source_folder:
121
+ return {
122
+ 'success': False,
123
+ 'error': 'source_folder is required'
124
+ }
125
+
126
+ # Default target base
127
+ if target_base is None:
128
+ target_base = f"{source_folder}_Organized"
129
+
130
+ # Default organization rules
131
+ if organization_rules is None:
132
+ organization_rules = {
133
+ "Blueprint": f"{target_base}/Blueprints",
134
+ "InputAction": f"{target_base}/Input/Actions",
135
+ "InputMappingContext": f"{target_base}/Input/Contexts",
136
+ "Material": f"{target_base}/Materials",
137
+ "MaterialInstance": f"{target_base}/Materials/Instances",
138
+ "StaticMesh": f"{target_base}/Meshes/Static",
139
+ "SkeletalMesh": f"{target_base}/Meshes/Skeletal",
140
+ "Texture2D": f"{target_base}/Textures",
141
+ "SoundWave": f"{target_base}/Audio/SFX",
142
+ "AnimSequence": f"{target_base}/Animations",
143
+ "WidgetBlueprint": f"{target_base}/UI/Widgets",
144
+ }
145
+
146
+ # Create necessary folders
147
+ for folder_path in set(organization_rules.values()):
148
+ if not unreal.EditorAssetLibrary.does_directory_exist(folder_path):
149
+ unreal.EditorAssetLibrary.make_directory(folder_path)
150
+
151
+ # Get all assets in source folder
152
+ assets = unreal.EditorAssetLibrary.list_assets(source_folder, recursive=True)
153
+
154
+ moved_assets = []
155
+ skipped_assets = []
156
+ failed_assets = []
157
+
158
+ for asset_path in assets:
159
+ asset = unreal.load_asset(asset_path)
160
+ if not asset:
161
+ continue
162
+
163
+ asset_name = asset.get_name()
164
+ asset_class = asset.get_class().get_name()
165
+
166
+ # Find target folder for this asset type
167
+ target_folder = organization_rules.get(asset_class)
168
+ if not target_folder:
169
+ skipped_assets.append({
170
+ 'name': asset_name,
171
+ 'class': asset_class,
172
+ 'reason': 'No rule for this asset type'
173
+ })
174
+ continue
175
+
176
+ # Build new path
177
+ new_path = f"{target_folder}/{asset_name}"
178
+
179
+ # Skip if already in correct location
180
+ if asset_path.startswith(target_folder):
181
+ skipped_assets.append({
182
+ 'name': asset_name,
183
+ 'class': asset_class,
184
+ 'reason': 'Already in correct location'
185
+ })
186
+ continue
187
+
188
+ # Move asset (or simulate if dry run)
189
+ if dry_run:
190
+ moved_assets.append({
191
+ 'name': asset_name,
192
+ 'class': asset_class,
193
+ 'from': asset_path,
194
+ 'to': new_path,
195
+ 'moved': False,
196
+ 'dry_run': True
197
+ })
198
+ else:
199
+ success = unreal.EditorAssetLibrary.rename_asset(asset_path, new_path)
200
+ if success:
201
+ moved_assets.append({
202
+ 'name': asset_name,
203
+ 'class': asset_class,
204
+ 'from': asset_path,
205
+ 'to': new_path,
206
+ 'moved': True
207
+ })
208
+ else:
209
+ failed_assets.append({
210
+ 'name': asset_name,
211
+ 'class': asset_class,
212
+ 'path': asset_path,
213
+ 'target': new_path
214
+ })
215
+
216
+ return {
217
+ 'success': True,
218
+ 'dry_run': dry_run,
219
+ 'source_folder': source_folder,
220
+ 'target_base': target_base,
221
+ 'total_assets_scanned': len(assets),
222
+ 'moved_count': len(moved_assets),
223
+ 'skipped_count': len(skipped_assets),
224
+ 'failed_count': len(failed_assets),
225
+ 'moved_assets': moved_assets[:10] if len(moved_assets) > 10 else moved_assets,
226
+ 'skipped_assets': skipped_assets[:5] if len(skipped_assets) > 5 else skipped_assets,
227
+ 'failed_assets': failed_assets,
228
+ 'truncated': len(moved_assets) > 10
229
+ }
230
+
231
+ except Exception as e:
232
+ log.log_error(f"Error organizing assets: {str(e)}")
233
+ return {
234
+ 'success': False,
235
+ 'error': str(e)
236
+ }
237
+
238
+
239
+ def handle_organize_world_outliner(command: Dict[str, Any]) -> Dict[str, Any]:
240
+ """
241
+ Organize actors in the World Outliner into folder hierarchy.
242
+
243
+ Args:
244
+ command: Dictionary containing:
245
+ - organization_rules: Dictionary mapping actor classes to folder paths (optional)
246
+ - target_actors: List of specific actor names to organize (optional)
247
+
248
+ Returns:
249
+ Dictionary with organization results
250
+ """
251
+ try:
252
+ organization_rules = command.get("organization_rules", None)
253
+ target_actors = command.get("target_actors", None)
254
+
255
+ # Default organization rules
256
+ if organization_rules is None:
257
+ organization_rules = {
258
+ # Lighting
259
+ "DirectionalLight": "/Environment/Lighting/Directional",
260
+ "PointLight": "/Environment/Lighting/Point",
261
+ "SpotLight": "/Environment/Lighting/Spot",
262
+ "SkyLight": "/Environment/Lighting/Sky",
263
+ "RectLight": "/Environment/Lighting/Rect",
264
+
265
+ # Environment
266
+ "Landscape": "/Environment/Terrain",
267
+ "StaticMeshActor": "/Props/Static",
268
+ "Foliage": "/Environment/Foliage",
269
+
270
+ # Gameplay
271
+ "PlayerStart": "/Gameplay/Spawn",
272
+ "TriggerBox": "/Gameplay/Triggers/Box",
273
+ "TriggerSphere": "/Gameplay/Triggers/Sphere",
274
+ "TriggerVolume": "/Gameplay/Triggers/Volume",
275
+ "NavMeshBoundsVolume": "/Gameplay/Navigation",
276
+
277
+ # Cinematics
278
+ "Camera": "/Cinematics/Cameras",
279
+ "CineCameraActor": "/Cinematics/Cameras/Cine",
280
+
281
+ # Audio
282
+ "AmbientSound": "/Audio/Ambient",
283
+
284
+ # Effects
285
+ "ParticleSystem": "/Effects/Particles",
286
+ "Niagara": "/Effects/Niagara",
287
+ }
288
+
289
+ # Get all actors in level
290
+ all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
291
+
292
+ # Filter to target actors if specified
293
+ if target_actors:
294
+ all_actors = [a for a in all_actors if a and a.get_name() in target_actors]
295
+
296
+ organized_actors = []
297
+ skipped_actors = []
298
+
299
+ for actor in all_actors:
300
+ if not actor:
301
+ continue
302
+
303
+ actor_name = actor.get_name()
304
+ actor_class = actor.get_class().get_name()
305
+
306
+ # Find matching folder path
307
+ folder_path = None
308
+ for class_pattern, path in organization_rules.items():
309
+ if class_pattern in actor_class:
310
+ folder_path = path
311
+ break
312
+
313
+ if folder_path:
314
+ # Set folder path for actor
315
+ actor.set_folder_path(folder_path)
316
+ organized_actors.append({
317
+ 'name': actor_name,
318
+ 'class': actor_class,
319
+ 'folder': folder_path
320
+ })
321
+ else:
322
+ skipped_actors.append({
323
+ 'name': actor_name,
324
+ 'class': actor_class,
325
+ 'reason': 'No matching organization rule'
326
+ })
327
+
328
+ # Generate folder summary
329
+ folder_counts = {}
330
+ for item in organized_actors:
331
+ folder = item['folder']
332
+ folder_counts[folder] = folder_counts.get(folder, 0) + 1
333
+
334
+ return {
335
+ 'success': True,
336
+ 'total_actors': len(all_actors),
337
+ 'organized_count': len(organized_actors),
338
+ 'skipped_count': len(skipped_actors),
339
+ 'folder_summary': folder_counts,
340
+ 'organized_actors': organized_actors[:10] if len(organized_actors) > 10 else organized_actors,
341
+ 'skipped_actors': skipped_actors[:5] if len(skipped_actors) > 5 else skipped_actors,
342
+ 'truncated': len(organized_actors) > 10
343
+ }
344
+
345
+ except Exception as e:
346
+ log.log_error(f"Error organizing world outliner: {str(e)}")
347
+ return {
348
+ 'success': False,
349
+ 'error': str(e)
350
+ }
351
+
352
+
353
+ def handle_tag_assets(command: Dict[str, Any]) -> Dict[str, Any]:
354
+ """
355
+ Add metadata tags to assets for organization and search.
356
+
357
+ Args:
358
+ command: Dictionary containing:
359
+ - asset_paths: List of asset paths to tag
360
+ - tags: List of tag strings to apply
361
+ - auto_tag: If True, automatically generate tags (optional)
362
+
363
+ Returns:
364
+ Dictionary with tagging results
365
+ """
366
+ try:
367
+ asset_paths = command.get("asset_paths", [])
368
+ tags = command.get("tags", [])
369
+ auto_tag = command.get("auto_tag", False)
370
+
371
+ if not asset_paths:
372
+ return {
373
+ 'success': False,
374
+ 'error': 'asset_paths is required'
375
+ }
376
+
377
+ tagged_assets = []
378
+ failed_assets = []
379
+
380
+ for asset_path in asset_paths:
381
+ asset = unreal.load_asset(asset_path)
382
+ if not asset:
383
+ failed_assets.append({
384
+ 'path': asset_path,
385
+ 'reason': 'Asset not found'
386
+ })
387
+ continue
388
+
389
+ asset_name = asset.get_name()
390
+ asset_class = asset.get_class().get_name()
391
+
392
+ # Generate auto tags if requested
393
+ applied_tags = list(tags)
394
+ if auto_tag:
395
+ auto_tags = []
396
+
397
+ # Tag based on name patterns
398
+ name_upper = asset_name.upper()
399
+ if "FPS" in name_upper:
400
+ auto_tags.append("FPS")
401
+ if "JUMP" in name_upper:
402
+ auto_tags.extend(["Movement", "Character"])
403
+ if "FIRE" in name_upper or "SHOOT" in name_upper:
404
+ auto_tags.extend(["Combat", "Weapon"])
405
+ if "MOVE" in name_upper or "WALK" in name_upper or "RUN" in name_upper:
406
+ auto_tags.append("Movement")
407
+ if "LOOK" in name_upper or "AIM" in name_upper:
408
+ auto_tags.append("Camera")
409
+ if "RELOAD" in name_upper:
410
+ auto_tags.extend(["Combat", "Weapon"])
411
+ if "CROUCH" in name_upper:
412
+ auto_tags.extend(["Movement", "Character"])
413
+
414
+ # Tag based on class
415
+ if "Input" in asset_class:
416
+ auto_tags.append("Input")
417
+ if "Blueprint" in asset_class:
418
+ auto_tags.append("Blueprint")
419
+ if "Material" in asset_class:
420
+ auto_tags.append("Material")
421
+
422
+ applied_tags.extend(auto_tags)
423
+
424
+ # Remove duplicates
425
+ applied_tags = list(set(applied_tags))
426
+
427
+ # Apply tags as metadata
428
+ for tag in applied_tags:
429
+ unreal.EditorAssetLibrary.set_metadata_tag(asset, tag, "true")
430
+
431
+ tagged_assets.append({
432
+ 'name': asset_name,
433
+ 'class': asset_class,
434
+ 'path': asset_path,
435
+ 'tags': applied_tags
436
+ })
437
+
438
+ return {
439
+ 'success': True,
440
+ 'tagged_count': len(tagged_assets),
441
+ 'failed_count': len(failed_assets),
442
+ 'auto_tag': auto_tag,
443
+ 'tagged_assets': tagged_assets,
444
+ 'failed_assets': failed_assets
445
+ }
446
+
447
+ except Exception as e:
448
+ log.log_error(f"Error tagging assets: {str(e)}")
449
+ return {
450
+ 'success': False,
451
+ 'error': str(e)
452
+ }
453
+
454
+
455
+ def handle_search_assets_by_tag(command: Dict[str, Any]) -> Dict[str, Any]:
456
+ """
457
+ Search for assets with specific metadata tags.
458
+
459
+ Args:
460
+ command: Dictionary containing:
461
+ - folder_path: Folder to search in
462
+ - tags: List of tags to search for
463
+ - match_all: If True, asset must have ALL tags (optional)
464
+
465
+ Returns:
466
+ Dictionary with search results
467
+ """
468
+ try:
469
+ folder_path = command.get("folder_path")
470
+ tags = command.get("tags", [])
471
+ match_all = command.get("match_all", False)
472
+
473
+ if not folder_path:
474
+ return {
475
+ 'success': False,
476
+ 'error': 'folder_path is required'
477
+ }
478
+
479
+ if not tags:
480
+ return {
481
+ 'success': False,
482
+ 'error': 'tags list is required'
483
+ }
484
+
485
+ # Get all assets in folder
486
+ assets = unreal.EditorAssetLibrary.list_assets(folder_path, recursive=True)
487
+
488
+ matching_assets = []
489
+
490
+ for asset_path in assets:
491
+ asset = unreal.load_asset(asset_path)
492
+ if not asset:
493
+ continue
494
+
495
+ asset_name = asset.get_name()
496
+ asset_class = asset.get_class().get_name()
497
+
498
+ # Check tags
499
+ asset_tags = []
500
+ for tag in tags:
501
+ has_tag = unreal.EditorAssetLibrary.get_metadata_tag(asset, tag)
502
+ if has_tag:
503
+ asset_tags.append(tag)
504
+
505
+ # Determine if this asset matches
506
+ if match_all:
507
+ matches = len(asset_tags) == len(tags)
508
+ else:
509
+ matches = len(asset_tags) > 0
510
+
511
+ if matches:
512
+ matching_assets.append({
513
+ 'name': asset_name,
514
+ 'class': asset_class,
515
+ 'path': asset_path,
516
+ 'matched_tags': asset_tags
517
+ })
518
+
519
+ return {
520
+ 'success': True,
521
+ 'search_folder': folder_path,
522
+ 'search_tags': tags,
523
+ 'match_all': match_all,
524
+ 'total_assets_scanned': len(assets),
525
+ 'matching_count': len(matching_assets),
526
+ 'matching_assets': matching_assets
527
+ }
528
+
529
+ except Exception as e:
530
+ log.log_error(f"Error searching assets by tag: {str(e)}")
531
+ return {
532
+ 'success': False,
533
+ 'error': str(e)
534
+ }
535
+
536
+
537
+ def handle_generate_organization_report(command: Dict[str, Any]) -> Dict[str, Any]:
538
+ """
539
+ Generate a comprehensive organization report.
540
+
541
+ Args:
542
+ command: Dictionary containing:
543
+ - content_browser_paths: List of paths to analyze (optional)
544
+ - include_world_outliner: Include World Outliner stats (optional)
545
+
546
+ Returns:
547
+ Dictionary with comprehensive organization report
548
+ """
549
+ try:
550
+ content_browser_paths = command.get("content_browser_paths", None)
551
+ include_world_outliner = command.get("include_world_outliner", True)
552
+
553
+ # Default paths
554
+ if content_browser_paths is None:
555
+ content_browser_paths = ["/Game"]
556
+
557
+ report = {
558
+ 'content_browser': {},
559
+ 'world_outliner': {},
560
+ 'summary': {}
561
+ }
562
+
563
+ # Analyze Content Browser
564
+ total_assets = 0
565
+ assets_by_type = {}
566
+ assets_by_folder = {}
567
+
568
+ for base_path in content_browser_paths:
569
+ # Get all assets recursively
570
+ assets = unreal.EditorAssetLibrary.list_assets(base_path, recursive=True)
571
+ total_assets += len(assets)
572
+
573
+ for asset_path in assets:
574
+ asset = unreal.load_asset(asset_path)
575
+ if not asset:
576
+ continue
577
+
578
+ asset_class = asset.get_class().get_name()
579
+
580
+ # Count by type
581
+ assets_by_type[asset_class] = assets_by_type.get(asset_class, 0) + 1
582
+
583
+ # Count by folder (immediate parent folder)
584
+ folder = '/'.join(asset_path.split('/')[:-1])
585
+ assets_by_folder[folder] = assets_by_folder.get(folder, 0) + 1
586
+
587
+ report['content_browser'] = {
588
+ 'total_assets': total_assets,
589
+ 'assets_by_type': dict(sorted(assets_by_type.items(), key=lambda x: x[1], reverse=True)),
590
+ 'top_folders': dict(sorted(assets_by_folder.items(), key=lambda x: x[1], reverse=True)[:20])
591
+ }
592
+
593
+ # Analyze World Outliner
594
+ if include_world_outliner:
595
+ all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
596
+ actors_by_class = {}
597
+ actors_by_folder = {}
598
+ organized_actors = 0
599
+ unorganized_actors = 0
600
+
601
+ for actor in all_actors:
602
+ if not actor:
603
+ continue
604
+
605
+ actor_class = actor.get_class().get_name()
606
+ folder = str(actor.get_folder_path())
607
+
608
+ # Count by class
609
+ actors_by_class[actor_class] = actors_by_class.get(actor_class, 0) + 1
610
+
611
+ # Count by folder
612
+ if folder and folder != "None":
613
+ actors_by_folder[folder] = actors_by_folder.get(folder, 0) + 1
614
+ organized_actors += 1
615
+ else:
616
+ unorganized_actors += 1
617
+
618
+ report['world_outliner'] = {
619
+ 'total_actors': len(all_actors),
620
+ 'organized_actors': organized_actors,
621
+ 'unorganized_actors': unorganized_actors,
622
+ 'organization_percentage': round((organized_actors / len(all_actors) * 100), 2) if len(all_actors) > 0 else 0,
623
+ 'actors_by_class': dict(sorted(actors_by_class.items(), key=lambda x: x[1], reverse=True)[:15]),
624
+ 'actors_by_folder': dict(sorted(actors_by_folder.items(), key=lambda x: x[1], reverse=True)[:15])
625
+ }
626
+
627
+ # Generate summary
628
+ report['summary'] = {
629
+ 'content_browser_total_assets': total_assets,
630
+ 'content_browser_asset_types': len(assets_by_type),
631
+ 'content_browser_folders': len(assets_by_folder),
632
+ 'world_outliner_total_actors': report['world_outliner'].get('total_actors', 0),
633
+ 'world_outliner_organized_percentage': report['world_outliner'].get('organization_percentage', 0)
634
+ }
635
+
636
+ return {
637
+ 'success': True,
638
+ 'report': report
639
+ }
640
+
641
+ except Exception as e:
642
+ log.log_error(f"Error generating organization report: {str(e)}")
643
+ return {
644
+ 'success': False,
645
+ 'error': str(e)
646
+ }
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/physics_commands.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Physics and Selection Command Handlers - Simplified Version
3
+
4
+ Based on proven working code that avoids collision enum mismatches.
5
+ Only modifies mobility and physics simulation.
6
+
7
+ Date: October 8, 2025
8
+ Status: Production Ready
9
+ """
10
+
11
+ import json
12
+ import unreal
13
+ from typing import Dict, Any
14
+
15
+
16
+ def handle_get_selected_actors(command: Dict[str, Any]) -> Dict[str, Any]:
17
+ """
18
+ Get all currently selected actors in the Unreal Editor viewport.
19
+
20
+ Returns actor names, classes, locations, and physics status.
21
+
22
+ Args:
23
+ command: Command dictionary (no parameters needed)
24
+
25
+ Returns:
26
+ JSON dict with list of selected actors and their details
27
+ """
28
+ try:
29
+ # Get selected actors
30
+ selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
31
+
32
+ if not selected_actors:
33
+ return {
34
+ 'success': True,
35
+ 'selected_count': 0,
36
+ 'message': 'No actors selected',
37
+ 'actors': []
38
+ }
39
+
40
+ # Build actor info list
41
+ actors_info = []
42
+ for actor in selected_actors:
43
+ if not actor:
44
+ continue
45
+
46
+ actor_name = actor.get_name()
47
+ actor_class = actor.get_class().get_name()
48
+ location = actor.get_actor_location()
49
+
50
+ # Check for mesh component
51
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
52
+ if not mesh_component:
53
+ mesh_component = actor.get_component_by_class(unreal.SkeletalMeshComponent)
54
+
55
+ has_mesh = mesh_component is not None
56
+ physics_enabled = False
57
+ if mesh_component:
58
+ physics_enabled = mesh_component.body_instance.simulate_physics
59
+
60
+ actors_info.append({
61
+ 'name': actor_name,
62
+ 'class': actor_class,
63
+ 'location': {
64
+ 'x': round(location.x, 2),
65
+ 'y': round(location.y, 2),
66
+ 'z': round(location.z, 2)
67
+ },
68
+ 'has_mesh': has_mesh,
69
+ 'physics_enabled': physics_enabled
70
+ })
71
+
72
+ return {
73
+ 'success': True,
74
+ 'selected_count': len(actors_info),
75
+ 'actors': actors_info
76
+ }
77
+
78
+ except Exception as e:
79
+ return {
80
+ 'success': False,
81
+ 'error': str(e)
82
+ }
83
+
84
+
85
+ def handle_enable_physics_on_selected(command: Dict[str, Any]) -> Dict[str, Any]:
86
+ """
87
+ Enable physics simulation on all currently selected actors.
88
+
89
+ Uses the proven working approach:
90
+ 1. Set mobility to Movable
91
+ 2. Enable physics simulation
92
+ 3. Does NOT modify collision (avoids enum mismatches)
93
+
94
+ Args:
95
+ command: Command dictionary (no parameters needed)
96
+
97
+ Returns:
98
+ JSON dict with results for each processed actor
99
+ """
100
+ try:
101
+ # Get selected actors
102
+ selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
103
+
104
+ if not selected_actors:
105
+ return {
106
+ 'success': False,
107
+ 'error': 'No actors selected',
108
+ 'message': 'Please select actors in the viewport first'
109
+ }
110
+
111
+ processed_actors = []
112
+ skipped_actors = []
113
+
114
+ for actor in selected_actors:
115
+ if not actor:
116
+ continue
117
+
118
+ actor_name = actor.get_name()
119
+ actor_class = actor.get_class().get_name()
120
+
121
+ # Try StaticMeshComponent first, then SkeletalMeshComponent
122
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
123
+ if not mesh_component:
124
+ mesh_component = actor.get_component_by_class(unreal.SkeletalMeshComponent)
125
+
126
+ if not mesh_component:
127
+ skipped_actors.append({
128
+ 'name': actor_name,
129
+ 'class': actor_class,
130
+ 'reason': 'No StaticMeshComponent or SkeletalMeshComponent found'
131
+ })
132
+ continue
133
+
134
+ # Store before state
135
+ component_name = mesh_component.get_name()
136
+ before_mobility = str(mesh_component.mobility)
137
+ before_physics = mesh_component.body_instance.simulate_physics
138
+
139
+ # Step 1: Set mobility to Movable (required for physics)
140
+ mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
141
+
142
+ # Step 2: Enable physics simulation
143
+ mesh_component.set_simulate_physics(True)
144
+
145
+ # Verify it worked
146
+ after_physics = mesh_component.body_instance.simulate_physics
147
+
148
+ processed_actors.append({
149
+ 'name': actor_name,
150
+ 'class': actor_class,
151
+ 'component': component_name,
152
+ 'before': {
153
+ 'mobility': before_mobility,
154
+ 'physics': before_physics
155
+ },
156
+ 'after': {
157
+ 'mobility': 'MOVABLE',
158
+ 'physics': after_physics
159
+ }
160
+ })
161
+
162
+ return {
163
+ 'success': True,
164
+ 'processed_count': len(processed_actors),
165
+ 'skipped_count': len(skipped_actors),
166
+ 'processed_actors': processed_actors,
167
+ 'skipped_actors': skipped_actors,
168
+ 'message': f'Enabled physics on {len(processed_actors)} actor(s)'
169
+ }
170
+
171
+ except Exception as e:
172
+ return {
173
+ 'success': False,
174
+ 'error': str(e)
175
+ }
176
+
177
+
178
+ def handle_enable_physics_on_actor(command: Dict[str, Any]) -> Dict[str, Any]:
179
+ """
180
+ Enable physics simulation on a specific actor by name.
181
+
182
+ No selection required - targets actor by name directly.
183
+
184
+ Args:
185
+ command: Command dictionary with parameters:
186
+ - actor_name (str): Name of the actor (e.g., "SM_Table_01_60")
187
+
188
+ Returns:
189
+ JSON dict with results
190
+ """
191
+ try:
192
+ actor_name = command.get('actor_name')
193
+ if not actor_name:
194
+ return {
195
+ 'success': False,
196
+ 'error': 'actor_name parameter required'
197
+ }
198
+
199
+ # Get all actors and find the target
200
+ all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
201
+
202
+ target_actor = None
203
+ for actor in all_actors:
204
+ if actor and actor.get_name() == actor_name:
205
+ target_actor = actor
206
+ break
207
+
208
+ if not target_actor:
209
+ return {
210
+ 'success': False,
211
+ 'error': f'Actor not found: {actor_name}',
212
+ 'message': 'Check actor name and try again'
213
+ }
214
+
215
+ actor_class = target_actor.get_class().get_name()
216
+
217
+ # Try StaticMeshComponent first, then SkeletalMeshComponent
218
+ mesh_component = target_actor.get_component_by_class(unreal.StaticMeshComponent)
219
+ if not mesh_component:
220
+ mesh_component = target_actor.get_component_by_class(unreal.SkeletalMeshComponent)
221
+
222
+ if not mesh_component:
223
+ return {
224
+ 'success': False,
225
+ 'error': 'No mesh component found',
226
+ 'actor_name': actor_name,
227
+ 'actor_class': actor_class
228
+ }
229
+
230
+ # Store before state
231
+ component_name = mesh_component.get_name()
232
+ before_mobility = str(mesh_component.mobility)
233
+ before_physics = mesh_component.body_instance.simulate_physics
234
+
235
+ # Enable physics
236
+ mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
237
+ mesh_component.set_simulate_physics(True)
238
+
239
+ # Verify it worked
240
+ after_physics = mesh_component.body_instance.simulate_physics
241
+
242
+ return {
243
+ 'success': True,
244
+ 'actor_name': actor_name,
245
+ 'actor_class': actor_class,
246
+ 'component': component_name,
247
+ 'before': {
248
+ 'mobility': before_mobility,
249
+ 'physics': before_physics
250
+ },
251
+ 'after': {
252
+ 'mobility': 'MOVABLE',
253
+ 'physics': after_physics
254
+ },
255
+ 'message': f'Enabled physics on {actor_name}'
256
+ }
257
+
258
+ except Exception as e:
259
+ return {
260
+ 'success': False,
261
+ 'error': str(e)
262
+ }
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/physics_commands_backup.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Physics and Selection Command Handlers
3
+
4
+ Handles physics simulation and actor selection commands for the Unreal MCP plugin.
5
+ Commands include getting selected actors, enabling physics on selected/named actors.
6
+
7
+ Date: October 8, 2025
8
+ Status: Production Ready
9
+ """
10
+
11
+ import json
12
+ import unreal
13
+ from typing import Dict, Any
14
+
15
+
16
+ def handle_get_selected_actors(command: Dict[str, Any]) -> Dict[str, Any]:
17
+ """
18
+ Get all currently selected actors in the Unreal Editor viewport.
19
+
20
+ Returns actor names, classes, locations, and physics status.
21
+
22
+ Args:
23
+ command: Command dictionary (no parameters needed)
24
+
25
+ Returns:
26
+ JSON dict with list of selected actors and their details
27
+ """
28
+ try:
29
+ # Get selected actors
30
+ selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
31
+
32
+ if not selected_actors:
33
+ return {
34
+ 'success': True,
35
+ 'selected_count': 0,
36
+ 'message': 'No actors selected',
37
+ 'actors': []
38
+ }
39
+
40
+ # Build actor info list
41
+ actors_info = []
42
+ for actor in selected_actors:
43
+ if not actor:
44
+ continue
45
+
46
+ actor_name = actor.get_name()
47
+ actor_class = actor.get_class().get_name()
48
+ location = actor.get_actor_location()
49
+
50
+ # Check if it has a static mesh component
51
+ components = actor.get_components_by_class(unreal.StaticMeshComponent)
52
+ has_static_mesh = len(components) > 0
53
+
54
+ # Check if physics is enabled (if it has static mesh)
55
+ physics_enabled = False
56
+ if components:
57
+ physics_enabled = components[0].body_instance.simulate_physics
58
+
59
+ actors_info.append({
60
+ 'name': actor_name,
61
+ 'class': actor_class,
62
+ 'location': {
63
+ 'x': location.x,
64
+ 'y': location.y,
65
+ 'z': location.z
66
+ },
67
+ 'has_static_mesh': has_static_mesh,
68
+ 'physics_enabled': physics_enabled
69
+ })
70
+
71
+ return {
72
+ 'success': True,
73
+ 'selected_count': len(actors_info),
74
+ 'actors': actors_info
75
+ }
76
+
77
+ except Exception as e:
78
+ return {
79
+ 'success': False,
80
+ 'error': str(e)
81
+ }
82
+
83
+
84
+ def handle_enable_physics_on_selected(command: Dict[str, Any]) -> Dict[str, Any]:
85
+ """
86
+ Enable physics simulation on all currently selected actors.
87
+
88
+ This function:
89
+ 1. Sets mobility to Movable
90
+ 2. Enables physics simulation
91
+ 3. Sets collision to QUERY_AND_PHYSICS
92
+ 4. Optionally overrides mass
93
+
94
+ Args:
95
+ command: Command dictionary with optional parameters:
96
+ - enable_collision (bool): Enable collision (default: True)
97
+ - mass_override (float): Override mass in kg (default: None)
98
+
99
+ Returns:
100
+ JSON dict with results for each actor
101
+ """
102
+ try:
103
+ enable_collision = command.get('enable_collision', True)
104
+ mass_override = command.get('mass_override', None)
105
+
106
+ # Get selected actors
107
+ selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
108
+
109
+ if not selected_actors:
110
+ return {
111
+ 'success': False,
112
+ 'error': 'No actors selected',
113
+ 'message': 'Please select actors in the viewport first'
114
+ }
115
+
116
+ processed_actors = []
117
+ skipped_actors = []
118
+
119
+ for actor in selected_actors:
120
+ if not actor:
121
+ continue
122
+
123
+ actor_name = actor.get_name()
124
+ actor_class = actor.get_class().get_name()
125
+
126
+ # Get static mesh components
127
+ components = actor.get_components_by_class(unreal.StaticMeshComponent)
128
+
129
+ if not components:
130
+ skipped_actors.append({
131
+ 'name': actor_name,
132
+ 'class': actor_class,
133
+ 'reason': 'No StaticMeshComponent found'
134
+ })
135
+ continue
136
+
137
+ # Process each component (usually just one)
138
+ components_processed = []
139
+ for component in components:
140
+ component_name = component.get_name()
141
+
142
+ # Store before state
143
+ before_mobility = str(component.mobility)
144
+ before_physics = component.body_instance.simulate_physics
145
+
146
+ # Step 1: Set mobility to Movable
147
+ component.set_mobility(unreal.ComponentMobility.MOVABLE)
148
+
149
+ # Step 2: Enable physics simulation
150
+ component.set_simulate_physics(True)
151
+
152
+ # Step 3: Enable collision if requested
153
+ if enable_collision:
154
+ component.set_collision_enabled(unreal.CollisionEnabled.QUERY_AND_PHYSICS)
155
+
156
+ # Step 4: Override mass if specified
157
+ if mass_override is not None:
158
+ component.set_mass_override_in_kg(name='', mass_in_kg=mass_override, override=True)
159
+
160
+ # Get final mass
161
+ final_mass = component.get_mass()
162
+
163
+ components_processed.append({
164
+ 'component_name': component_name,
165
+ 'before': {
166
+ 'mobility': before_mobility,
167
+ 'physics': before_physics
168
+ },
169
+ 'after': {
170
+ 'mobility': 'MOVABLE',
171
+ 'physics': True,
172
+ 'mass_kg': round(final_mass, 2)
173
+ }
174
+ })
175
+
176
+ processed_actors.append({
177
+ 'name': actor_name,
178
+ 'class': actor_class,
179
+ 'components': components_processed
180
+ })
181
+
182
+ return {
183
+ 'success': True,
184
+ 'processed_count': len(processed_actors),
185
+ 'skipped_count': len(skipped_actors),
186
+ 'processed_actors': processed_actors,
187
+ 'skipped_actors': skipped_actors,
188
+ 'message': f'Enabled physics on {len(processed_actors)} actor(s)'
189
+ }
190
+
191
+ except Exception as e:
192
+ return {
193
+ 'success': False,
194
+ 'error': str(e)
195
+ }
196
+
197
+
198
+ def handle_enable_physics_on_actor(command: Dict[str, Any]) -> Dict[str, Any]:
199
+ """
200
+ Enable physics simulation on a specific actor by name.
201
+
202
+ Args:
203
+ command: Command dictionary with parameters:
204
+ - actor_name (str): Name of the actor (e.g., "SM_Stairs_632", "Cube_1")
205
+ - enable_collision (bool): Enable collision (default: True)
206
+ - mass_override (float): Override mass in kg (default: None)
207
+
208
+ Returns:
209
+ JSON dict with results
210
+ """
211
+ try:
212
+ actor_name = command.get('actor_name')
213
+ enable_collision = command.get('enable_collision', True)
214
+ mass_override = command.get('mass_override', None)
215
+
216
+ if not actor_name:
217
+ return {
218
+ 'success': False,
219
+ 'error': 'actor_name parameter is required'
220
+ }
221
+
222
+ # Get all actors
223
+ all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
224
+
225
+ # Find actor by name
226
+ target_actor = None
227
+ for actor in all_actors:
228
+ if actor and actor.get_name() == actor_name:
229
+ target_actor = actor
230
+ break
231
+
232
+ if not target_actor:
233
+ return {
234
+ 'success': False,
235
+ 'error': f'Actor not found: {actor_name}',
236
+ 'message': 'Check actor name and try again'
237
+ }
238
+
239
+ actor_class = target_actor.get_class().get_name()
240
+
241
+ # Get static mesh components
242
+ components = target_actor.get_components_by_class(unreal.StaticMeshComponent)
243
+
244
+ if not components:
245
+ return {
246
+ 'success': False,
247
+ 'error': 'No StaticMeshComponent found',
248
+ 'actor_name': actor_name,
249
+ 'actor_class': actor_class
250
+ }
251
+
252
+ # Process components
253
+ components_processed = []
254
+ for component in components:
255
+ component_name = component.get_name()
256
+
257
+ # Store before state
258
+ before_mobility = str(component.mobility)
259
+ before_physics = component.body_instance.simulate_physics
260
+
261
+ # Step 1: Set mobility to Movable
262
+ component.set_mobility(unreal.ComponentMobility.MOVABLE)
263
+
264
+ # Step 2: Enable physics simulation
265
+ component.set_simulate_physics(True)
266
+
267
+ # Step 3: Enable collision if requested
268
+ if enable_collision:
269
+ component.set_collision_enabled(unreal.CollisionEnabled.QUERY_AND_PHYSICS)
270
+
271
+ # Step 4: Override mass if specified
272
+ if mass_override is not None:
273
+ component.set_mass_override_in_kg(name='', mass_in_kg=mass_override, override=True)
274
+
275
+ # Get final mass
276
+ final_mass = component.get_mass()
277
+
278
+ components_processed.append({
279
+ 'component_name': component_name,
280
+ 'before': {
281
+ 'mobility': before_mobility,
282
+ 'physics': before_physics
283
+ },
284
+ 'after': {
285
+ 'mobility': 'MOVABLE',
286
+ 'physics': True,
287
+ 'mass_kg': round(final_mass, 2)
288
+ }
289
+ })
290
+
291
+ return {
292
+ 'success': True,
293
+ 'actor_name': actor_name,
294
+ 'actor_class': actor_class,
295
+ 'components': components_processed,
296
+ 'message': f'Enabled physics on {actor_name}'
297
+ }
298
+
299
+ except Exception as e:
300
+ return {
301
+ 'success': False,
302
+ 'error': str(e)
303
+ }
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/physics_commands_simple.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Physics and Selection Command Handlers - Simplified Version
3
+
4
+ Based on proven working code that avoids collision enum mismatches.
5
+ Only modifies mobility and physics simulation.
6
+
7
+ Date: October 8, 2025
8
+ Status: Production Ready
9
+ """
10
+
11
+ import json
12
+ import unreal
13
+ from typing import Dict, Any
14
+
15
+
16
+ def handle_get_selected_actors(command: Dict[str, Any]) -> Dict[str, Any]:
17
+ """
18
+ Get all currently selected actors in the Unreal Editor viewport.
19
+
20
+ Returns actor names, classes, locations, and physics status.
21
+
22
+ Args:
23
+ command: Command dictionary (no parameters needed)
24
+
25
+ Returns:
26
+ JSON dict with list of selected actors and their details
27
+ """
28
+ try:
29
+ # Get selected actors
30
+ selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
31
+
32
+ if not selected_actors:
33
+ return {
34
+ 'success': True,
35
+ 'selected_count': 0,
36
+ 'message': 'No actors selected',
37
+ 'actors': []
38
+ }
39
+
40
+ # Build actor info list
41
+ actors_info = []
42
+ for actor in selected_actors:
43
+ if not actor:
44
+ continue
45
+
46
+ actor_name = actor.get_name()
47
+ actor_class = actor.get_class().get_name()
48
+ location = actor.get_actor_location()
49
+
50
+ # Check for mesh component
51
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
52
+ if not mesh_component:
53
+ mesh_component = actor.get_component_by_class(unreal.SkeletalMeshComponent)
54
+
55
+ has_mesh = mesh_component is not None
56
+ physics_enabled = False
57
+ if mesh_component:
58
+ physics_enabled = mesh_component.body_instance.simulate_physics
59
+
60
+ actors_info.append({
61
+ 'name': actor_name,
62
+ 'class': actor_class,
63
+ 'location': {
64
+ 'x': round(location.x, 2),
65
+ 'y': round(location.y, 2),
66
+ 'z': round(location.z, 2)
67
+ },
68
+ 'has_mesh': has_mesh,
69
+ 'physics_enabled': physics_enabled
70
+ })
71
+
72
+ return {
73
+ 'success': True,
74
+ 'selected_count': len(actors_info),
75
+ 'actors': actors_info
76
+ }
77
+
78
+ except Exception as e:
79
+ return {
80
+ 'success': False,
81
+ 'error': str(e)
82
+ }
83
+
84
+
85
+ def handle_enable_physics_on_selected(command: Dict[str, Any]) -> Dict[str, Any]:
86
+ """
87
+ Enable physics simulation on all currently selected actors.
88
+
89
+ Uses the proven working approach:
90
+ 1. Set mobility to Movable
91
+ 2. Enable physics simulation
92
+ 3. Does NOT modify collision (avoids enum mismatches)
93
+
94
+ Args:
95
+ command: Command dictionary (no parameters needed)
96
+
97
+ Returns:
98
+ JSON dict with results for each processed actor
99
+ """
100
+ try:
101
+ # Get selected actors
102
+ selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
103
+
104
+ if not selected_actors:
105
+ return {
106
+ 'success': False,
107
+ 'error': 'No actors selected',
108
+ 'message': 'Please select actors in the viewport first'
109
+ }
110
+
111
+ processed_actors = []
112
+ skipped_actors = []
113
+
114
+ for actor in selected_actors:
115
+ if not actor:
116
+ continue
117
+
118
+ actor_name = actor.get_name()
119
+ actor_class = actor.get_class().get_name()
120
+
121
+ # Try StaticMeshComponent first, then SkeletalMeshComponent
122
+ mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
123
+ if not mesh_component:
124
+ mesh_component = actor.get_component_by_class(unreal.SkeletalMeshComponent)
125
+
126
+ if not mesh_component:
127
+ skipped_actors.append({
128
+ 'name': actor_name,
129
+ 'class': actor_class,
130
+ 'reason': 'No StaticMeshComponent or SkeletalMeshComponent found'
131
+ })
132
+ continue
133
+
134
+ # Store before state
135
+ component_name = mesh_component.get_name()
136
+ before_mobility = str(mesh_component.mobility)
137
+ before_physics = mesh_component.body_instance.simulate_physics
138
+
139
+ # Step 1: Set mobility to Movable (required for physics)
140
+ mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
141
+
142
+ # Step 2: Enable physics simulation
143
+ mesh_component.set_simulate_physics(True)
144
+
145
+ # Verify it worked
146
+ after_physics = mesh_component.body_instance.simulate_physics
147
+
148
+ processed_actors.append({
149
+ 'name': actor_name,
150
+ 'class': actor_class,
151
+ 'component': component_name,
152
+ 'before': {
153
+ 'mobility': before_mobility,
154
+ 'physics': before_physics
155
+ },
156
+ 'after': {
157
+ 'mobility': 'MOVABLE',
158
+ 'physics': after_physics
159
+ }
160
+ })
161
+
162
+ return {
163
+ 'success': True,
164
+ 'processed_count': len(processed_actors),
165
+ 'skipped_count': len(skipped_actors),
166
+ 'processed_actors': processed_actors,
167
+ 'skipped_actors': skipped_actors,
168
+ 'message': f'Enabled physics on {len(processed_actors)} actor(s)'
169
+ }
170
+
171
+ except Exception as e:
172
+ return {
173
+ 'success': False,
174
+ 'error': str(e)
175
+ }
176
+
177
+
178
+ def handle_enable_physics_on_actor(command: Dict[str, Any]) -> Dict[str, Any]:
179
+ """
180
+ Enable physics simulation on a specific actor by name.
181
+
182
+ No selection required - targets actor by name directly.
183
+
184
+ Args:
185
+ command: Command dictionary with parameters:
186
+ - actor_name (str): Name of the actor (e.g., "SM_Table_01_60")
187
+
188
+ Returns:
189
+ JSON dict with results
190
+ """
191
+ try:
192
+ actor_name = command.get('actor_name')
193
+ if not actor_name:
194
+ return {
195
+ 'success': False,
196
+ 'error': 'actor_name parameter required'
197
+ }
198
+
199
+ # Get all actors and find the target
200
+ all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
201
+
202
+ target_actor = None
203
+ for actor in all_actors:
204
+ if actor and actor.get_name() == actor_name:
205
+ target_actor = actor
206
+ break
207
+
208
+ if not target_actor:
209
+ return {
210
+ 'success': False,
211
+ 'error': f'Actor not found: {actor_name}',
212
+ 'message': 'Check actor name and try again'
213
+ }
214
+
215
+ actor_class = target_actor.get_class().get_name()
216
+
217
+ # Try StaticMeshComponent first, then SkeletalMeshComponent
218
+ mesh_component = target_actor.get_component_by_class(unreal.StaticMeshComponent)
219
+ if not mesh_component:
220
+ mesh_component = target_actor.get_component_by_class(unreal.SkeletalMeshComponent)
221
+
222
+ if not mesh_component:
223
+ return {
224
+ 'success': False,
225
+ 'error': 'No mesh component found',
226
+ 'actor_name': actor_name,
227
+ 'actor_class': actor_class
228
+ }
229
+
230
+ # Store before state
231
+ component_name = mesh_component.get_name()
232
+ before_mobility = str(mesh_component.mobility)
233
+ before_physics = mesh_component.body_instance.simulate_physics
234
+
235
+ # Enable physics
236
+ mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
237
+ mesh_component.set_simulate_physics(True)
238
+
239
+ # Verify it worked
240
+ after_physics = mesh_component.body_instance.simulate_physics
241
+
242
+ return {
243
+ 'success': True,
244
+ 'actor_name': actor_name,
245
+ 'actor_class': actor_class,
246
+ 'component': component_name,
247
+ 'before': {
248
+ 'mobility': before_mobility,
249
+ 'physics': before_physics
250
+ },
251
+ 'after': {
252
+ 'mobility': 'MOVABLE',
253
+ 'physics': after_physics
254
+ },
255
+ 'message': f'Enabled physics on {actor_name}'
256
+ }
257
+
258
+ except Exception as e:
259
+ return {
260
+ 'success': False,
261
+ 'error': str(e)
262
+ }
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/python_commands.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import textwrap
2
+
3
+ import unreal
4
+ import sys
5
+ from typing import Dict, Any
6
+ import os
7
+ import uuid
8
+ import time
9
+ import traceback
10
+
11
+ # Assuming a logging module similar to your example
12
+ from utils import logging as log
13
+
14
+
15
+ def execute_script(script_file, output_file, error_file, status_file):
16
+ """Execute a Python script with output and error redirection."""
17
+ with open(output_file, 'w') as output_file_handle, open(error_file, 'w') as error_file_handle:
18
+ original_stdout = sys.stdout
19
+ original_stderr = sys.stderr
20
+ sys.stdout = output_file_handle
21
+ sys.stderr = error_file_handle
22
+
23
+ success = True
24
+ try:
25
+ with open(script_file, 'r') as f:
26
+ exec(f.read())
27
+ except Exception as e:
28
+ traceback.print_exc()
29
+ success = False
30
+ finally:
31
+ sys.stdout = original_stdout
32
+ sys.stderr = original_stderr
33
+
34
+ with open(status_file, 'w') as f:
35
+ f.write('1' if success else '0')
36
+
37
+
38
+ def get_log_line_count():
39
+ """
40
+ Get the current line count of the Unreal log file
41
+ """
42
+ try:
43
+ log_path = os.path.join(unreal.Paths.project_log_dir(), "Unreal.log")
44
+ if os.path.exists(log_path):
45
+ with open(log_path, 'r', encoding='utf-8', errors='ignore') as f:
46
+ return sum(1 for _ in f)
47
+ return 0
48
+ except Exception as e:
49
+ log.log_error(f"Error getting log line count: {str(e)}")
50
+ return 0
51
+
52
+
53
+ def get_recent_unreal_logs(start_line=None):
54
+ """
55
+ Retrieve recent Unreal Engine log entries to provide context for errors
56
+
57
+ Args:
58
+ start_line: Optional line number to start from (to only get new logs)
59
+
60
+ Returns:
61
+ String containing log entries or None if logs couldn't be accessed
62
+ """
63
+ try:
64
+ log_path = os.path.join(unreal.Paths.project_log_dir(), "Unreal.log")
65
+ if os.path.exists(log_path):
66
+ with open(log_path, 'r', encoding='utf-8', errors='ignore') as f:
67
+ if start_line is None:
68
+ # Legacy behavior - get last 20 lines
69
+ lines = f.readlines()
70
+ return "".join(lines[-20:])
71
+ else:
72
+ # Skip to the starting line
73
+ for i, _ in enumerate(f):
74
+ if i >= start_line - 1:
75
+ break
76
+
77
+ # Get all new lines
78
+ new_lines = f.readlines()
79
+ return "".join(new_lines) if new_lines else "No new log entries generated"
80
+ return None
81
+ except Exception as e:
82
+ log.log_error(f"Error getting recent logs: {str(e)}")
83
+ return None
84
+
85
+
86
+ def handle_execute_python(command: Dict[str, Any]) -> Dict[str, Any]:
87
+ """
88
+ Handle a command to execute a Python script in Unreal Engine
89
+
90
+ Args:
91
+ command: The command dictionary containing:
92
+ - script: The Python code to execute as a string
93
+ - force: Optional boolean to bypass safety checks (default: False)
94
+
95
+ Returns:
96
+ Response dictionary with success/failure status and output if successful
97
+ """
98
+ try:
99
+ script = command.get("script")
100
+ force = command.get("force", False)
101
+
102
+ if not script:
103
+ log.log_error("Missing required parameter for execute_python: script")
104
+ return {"success": False, "error": "Missing required parameter: script"}
105
+
106
+ log.log_command("execute_python", f"Script: {script[:50]}...")
107
+
108
+ # Get log line count before execution
109
+ log_start_line = get_log_line_count()
110
+
111
+ destructive_keywords = [
112
+ "unreal.EditorAssetLibrary.delete_asset",
113
+ "unreal.EditorLevelLibrary.destroy_actor",
114
+ "unreal.save_package",
115
+ "os.remove",
116
+ "shutil.rmtree",
117
+ "file.write",
118
+ "unreal.EditorAssetLibrary.save_asset"
119
+ ]
120
+ is_destructive = any(keyword in script for keyword in destructive_keywords)
121
+
122
+ if is_destructive and not force:
123
+ log.log_warning("Potentially destructive script detected")
124
+ return {
125
+ "success": False,
126
+ "error": ("This script may involve destructive actions (e.g., deleting or saving files) "
127
+ "not explicitly requested. Please confirm with 'Yes, execute it' or set force=True.")
128
+ }
129
+
130
+ temp_dir = os.path.join(unreal.Paths.project_saved_dir(), "Temp", "PythonExec")
131
+ if not os.path.exists(temp_dir):
132
+ os.makedirs(temp_dir)
133
+
134
+ script_file = os.path.join(temp_dir, f"script_{uuid.uuid4().hex}.py")
135
+ output_file = os.path.join(temp_dir, "output.txt")
136
+ error_file = os.path.join(temp_dir, "error.txt")
137
+ status_file = os.path.join(temp_dir, "status.txt")
138
+
139
+ # Normalize user script indentation and write to file
140
+ dedented_script = textwrap.dedent(script).strip()
141
+ with open(script_file, 'w') as f:
142
+ f.write(dedented_script)
143
+
144
+ # Execute using the wrapper
145
+ execute_script(script_file, output_file, error_file, status_file)
146
+ time.sleep(0.5) # Allow execution to complete
147
+
148
+ output = ""
149
+ error = ""
150
+ success = False
151
+
152
+ if os.path.exists(output_file):
153
+ with open(output_file, 'r') as f:
154
+ output = f.read()
155
+ if os.path.exists(error_file):
156
+ with open(error_file, 'r') as f:
157
+ error = f.read()
158
+ if os.path.exists(status_file):
159
+ with open(status_file, 'r') as f:
160
+ success = f.read().strip() == "1"
161
+
162
+ for file in [script_file, output_file, error_file, status_file]:
163
+ if os.path.exists(file):
164
+ os.remove(file)
165
+
166
+ # Enhanced error handling for common Unreal API issues
167
+ if not success and error:
168
+ if "set_world_location() required argument 'sweep'" in error:
169
+ error += "\n\nHINT: The set_world_location() method requires a 'sweep' parameter. Try: set_world_location(location, sweep=False)"
170
+ elif "set_world_location() required argument 'teleport'" in error:
171
+ error += "\n\nHINT: The set_world_location() method requires 'teleport' parameter. Try: set_world_location(location, sweep=False, teleport=False)"
172
+ elif "set_actor_location() required argument 'teleport'" in error:
173
+ error += "\n\nHINT: The set_actor_location() method requires a 'teleport' parameter. Try: set_actor_location(location, sweep=False, teleport=False)"
174
+
175
+ # Get only new log entries
176
+ recent_logs = get_recent_unreal_logs(log_start_line)
177
+ if recent_logs:
178
+ error += "\n\nNew Unreal logs during execution:\n" + recent_logs
179
+
180
+ if success:
181
+ # Get only new log entries for successful execution as well
182
+ recent_logs = get_recent_unreal_logs(log_start_line)
183
+ if recent_logs:
184
+ output += "\n\nNew Unreal logs during execution:\n" + recent_logs
185
+
186
+ log.log_result("execute_python", True, f"Script executed with output: {output}")
187
+ return {"success": True, "output": output}
188
+ else:
189
+ log.log_error(f"Script execution failed with error: {error}")
190
+ return {"success": False, "error": error if error else "Execution failed without specific error", "output": output}
191
+
192
+ except Exception as e:
193
+ log.log_error(f"Error handling execute_python: {str(e)}", include_traceback=True)
194
+ return {"success": False, "error": str(e)}
195
+ finally:
196
+ for file in [script_file, output_file, error_file, status_file]:
197
+ if os.path.exists(file):
198
+ try:
199
+ os.remove(file)
200
+ except:
201
+ pass
202
+
203
+
204
+ def handle_execute_unreal_command(command: Dict[str, Any]) -> Dict[str, Any]:
205
+ """
206
+ Handle a command to execute an Unreal Engine console command
207
+
208
+ Args:
209
+ command: The command dictionary containing:
210
+ - command: The Unreal Engine console command to execute
211
+ - force: Optional boolean to bypass safety checks (default: False)
212
+
213
+ Returns:
214
+ Response dictionary with success/failure status and output if successful
215
+ """
216
+ script_file = None
217
+ output_file = None
218
+ error_file = None
219
+
220
+ try:
221
+ cmd = command.get("command")
222
+ force = command.get("force", False)
223
+
224
+ if not cmd:
225
+ log.log_error("Missing required parameter for execute_unreal_command: command")
226
+ return {"success": False, "error": "Missing required parameter: command"}
227
+
228
+ if cmd.strip().lower().startswith("py "):
229
+ log.log_error("Attempted to run a Python script with execute_unreal_command")
230
+ return {
231
+ "success": False,
232
+ "error": ("Use 'execute_python' command to run Python scripts instead of 'execute_unreal_command' with 'py'. "
233
+ "For example, send {'type': 'execute_python', 'script': 'your_code_here'}.")
234
+ }
235
+
236
+ log.log_command("execute_unreal_command", f"Command: {cmd}")
237
+
238
+ # Get log line count before execution
239
+ log_start_line = get_log_line_count()
240
+
241
+ destructive_keywords = ["delete", "save", "quit", "exit", "restart"]
242
+ is_destructive = any(keyword in cmd.lower() for keyword in destructive_keywords)
243
+
244
+ if is_destructive and not force:
245
+ log.log_warning("Potentially destructive command detected")
246
+ return {
247
+ "success": False,
248
+ "error": ("This command may involve destructive actions (e.g., deleting or saving). "
249
+ "Please confirm with 'Yes, execute it' or set force=True.")
250
+ }
251
+
252
+ # Execute the command
253
+ world = unreal.EditorLevelLibrary.get_editor_world()
254
+ unreal.SystemLibrary.execute_console_command(world, cmd)
255
+
256
+ # Add a short delay to allow logs to be captured
257
+ time.sleep(1.0) # Slightly longer delay to ensure logs are written
258
+
259
+ # Get new log entries generated during command execution
260
+ recent_logs = get_recent_unreal_logs(log_start_line)
261
+
262
+ output = f"Command '{cmd}' executed successfully"
263
+ if recent_logs:
264
+ output += "\n\nRelated Unreal logs:\n" + recent_logs
265
+
266
+ log.log_result("execute_unreal_command", True, f"Command '{cmd}' executed")
267
+ return {"success": True, "output": output}
268
+
269
+ except Exception as e:
270
+ # Get new log entries to provide context for the error
271
+ recent_logs = get_recent_unreal_logs(log_start_line) if 'log_start_line' in locals() else None
272
+ error_msg = f"Error executing command: {str(e)}"
273
+
274
+ if recent_logs:
275
+ error_msg += "\n\nUnreal logs around the time of error:\n" + recent_logs
276
+
277
+ log.log_error(f"Error handling execute_unreal_command: {str(e)}", include_traceback=True)
278
+ return {"success": False, "error": error_msg}
UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/ui_commands.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unreal
2
+ from typing import Dict, Any, Union
3
+ from utils import logging as log
4
+ import json # Import json for parsing C++ response
5
+ import re # For parsing property values
6
+
7
+ # Lazy load the C++ utils class to avoid issues during Unreal init
8
+ _widget_gen_utils = None
9
+ def get_widget_gen_utils():
10
+ global _widget_gen_utils
11
+ if _widget_gen_utils is None:
12
+ try:
13
+ # Attempt to load the class generated from C++
14
+ # Make sure your C++ class is compiled and accessible to Python
15
+ _widget_gen_utils = unreal.GenWidgetUtils()
16
+ except Exception as e:
17
+ log.log_error(f"Failed to get unreal.WidgetGenUtils: {e}. Ensure C++ module is compiled and loaded.")
18
+ raise # Re-raise to signal failure
19
+ return _widget_gen_utils
20
+
21
+ def handle_add_widget_to_user_widget(command: Dict[str, Any]) -> Dict[str, Any]:
22
+ """
23
+ Handles adding a widget component to a User Widget Blueprint.
24
+ """
25
+ try:
26
+ widget_gen_utils = get_widget_gen_utils()
27
+ user_widget_path = command.get("user_widget_path")
28
+ widget_type = command.get("widget_type")
29
+ widget_name = command.get("widget_name")
30
+ parent_name = command.get("parent_widget_name", "") # Default to empty string if not provided
31
+
32
+ log.log_command("add_widget_to_user_widget", f"Path: {user_widget_path}, Type: {widget_type}, Name: {widget_name}, Parent: {parent_name}")
33
+
34
+ if not all([user_widget_path, widget_type, widget_name]):
35
+ return {"success": False, "error": "Missing required arguments: user_widget_path, widget_type, widget_name"}
36
+
37
+ # Call the C++ function
38
+ response_str = widget_gen_utils.add_widget_to_user_widget(user_widget_path, widget_type, widget_name, parent_name)
39
+
40
+ # Parse the JSON response from C++
41
+ response_json = json.loads(response_str)
42
+ log.log_result("add_widget_to_user_widget", response_json.get("success", False), response_json.get("message") or response_json.get("error"))
43
+ return response_json
44
+
45
+ except Exception as e:
46
+ log.log_error(f"Error in handle_add_widget_to_user_widget: {str(e)}", include_traceback=True)
47
+ return {"success": False, "error": f"Python Handler Error: {str(e)}"}
48
+
49
+ def parse_property_value(property_name: str, value_string: str) -> Union[unreal.Vector2D, unreal.LinearColor, unreal.Anchors, bool, float, int, str]:
50
+ """
51
+ Helper function to parse property values based on their type.
52
+
53
+ Args:
54
+ property_name: Name of the property being set
55
+ value_string: String representation of the value
56
+
57
+ Returns:
58
+ Parsed value in appropriate Unreal type
59
+ """
60
+ # Vector2D properties (Position, Size)
61
+ if property_name.lower() in ['position', 'size']:
62
+ # Parse "(X=100.0,Y=200.0)" format
63
+ if 'X=' in value_string and 'Y=' in value_string:
64
+ x_match = re.search(r'X=([\d.-]+)', value_string)
65
+ y_match = re.search(r'Y=([\d.-]+)', value_string)
66
+ if x_match and y_match:
67
+ return unreal.Vector2D(float(x_match.group(1)), float(y_match.group(1)))
68
+ # Parse "[100, 200]" or "100, 200" format
69
+ elif '[' in value_string or ',' in value_string:
70
+ value_string = value_string.strip('[]')
71
+ coords = [float(x.strip()) for x in value_string.split(',')]
72
+ if len(coords) >= 2:
73
+ return unreal.Vector2D(coords[0], coords[1])
74
+
75
+ # Anchors
76
+ if property_name.lower() == 'anchors':
77
+ # Parse "(Minimum=(X=0.5,Y=0.0),Maximum=(X=0.5,Y=0.0))" format
78
+ min_x = re.search(r'Minimum=\(X=([\d.-]+)', value_string)
79
+ min_y = re.search(r'Minimum=\(X=[\d.-]+,Y=([\d.-]+)', value_string)
80
+ max_x = re.search(r'Maximum=\(X=([\d.-]+)', value_string)
81
+ max_y = re.search(r'Maximum=\(X=[\d.-]+,Y=([\d.-]+)', value_string)
82
+
83
+ if all([min_x, min_y, max_x, max_y]):
84
+ anchors = unreal.Anchors()
85
+ anchors.minimum = unreal.Vector2D(float(min_x.group(1)), float(min_y.group(1)))
86
+ anchors.maximum = unreal.Vector2D(float(max_x.group(1)), float(max_y.group(1)))
87
+ return anchors
88
+
89
+ # LinearColor (ColorAndOpacity, etc.)
90
+ if 'color' in property_name.lower():
91
+ # Parse "(R=1.0,G=0.0,B=0.0,A=1.0)" format
92
+ r = re.search(r'R=([\d.-]+)', value_string)
93
+ g = re.search(r'G=([\d.-]+)', value_string)
94
+ b = re.search(r'B=([\d.-]+)', value_string)
95
+ a = re.search(r'A=([\d.-]+)', value_string)
96
+
97
+ if all([r, g, b, a]):
98
+ return unreal.LinearColor(
99
+ float(r.group(1)),
100
+ float(g.group(1)),
101
+ float(b.group(1)),
102
+ float(a.group(1))
103
+ )
104
+
105
+ # Boolean
106
+ if value_string.lower() in ['true', 'false']:
107
+ return value_string.lower() == 'true'
108
+
109
+ # Float
110
+ try:
111
+ if '.' in value_string:
112
+ return float(value_string)
113
+ except:
114
+ pass
115
+
116
+ # Integer
117
+ try:
118
+ return int(value_string)
119
+ except:
120
+ pass
121
+
122
+ # Default: return as string
123
+ return value_string
124
+
125
+ def handle_edit_widget_property(command: Dict[str, Any]) -> Dict[str, Any]:
126
+ """
127
+ Handles editing a property of a widget inside a User Widget Blueprint.
128
+ Supports Text properties with unreal.Text conversion and other property types.
129
+ """
130
+ try:
131
+ widget_gen_utils = get_widget_gen_utils()
132
+ user_widget_path = command.get("user_widget_path")
133
+ widget_name = command.get("widget_name")
134
+ property_name = command.get("property_name")
135
+ value_str = command.get("value") # Value is expected as a string
136
+
137
+ log.log_command("edit_widget_property", f"Path: {user_widget_path}, Widget: {widget_name}, Property: {property_name}, Value: {value_str}")
138
+
139
+ if not all([user_widget_path, widget_name, property_name, value_str is not None]):
140
+ return {"success": False, "error": "Missing required arguments: user_widget_path, widget_name, property_name, value"}
141
+
142
+ # ENHANCED: Special handling for Text properties using unreal.Text()
143
+ if property_name.lower() == "text":
144
+ try:
145
+ # Try to create unreal.Text object directly
146
+ # This is the recommended approach for UE5
147
+ text_obj = unreal.Text(value_str)
148
+ # Convert to string representation that C++ can parse
149
+ value_str = f'NSLOCTEXT("", "", "{value_str}")'
150
+ log.log_info(f"Converted to FText format: {value_str}")
151
+ except Exception as text_err:
152
+ log.log_warning(f"Could not create unreal.Text object, using fallback: {text_err}")
153
+ # Fallback to NSLOCTEXT format
154
+ value_str = f'NSLOCTEXT("", "", "{value_str}")'
155
+
156
+ # ENHANCED: Parse other property types for better type safety
157
+ elif property_name.lower() in ['position', 'size', 'anchors'] or 'color' in property_name.lower():
158
+ try:
159
+ parsed_value = parse_property_value(property_name, value_str)
160
+ # Convert back to string format that C++ can parse
161
+ if isinstance(parsed_value, unreal.Vector2D):
162
+ value_str = f"(X={parsed_value.x},Y={parsed_value.y})"
163
+ elif isinstance(parsed_value, unreal.LinearColor):
164
+ value_str = f"(R={parsed_value.r},G={parsed_value.g},B={parsed_value.b},A={parsed_value.a})"
165
+ elif isinstance(parsed_value, unreal.Anchors):
166
+ value_str = f"(Minimum=(X={parsed_value.minimum.x},Y={parsed_value.minimum.y}),Maximum=(X={parsed_value.maximum.x},Y={parsed_value.maximum.y}))"
167
+ log.log_info(f"Parsed property value to: {value_str}")
168
+ except Exception as parse_err:
169
+ log.log_warning(f"Could not parse property value, using raw string: {parse_err}")
170
+
171
+ # Call the C++ function
172
+ response_str = widget_gen_utils.edit_widget_property(user_widget_path, widget_name, property_name, value_str)
173
+
174
+ # Parse the JSON response from C++
175
+ response_json = json.loads(response_str)
176
+ log.log_result("edit_widget_property", response_json.get("success", False), response_json.get("message") or response_json.get("error"))
177
+ return response_json
178
+
179
+ except Exception as e:
180
+ log.log_error(f"Error in handle_edit_widget_property: {str(e)}", include_traceback=True)
181
+ return {"success": False, "error": f"Python Handler Error: {str(e)}"}
UMCP.it-Unreal-Organizer-Assistant/Content/Python/init_unreal.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Content/Python/init_unreal.py - Using UE settings integration
2
+ import unreal
3
+ import importlib.util
4
+ import os
5
+ import sys
6
+ import subprocess
7
+ import atexit
8
+ from utils import logging as log
9
+
10
+ # Global process handle for MCP server
11
+ mcp_server_process = None
12
+
13
+ def shutdown_mcp_server():
14
+ """Shutdown the MCP server process when Unreal Editor closes"""
15
+ global mcp_server_process
16
+ if mcp_server_process:
17
+ log.log_info("Shutting down MCP server process...")
18
+ try:
19
+ mcp_server_process.terminate()
20
+ mcp_server_process = None
21
+ log.log_info("MCP server process terminated successfully")
22
+ except Exception as e:
23
+ log.log_error(f"Error terminating MCP server: {e}")
24
+
25
+ def start_mcp_server():
26
+ """Start the external MCP server process"""
27
+ global mcp_server_process
28
+ try:
29
+ # Find our plugin's Python directory - search for UMCP/Content/Python
30
+ # Skip subdirectories like blueprint_connections
31
+ plugin_python_path = None
32
+ for path in sys.path:
33
+ if ("UMCP/Content/Python" in path or "Content/Python" in path) and "blueprint_connections" not in path:
34
+ plugin_python_path = path
35
+ break
36
+
37
+ if not plugin_python_path:
38
+ log.log_error("Could not find plugin Python path")
39
+ return False
40
+
41
+ # Get the mcp_server.py path
42
+ mcp_server_path = os.path.join(plugin_python_path, "mcp_server.py")
43
+
44
+ if not os.path.exists(mcp_server_path):
45
+ log.log_error(f"MCP server script not found at: {mcp_server_path}")
46
+ return False
47
+
48
+ # Start the MCP server as a separate process
49
+ python_exe = sys.executable
50
+ log.log_info(f"Starting MCP server using Python: {python_exe}")
51
+ log.log_info(f"MCP server script path: {mcp_server_path}")
52
+
53
+ # Create a detached process that will continue running
54
+ # even if Unreal crashes (we'll handle proper shutdown with atexit)
55
+ creationflags = 0
56
+ if sys.platform == 'win32':
57
+ creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
58
+
59
+ mcp_server_process = subprocess.Popen(
60
+ [python_exe, mcp_server_path],
61
+ creationflags=creationflags,
62
+ stdout=subprocess.PIPE,
63
+ stderr=subprocess.PIPE,
64
+ text=True
65
+ )
66
+
67
+ log.log_info(f"MCP server started with PID: {mcp_server_process.pid}")
68
+
69
+ # Register cleanup handler to ensure process is terminated when Unreal exits
70
+ atexit.register(shutdown_mcp_server)
71
+
72
+ return True
73
+ except Exception as e:
74
+ log.log_error(f"Error starting MCP server: {e}")
75
+ return False
76
+ def initialize_socket_server():
77
+ """
78
+ Initialize the socket server if auto-start is enabled in UE settings
79
+ """
80
+ auto_start = False
81
+
82
+ # Get settings from UE settings system
83
+ try:
84
+ # First get the class reference
85
+ settings_class = unreal.load_class(None, '/Script/GenerativeAISupportEditor.GenerativeAISupportSettings')
86
+ if settings_class:
87
+ # Get the settings object using the class reference
88
+ settings = unreal.get_default_object(settings_class)
89
+
90
+ # Log available properties for debugging
91
+ log.log_info(f"Settings object properties: {dir(settings)}")
92
+
93
+ # Check if auto-start is enabled
94
+ # Unreal Python API converts bAutoStartSocketServer -> auto_start_socket_server
95
+ if hasattr(settings, 'auto_start_socket_server'):
96
+ auto_start = settings.auto_start_socket_server
97
+ log.log_info(f"Socket server auto-start setting: {auto_start}")
98
+ else:
99
+ log.log_warning("auto_start_socket_server property not found in settings")
100
+ # Try alternative property names that might exist
101
+ for prop in dir(settings):
102
+ if 'auto' in prop.lower() or 'socket' in prop.lower() or 'server' in prop.lower():
103
+ log.log_info(f"Found similar property: {prop}")
104
+ else:
105
+ log.log_error("Could not find GenerativeAISupportSettings class")
106
+ except Exception as e:
107
+ log.log_error(f"Error reading UE settings: {e}")
108
+ log.log_info("Falling back to disabled auto-start")
109
+
110
+ # Auto-start if configured
111
+ if auto_start:
112
+ log.log_info("Auto-starting Unreal Socket Server...")
113
+
114
+ # Start Unreal Socket Server
115
+ try:
116
+ # Find our plugin's Python directory - search for UMCP/Content/Python
117
+ # Skip subdirectories like blueprint_connections
118
+ plugin_python_path = None
119
+ for path in sys.path:
120
+ if ("UMCP/Content/Python" in path or "Content/Python" in path) and "blueprint_connections" not in path:
121
+ plugin_python_path = path
122
+ break
123
+
124
+ if plugin_python_path:
125
+ server_path = os.path.join(plugin_python_path, "unreal_socket_server.py")
126
+
127
+ if os.path.exists(server_path):
128
+ # Import and execute the server module
129
+ spec = importlib.util.spec_from_file_location("unreal_socket_server", server_path)
130
+ server_module = importlib.util.module_from_spec(spec)
131
+ spec.loader.exec_module(server_module)
132
+ log.log_info("Unreal Socket Server started successfully")
133
+ else:
134
+ log.log_error(f"Server file not found at: {server_path}")
135
+ else:
136
+ log.log_error("Could not find plugin Python path")
137
+ except Exception as e:
138
+ log.log_error(f"Error starting socket server: {e}")
139
+ else:
140
+ log.log_info("Unreal Socket Server auto-start is disabled")
141
+
142
+ # Run initialization when this script is loaded
143
+ initialize_socket_server()
UMCP.it-Unreal-Organizer-Assistant/Content/Python/knowledge_base/how_to_use.md ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Note: This guide is not for humans, LLMs can refer it to understand how to use the Unreal Engine MCP Plugin.
2
+
3
+ # Unreal Engine MCP Plugin Guide
4
+
5
+ This plugin lets an LLM (like you, Claude) boss around Unreal Engine with MCP. Automates Blueprint wizard-ry, node hookups, all that jazz. Check this s to not f it up.
6
+
7
+ ## Key Notes
8
+
9
+ - **Pin Connections**: For inbuilt Events like BeginPlay Use "then" for execution pin, not "OutputDelegate" (delegates). Verify pin names in JSON.
10
+ - **Node Types**: Use `add_node_to_blueprint` with "EventBeginPlay", "Multiply_FloatFloat", or basics like "Branch", "Sequence". Unrecognized types return suggestions.
11
+ - **Node Spacing**: Set `node_position` in JSON (e.g., [0, 0], [400, 0])โ€”maintain 400x, 300y gaps to prevent overlap.
12
+ - **Inputs**: Use `add_input_binding` to set up the binding (e.g., "Jump", "SpaceBar"), then `add_node_to_blueprint` with "K2Node_InputAction" and `"action_name": "Jump"` in `node_properties`. Ensure `action_name` matches.
13
+ - **Colliders**: Add via `add_component_with_events` (e.g., "MyBox", "BoxComponent")โ€”returns `"begin_overlap_guid"` (BeginOverlap node) and `"end_overlap_guid"` (EndOverlap node).
14
+ - **Materials**: Use `edit_component_property` with property_name as "Material", "SetMaterial", or "BaseMaterial" and value as a material path (e.g., "'/Game/Materials/M_MyMaterial'") to set on mesh components (slot 0 default).
15
+
16
+ ## Enhanced Input System (UE 5.6+)
17
+
18
+ Legacy input bindings are deprecated. Use Enhanced Input instead:
19
+
20
+ 1. **Create Input Action**: `create_enhanced_input_action("IA_Jump", "/Game/Input")` - Creates an Input Action asset
21
+ 2. **Create Mapping Context**: `create_enhanced_input_mapping_context("IMC_Default", "/Game/Input")` - Creates a Mapping Context asset
22
+ 3. **Add Key Mappings**: Open the Mapping Context asset in Content Browser and add key mappings manually
23
+ 4. **Use in Blueprint**: Reference the Input Action and Mapping Context using Enhanced Input nodes
24
+
25
+ **Value Types**: "BOOLEAN" (default), "AXIS_1D" (float), "AXIS_2D" (Vector2D), "AXIS_3D" (Vector)
26
+
27
+ **Example**:
28
+ ```python
29
+ create_enhanced_input_action("IA_Jump", "/Game/Input", "BOOLEAN")
30
+ create_enhanced_input_mapping_context("IMC_Default", "/Game/Input")
31
+ # Then open IMC_Default in Content Browser and map IA_Jump to SpaceBar
32
+ ```
33
+
34
+ If `add_input_binding` fails with Enhanced Input error, it'll give you these instructions automatically.
35
+
36
+ ## UMG Text Properties
37
+
38
+ Text properties on UMG widgets (TextBlocks, Buttons, etc.) are auto-converted. Just pass the string value:
39
+
40
+ ```python
41
+ edit_widget_property("/Game/UI/WBP_Menu", "TitleText", "Text", "My Game Title")
42
+ ```
43
+
44
+ No need for INVTEXT() or NSLOCTEXT() macrosโ€”the plugin handles it. Works for all text widgets.
45
+
46
+ **Other UMG Properties**:
47
+ - **Position/Size**: `"(X=100.0,Y=200.0)"` or `"[100, 200]"`
48
+ - **Color**: `"(R=1.0,G=0.0,B=0.0,A=1.0)"`
49
+ - **Anchors**: `"(Minimum=(X=0.5,Y=0.0),Maximum=(X=0.5,Y=0.0))"`
50
+ - **Boolean**: `"true"` or `"false"`
51
+
52
+ ## Finding Component Names
53
+
54
+ Scene actor components are auto-numbered (e.g., "StaticMeshComponent0"). To discover available components:
55
+
56
+ **For Scene Actors**:
57
+ ```python
58
+ get_component_names(actor_name="Cube_1", is_scene_actor=True)
59
+ ```
60
+
61
+ **For Blueprints**:
62
+ ```python
63
+ get_component_names(blueprint_path="/Game/BP_Player")
64
+ ```
65
+
66
+ Returns JSON with all component names and their classes. Use these exact names in `edit_component_property`.
67
+
68
+ ## Finding Pin Names
69
+
70
+ When connecting Blueprint nodes, you need exact pin names. Get them with:
71
+
72
+ ```python
73
+ get_node_pin_names("/Game/BP_Test", "FUNCTION_GUID", "NODE_GUID")
74
+ ```
75
+
76
+ Returns JSON with:
77
+ - `input_pins`: Array of input pins with name, type, and is_array
78
+ - `output_pins`: Array of output pins with name, type, and is_array
79
+ - `node_type`: Class name of the node
80
+
81
+ Use these pin names in `connect_blueprint_nodes` or `connect_blueprint_nodes_bulk`.
82
+
83
+ **Example Workflow**:
84
+ 1. Add node โ†’ Get node GUID
85
+ 2. Call `get_node_pin_names` with that GUID
86
+ 3. Use returned pin names to connect nodes
87
+
88
+ ## JSON Response Format
89
+
90
+ All MCP commands return JSON strings with consistent structure:
91
+
92
+ **Success**:
93
+ ```json
94
+ {
95
+ "success": true,
96
+ "message": "Operation completed",
97
+ "data": { ... }
98
+ }
99
+ ```
100
+
101
+ **Failure**:
102
+ ```json
103
+ {
104
+ "success": false,
105
+ "error": "Error description",
106
+ "available_options": ["option1", "option2"],
107
+ "suggestion": "Try using option1"
108
+ }
109
+ ```
110
+
111
+ Error messages include suggestions and available options when applicable. Read themโ€”they're helpful.
112
+
113
+ ## Asset & World Organization
114
+
115
+ Comprehensive system for organizing Content Browser assets and World Outliner actors. All functions tested in UE 5.6.1.
116
+
117
+ ### Create Folder Structure
118
+
119
+ Create hierarchical folder structure in Content Browser:
120
+
121
+ ```python
122
+ create_folder_structure("/Game/MyProject", '{"Blueprints": ["Characters", "Weapons"], "Input": ["Actions", "Contexts"], "Materials": []}')
123
+ ```
124
+
125
+ Creates parent folders and subfolders in one command. Useful for new project setup.
126
+
127
+ ### Organize Assets by Type
128
+
129
+ Automatically organize assets into folders by class type:
130
+
131
+ ```python
132
+ organize_assets_by_type("/Game/Input", dry_run=True) # Preview what will happen
133
+ organize_assets_by_type("/Game/Input") # Actually move assets
134
+ ```
135
+
136
+ Moves Input Actions to `/Actions/`, Contexts to `/Contexts/`, etc. Supports custom organization rules and dry-run mode.
137
+
138
+ ### Organize World Outliner
139
+
140
+ Organize actors in World Outliner into folder hierarchy:
141
+
142
+ ```python
143
+ organize_world_outliner() # Auto-organize all actors by type
144
+ ```
145
+
146
+ Places Lights in `/Environment/Lighting/`, Static Meshes in `/Props/Static/`, PlayerStart in `/Gameplay/Spawn/`, etc.
147
+
148
+ ### Tag Assets
149
+
150
+ Add metadata tags to assets for search and filtering:
151
+
152
+ ```python
153
+ tag_assets('["/Game/Input/IA_Jump"]', '["Movement", "Character"]')
154
+ tag_assets('["/Game/Input/IA_FPS_Fire"]', '[]', auto_tag=True) # Auto-generate tags
155
+ ```
156
+
157
+ Auto-tag analyzes asset names and types to suggest relevant tags (FPS, Combat, Movement, etc.).
158
+
159
+ ### Search by Tags
160
+
161
+ Find assets with specific tags:
162
+
163
+ ```python
164
+ search_assets_by_tag("/Game/Input", '["Combat"]') # Find assets with Combat tag
165
+ search_assets_by_tag("/Game", '["FPS", "Combat"]', match_all=True) # Must have BOTH tags
166
+ ```
167
+
168
+ ### Organization Report
169
+
170
+ Generate comprehensive organization statistics:
171
+
172
+ ```python
173
+ generate_organization_report() # Full report of /Game
174
+ generate_organization_report('["/Game/Blueprints"]', include_world_outliner=False) # Specific folder
175
+ ```
176
+
177
+ Returns asset counts by type, folder statistics, and World Outliner organization percentage.
178
+
179
+ **Example Workflow - New Project Setup**:
180
+ ```python
181
+ # 1. Create folder structure
182
+ create_folder_structure("/Game/FPSGame", '{"Blueprints": ["Characters", "Weapons"], "Materials": [], "Input": ["Actions", "Contexts"]}')
183
+
184
+ # 2. Organize existing assets
185
+ organize_assets_by_type("/Game/Content", "/Game/FPSGame")
186
+
187
+ # 3. Organize World Outliner
188
+ organize_world_outliner()
189
+
190
+ # 4. Tag important assets
191
+ tag_assets('["/Game/Input/IA_Fire", "/Game/Input/IA_Reload"]', '["Combat", "FPS"]')
192
+
193
+ # 5. Generate report
194
+ generate_organization_report(["/Game/FPSGame"])
195
+ ```
196
+
197
+ ## Physics & Selection Commands
198
+
199
+ Quick commands for working with selected actors and enabling physics simulation. All functions tested in UE 5.6.1.
200
+
201
+ ### Get Selected Actors
202
+
203
+ Check what actors are currently selected in the viewport:
204
+
205
+ ```python
206
+ get_selected_actors()
207
+ ```
208
+
209
+ Returns JSON with actor names, classes, locations, and physics status. Useful before performing operations to verify selection.
210
+
211
+ **Response includes**:
212
+ - `name`: Actor name (e.g., "SM_Chair_1", "Cube_632")
213
+ - `class`: Actor class (e.g., "StaticMeshActor", "Blueprint")
214
+ - `location`: X/Y/Z coordinates
215
+ - `has_static_mesh`: Whether it has a static mesh component
216
+ - `physics_enabled`: Current physics simulation state
217
+
218
+ ### Enable Physics on Selected
219
+
220
+ Enable physics on all selected actors in one command:
221
+
222
+ ```python
223
+ enable_physics_on_selected() # Default: collision enabled, calculated mass
224
+ enable_physics_on_selected(mass_override=100.0) # Custom mass in kg
225
+ enable_physics_on_selected(enable_collision=False) # No collision
226
+ ```
227
+
228
+ **What it does**:
229
+ 1. Sets mobility to Movable
230
+ 2. Enables physics simulation
231
+ 3. Sets collision to QUERY_AND_PHYSICS (if `enable_collision=True`)
232
+ 4. Optionally overrides mass
233
+
234
+ Perfect for quick physics testing. Select objects in viewport, run command, press Simulate.
235
+
236
+ ### Enable Physics on Actor
237
+
238
+ Enable physics on specific actor by name (no selection needed):
239
+
240
+ ```python
241
+ enable_physics_on_actor("SM_Chair_1")
242
+ enable_physics_on_actor("Barrel_2", mass_override=50.0)
243
+ enable_physics_on_actor("Crate_1", enable_collision=False)
244
+ ```
245
+
246
+ Useful for scripted physics setup where you know actor names. Same settings as `enable_physics_on_selected` but targets named actors.
247
+
248
+ **Example Workflows**:
249
+
250
+ **Quick Testing**:
251
+ ```python
252
+ # Select object in viewport, make it physics-enabled
253
+ get_selected_actors() # Verify selection
254
+ enable_physics_on_selected() # Enable physics
255
+ # Press Simulate (Alt+S) to test
256
+ ```
257
+
258
+ **Batch Setup**:
259
+ ```python
260
+ # Enable physics on multiple props at once
261
+ # Select all props in viewport, then:
262
+ enable_physics_on_selected()
263
+ ```
264
+
265
+ **Scripted Setup**:
266
+ ```python
267
+ # Set up physics objects without selection
268
+ enable_physics_on_actor("Barrel_1")
269
+ enable_physics_on_actor("Barrel_2")
270
+ enable_physics_on_actor("Crate_1", mass_override=25.0) # Light crate
271
+ enable_physics_on_actor("Boulder_1", mass_override=500.0) # Heavy boulder
272
+ ```
273
+
274
+ **With Organization**:
275
+ ```python
276
+ # Organize physics props, then enable physics
277
+ organize_world_outliner({"StaticMeshActor": "/Physics/Props"})
278
+ enable_physics_on_actor("PhysicsBarrel_1")
279
+ tag_assets(["/Game/Props/Barrel"], ["Physics", "Destructible"])
280
+ ```
281
+
282
+ **Common Issues**:
283
+ - **"No StaticMeshComponent found"**: Actor doesn't have mesh (e.g., empty actor, light, camera)
284
+ - **Actor falls through floor**: Ensure floor has collision enabled (BlockAll)
285
+ - **Physics doesn't work**: Check mobility is Movable, collision enabled, not attached/constrained
286
+
287
+
288
+
289
+ *(Additional quirks will be added as discovered.)*
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mcp_server.py ADDED
@@ -0,0 +1,1911 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import socket
2
+ import json
3
+ import sys
4
+ import os
5
+ from mcp.server.fastmcp import FastMCP
6
+ import re
7
+ import mss
8
+ import base64
9
+ import tempfile # For creating a secure temporary file
10
+ from io import BytesIO
11
+ from pathlib import Path
12
+ from mcp.server.fastmcp import FastMCP, Image
13
+
14
+
15
+ # THIS FILE WILL RUN OUTSIDE THE UNREAL ENGINE SCOPE,
16
+ # DO NOT IMPORT UNREAL MODULES HERE OR EXECUTE IT IN THE UNREAL ENGINE PYTHON INTERPRETER
17
+
18
+ # Create a PID file to let the Unreal plugin know this process is running
19
+ def write_pid_file():
20
+ try:
21
+ pid = os.getpid()
22
+ pid_dir = os.path.join(os.path.expanduser("~"), ".unrealgenai")
23
+ os.makedirs(pid_dir, exist_ok=True)
24
+ pid_path = os.path.join(pid_dir, "mcp_server.pid")
25
+
26
+ with open(pid_path, "w") as f:
27
+ f.write(f"{pid}\n9877") # Store PID and port
28
+
29
+ # Register to delete the PID file on exit
30
+ import atexit
31
+ def cleanup_pid_file():
32
+ try:
33
+ if os.path.exists(pid_path):
34
+ os.remove(pid_path)
35
+ except:
36
+ pass
37
+
38
+ atexit.register(cleanup_pid_file)
39
+
40
+ return pid_path
41
+ except Exception as e:
42
+ print(f"Failed to write PID file: {e}", file=sys.stderr)
43
+ return None
44
+
45
+
46
+ # Write PID file on startup
47
+ pid_file = write_pid_file()
48
+ if pid_file:
49
+ print(f"MCP Server started with PID file at: {pid_file}", file=sys.stderr)
50
+
51
+ # Create an MCP server
52
+ mcp = FastMCP("UnrealHandshake")
53
+
54
+
55
+ # Function to send a message to Unreal Engine via socket
56
+ def send_to_unreal(command):
57
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
58
+ try:
59
+ s.connect(('localhost', 9877)) # Unreal listens on port 9877
60
+
61
+ # Ensure proper JSON encoding
62
+ json_str = json.dumps(command)
63
+ s.sendall(json_str.encode('utf-8'))
64
+
65
+ # Implement robust response handling
66
+ buffer_size = 8192 # Increased buffer size
67
+ response_data = b""
68
+
69
+ # Keep receiving data until we have complete JSON
70
+ while True:
71
+ chunk = s.recv(buffer_size)
72
+ if not chunk:
73
+ break
74
+
75
+ response_data += chunk
76
+
77
+ # Check if we have complete JSON
78
+ try:
79
+ json.loads(response_data.decode('utf-8'))
80
+ # If we get here, we have valid JSON
81
+ break
82
+ except json.JSONDecodeError:
83
+ # Need more data, continue receiving
84
+ continue
85
+
86
+ # Parse the complete response
87
+ if response_data:
88
+ return json.loads(response_data.decode('utf-8'))
89
+ else:
90
+ return {"success": False, "error": "No response received"}
91
+
92
+ except Exception as e:
93
+ print(f"Error sending to Unreal: {e}", file=sys.stderr)
94
+ return {"success": False, "error": str(e)}
95
+
96
+
97
+ @mcp.tool()
98
+ def how_to_use() -> str:
99
+ """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."""
100
+ try:
101
+ current_dir = Path(__file__).parent
102
+ md_file_path = current_dir / "knowledge_base" / "how_to_use.md"
103
+
104
+ if not md_file_path.exists():
105
+ return "Error: how_to_use.md not found in knowledge_base subfolder."
106
+
107
+ with open(md_file_path, "r", encoding="utf-8") as md_file:
108
+ return md_file.read()
109
+
110
+ except Exception as e:
111
+ return f"Error loading how_to_use.md: {str(e)}โ€”fix your shit."
112
+
113
+
114
+ @mcp.tool()
115
+ def ue5_naming_conventions() -> str:
116
+ """Get UE5 naming conventions and style guide. Use this to validate asset names, suggest proper prefixes, and ensure consistency with Unreal Engine 5 best practices. Essential for proper Input Action (IA_), Mapping Context (IMC_), Blueprint (BP_), Widget (WBP_), and all other asset naming."""
117
+ try:
118
+ current_dir = Path(__file__).parent
119
+ md_file_path = current_dir / "knowledge_base" / "ue5_naming_conventions.md"
120
+
121
+ if not md_file_path.exists():
122
+ return "Error: ue5_naming_conventions.md not found in knowledge_base subfolder."
123
+
124
+ with open(md_file_path, "r", encoding="utf-8") as md_file:
125
+ return md_file.read()
126
+
127
+ except Exception as e:
128
+ return f"Error loading ue5_naming_conventions.md: {str(e)}"
129
+
130
+
131
+ @mcp.tool()
132
+ def validate_ue5_asset_name(asset_name: str, asset_type: str) -> str:
133
+ """
134
+ Validate an asset name against UE5 naming conventions and provide suggestions.
135
+
136
+ Args:
137
+ asset_name: The asset name to validate (e.g., "jump action", "BP_Player", "IA_Fire")
138
+ asset_type: Type of asset - one of: "input_action", "mapping_context", "blueprint",
139
+ "widget", "material", "texture", "static_mesh", "skeletal_mesh",
140
+ "animation", "sound", "particle", "game_mode", "data_table"
141
+
142
+ Returns:
143
+ JSON string with validation results and suggestions
144
+ """
145
+ # Asset type to prefix mapping
146
+ prefix_map = {
147
+ "input_action": "IA_",
148
+ "mapping_context": "IMC_",
149
+ "blueprint": "BP_",
150
+ "widget": "WBP_",
151
+ "material": "M_",
152
+ "material_instance": "MI_",
153
+ "texture": "T_",
154
+ "static_mesh": "SM_",
155
+ "skeletal_mesh": "SK_",
156
+ "animation": "AS_",
157
+ "animation_blueprint": "ABP_",
158
+ "sound": "A_",
159
+ "particle": "NS_",
160
+ "game_mode": "GM_",
161
+ "data_table": "DT_",
162
+ "blueprint_interface": "BPI_",
163
+ "data_asset": "DA_"
164
+ }
165
+
166
+ expected_prefix = prefix_map.get(asset_type, "")
167
+
168
+ validation_result = {
169
+ "is_valid": True,
170
+ "issues": [],
171
+ "suggestions": [],
172
+ "corrected_name": asset_name
173
+ }
174
+
175
+ # Check for spaces
176
+ if " " in asset_name:
177
+ validation_result["is_valid"] = False
178
+ validation_result["issues"].append("Asset name contains spaces")
179
+ corrected = asset_name.replace(" ", "")
180
+ # Convert to PascalCase
181
+ words = asset_name.split()
182
+ corrected = "".join(word.capitalize() for word in words)
183
+ validation_result["suggestions"].append(f"Remove spaces and use PascalCase: {corrected}")
184
+ validation_result["corrected_name"] = corrected
185
+ asset_name = corrected
186
+
187
+ # Check for lowercase start (should be PascalCase)
188
+ if asset_name and asset_name[0].islower():
189
+ validation_result["is_valid"] = False
190
+ validation_result["issues"].append("Asset name should start with uppercase (PascalCase)")
191
+ corrected = asset_name[0].upper() + asset_name[1:]
192
+ validation_result["suggestions"].append(f"Use PascalCase: {corrected}")
193
+ validation_result["corrected_name"] = corrected
194
+ asset_name = corrected
195
+
196
+ # Check for correct prefix
197
+ if expected_prefix:
198
+ if not asset_name.startswith(expected_prefix):
199
+ validation_result["is_valid"] = False
200
+ validation_result["issues"].append(f"Missing expected prefix '{expected_prefix}' for {asset_type}")
201
+
202
+ # If it has a different prefix, replace it; otherwise add it
203
+ has_other_prefix = False
204
+ for prefix in prefix_map.values():
205
+ if asset_name.startswith(prefix):
206
+ # Replace wrong prefix
207
+ corrected = expected_prefix + asset_name[len(prefix):]
208
+ has_other_prefix = True
209
+ break
210
+
211
+ if not has_other_prefix:
212
+ corrected = expected_prefix + asset_name
213
+
214
+ validation_result["suggestions"].append(f"Add proper prefix: {corrected}")
215
+ validation_result["corrected_name"] = corrected
216
+
217
+ # Check for camelCase (should be PascalCase with underscores)
218
+ if asset_name and any(c.islower() and prev.isupper() for prev, c in zip(asset_name, asset_name[1:])):
219
+ validation_result["issues"].append("Avoid camelCase - use PascalCase with underscores between parts")
220
+
221
+ return json.dumps(validation_result, indent=2)
222
+
223
+
224
+ # Define basic tools for Claude to call
225
+
226
+ @mcp.tool()
227
+ def handshake_test(message: str) -> str:
228
+ """Send a handshake message to Unreal Engine"""
229
+ try:
230
+ command = {
231
+ "type": "handshake",
232
+ "message": message
233
+ }
234
+ response = send_to_unreal(command)
235
+ if response.get("success"):
236
+ return f"Handshake successful: {response['message']}"
237
+ else:
238
+ return f"Handshake failed: {response.get('error', 'Unknown error')}"
239
+ except Exception as e:
240
+ return f"Error communicating with Unreal: {str(e)}"
241
+
242
+
243
+ @mcp.tool()
244
+ def execute_python_script(script: str) -> str:
245
+ """
246
+ Execute a Python script within Unreal Engine's Python interpreter.
247
+
248
+ Args:
249
+ script: A string containing the Python code to execute in Unreal Engine.
250
+
251
+ Returns:
252
+ Message indicating success, failure, or a request for confirmation.
253
+
254
+ Note:
255
+ This tool sends the script to Unreal Engine, where it is executed via a temporary file using Unreal's internal
256
+ Python execution system (similar to GEngine->Exec). This method is stable but may not handle Blueprint-specific
257
+ APIs as seamlessly as direct Python API calls. For Blueprint manipulation, consider using dedicated tools like
258
+ `add_node_to_blueprint` or ensuring the script uses stable `unreal` module functions. Use this tool for Python
259
+ script execution instead of `execute_unreal_command` with 'py' commands.
260
+ """
261
+ try:
262
+ if is_potentially_destructive(script):
263
+ return ("This script appears to involve potentially destructive actions (e.g., deleting or saving files) "
264
+ "that were not explicitly requested. Please confirm if you want to proceed by saying 'Yes, execute it' "
265
+ "or modify your request to explicitly allow such actions.")
266
+
267
+ command = {
268
+ "type": "execute_python",
269
+ "script": script
270
+ }
271
+ response = send_to_unreal(command)
272
+ if response.get("success"):
273
+ output = response.get("output", "No output returned")
274
+ return f"Script executed successfully. Output: {output}"
275
+ else:
276
+ error = response.get("error", "Unknown error")
277
+ output = response.get("output", "")
278
+ if output:
279
+ error += f"\n\nPartial output before error: {output}"
280
+ return f"Failed to execute script: {response.get('error', 'Unknown error')}"
281
+ except Exception as e:
282
+ return f"Error sending script to Unreal: {str(e)}"
283
+
284
+
285
+ @mcp.tool()
286
+ def execute_unreal_command(command: str) -> str:
287
+ """
288
+ Execute an Unreal Engine command-line (CMD) command.
289
+
290
+ Args:
291
+ command: A string containing the Unreal Engine command to execute (e.g., "obj list", "stat fps").
292
+
293
+ Returns:
294
+ Message indicating success or failure, including any output or errors.
295
+
296
+ Note:
297
+ This tool executes commands directly in Unreal Engine's command system, similar to the editor's console.
298
+ It is intended for built-in editor commands (e.g., "stat fps", "obj list") and not for running Python scripts.
299
+ Do not use this tool with 'py' commands (e.g., "py script.py"); instead, use `execute_python_script` for Python
300
+ execution, which provides dedicated safety checks and output handling. Output capture is limited; for detailed
301
+ output, consider wrapping the command in a Python script with `execute_python_script`.
302
+ """
303
+ try:
304
+ # Check if the command is attempting to run a Python script
305
+ if command.strip().lower().startswith("py "):
306
+ return (
307
+ "Error: Use `execute_python_script` to run Python scripts instead of `execute_unreal_command` with 'py' commands. "
308
+ "For example, use `execute_python_script(script='your_code_here')` for Python execution.")
309
+
310
+ # Check for potentially destructive commands
311
+ destructive_keywords = ["delete", "save", "quit", "exit", "restart"]
312
+ if any(keyword in command.lower() for keyword in destructive_keywords):
313
+ return ("This command appears to involve potentially destructive actions (e.g., deleting or saving). "
314
+ "Please confirm by saying 'Yes, execute it' or explicitly request such actions.")
315
+
316
+ command_dict = {
317
+ "type": "execute_unreal_command",
318
+ "command": command
319
+ }
320
+ response = send_to_unreal(command_dict)
321
+ if response.get("success"):
322
+ output = response.get("output", "Command executed with no detailed output returned")
323
+ return f"Command '{command}' executed successfully. Output: {output}"
324
+ else:
325
+ return f"Failed to execute command '{command}': {response.get('error', 'Unknown error')}"
326
+ except Exception as e:
327
+ return f"Error sending command to Unreal: {str(e)}"
328
+
329
+
330
+ #
331
+ # Basic Object Commands
332
+ #
333
+
334
+ @mcp.tool()
335
+ def spawn_object(actor_class: str, location: list = [0, 0, 0], rotation: list = [0, 0, 0],
336
+ scale: list = [1, 1, 1], actor_label: str = None) -> str:
337
+ """
338
+ Spawn an object in the Unreal Engine level
339
+
340
+ Args:
341
+ actor_class: For basic shapes, use: "Cube", "Sphere", "Cylinder", or "Cone".
342
+ For other actors, use class name like "PointLight" or full path.
343
+ location: [X, Y, Z] coordinates
344
+ rotation: [Pitch, Yaw, Roll] in degrees
345
+ scale: [X, Y, Z] scale factors
346
+ actor_label: Optional custom name for the actor
347
+
348
+ Returns:
349
+ Message indicating success or failure
350
+ """
351
+ command = {
352
+ "type": "spawn",
353
+ "actor_class": actor_class,
354
+ "location": location,
355
+ "rotation": rotation,
356
+ "scale": scale,
357
+ "actor_label": actor_label
358
+ }
359
+
360
+ response = send_to_unreal(command)
361
+ if response.get("success"):
362
+ return f"Successfully spawned {actor_class}" + (f" with label '{actor_label}'" if actor_label else "")
363
+ else:
364
+ error = response.get('error', 'Unknown error')
365
+ # Add hint for Claude to understand what went wrong
366
+ if "not found" in error:
367
+ hint = "\nHint: For basic shapes, use 'Cube', 'Sphere', 'Cylinder', or 'Cone'. For other actors, try using '/Script/Engine.PointLight' format."
368
+ error += hint
369
+ return f"Failed to spawn object: {error}"
370
+
371
+
372
+ @mcp.tool()
373
+ def edit_component_property(blueprint_path: str, component_name: str, property_name: str, value: str,
374
+ is_scene_actor: bool = False, actor_name: str = "") -> str:
375
+ """
376
+ Edit a property of a component in a Blueprint or scene actor.
377
+
378
+ Args:
379
+ blueprint_path: Path to the Blueprint (e.g., "/Game/FlappyBird/BP_FlappyBird") or "" for scene actors
380
+ component_name: Name of the component (e.g., "BirdMesh", "RootComponent")
381
+ property_name: Name of the property to edit (e.g., "StaticMesh", "RelativeLocation")
382
+ value: New value as a string (e.g., "'/Engine/BasicShapes/Sphere.Sphere'", "100,200,300")
383
+ is_scene_actor: If True, edit a component on a scene actor (default: False)
384
+ actor_name: Name of the actor in the scene (required if is_scene_actor is True, e.g., "Cube_1")
385
+
386
+ Returns:
387
+ Message indicating success or failure, with optional property suggestions if the property is not found.
388
+
389
+ Capabilities:
390
+ - Set component properties in Blueprints (e.g., StaticMesh, bSimulatePhysics).
391
+ - Modify scene actor components (e.g., position, rotation, scale, material).
392
+ - Supports scalar types (float, int, bool), objects (e.g., materials), and vectors/rotators (e.g., "100,200,300" for FVector).
393
+ - Examples:
394
+ - Set a mesh: edit_component_property("/Game/FlappyBird/BP_FlappyBird", "BirdMesh", "StaticMesh", "'/Engine/BasicShapes/Sphere.Sphere'")
395
+ - Move an actor: edit_component_property("", "RootComponent", "RelativeLocation", "100,200,300", True, "Cube_1")
396
+ - Rotate an actor: edit_component_property("", "RootComponent", "RelativeRotation", "0,90,0", True, "Cube_1")
397
+ - Scale an actor: edit_component_property("", "RootComponent", "RelativeScale3D", "2,2,2", True, "Cube_1")
398
+ - Enable physics: edit_component_property("/Game/FlappyBird/BP_FlappyBird", "BirdMesh", "bSimulatePhysics", "true")
399
+ """
400
+ command = {
401
+ "type": "edit_component_property",
402
+ "blueprint_path": blueprint_path,
403
+ "component_name": component_name,
404
+ "property_name": property_name,
405
+ "value": value,
406
+ "is_scene_actor": is_scene_actor,
407
+ "actor_name": actor_name
408
+ }
409
+ response = send_to_unreal(command)
410
+
411
+ # CHANGED: Improved response handling to support both string and dict responses
412
+ try:
413
+ # Handle case where response is already a dict
414
+ if isinstance(response, dict):
415
+ result = response
416
+ # Handle case where response is a string
417
+ elif isinstance(response, str):
418
+ import json
419
+ result = json.loads(response)
420
+ else:
421
+ return f"Error: Unexpected response type: {type(response)}"
422
+
423
+ if result.get("success"):
424
+ return result.get("message", f"Set {property_name} of {component_name} to {value}")
425
+ else:
426
+ error = result.get("error", "Unknown error")
427
+ if "suggestions" in result:
428
+ error += f"\nSuggestions: {result['suggestions']}"
429
+ return f"Failed: {error}"
430
+ except Exception as e:
431
+ return f"Error: {str(e)}\nRaw response: {response}"
432
+
433
+
434
+ @mcp.tool()
435
+ def create_material(material_name: str, color: list) -> str:
436
+ """
437
+ Create a new material with the specified color
438
+
439
+ Args:
440
+ material_name: Name for the new material
441
+ color: [R, G, B] color values (0-1)
442
+
443
+ Returns:
444
+ Message indicating success or failure, and the material path if successful
445
+ """
446
+ command = {
447
+ "type": "create_material",
448
+ "material_name": material_name,
449
+ "color": color
450
+ }
451
+
452
+ response = send_to_unreal(command)
453
+ if response.get("success"):
454
+ return f"Successfully created material '{material_name}' with path: {response.get('material_path')}"
455
+ else:
456
+ return f"Failed to create material: {response.get('error', 'Unknown error')}"
457
+
458
+
459
+ #
460
+ # Blueprint Commands
461
+ #
462
+
463
+ @mcp.tool()
464
+ def create_blueprint(blueprint_name: str, parent_class: str = "Actor", save_path: str = "/Game/Blueprints") -> str:
465
+ """
466
+ Create a new Blueprint class
467
+
468
+ Args:
469
+ blueprint_name: Name for the new Blueprint
470
+ parent_class: Parent class name or path (e.g., "Actor", "/Script/Engine.Actor")
471
+ save_path: Path to save the Blueprint asset
472
+
473
+ Returns:
474
+ Message indicating success or failure
475
+ """
476
+ command = {
477
+ "type": "create_blueprint",
478
+ "blueprint_name": blueprint_name,
479
+ "parent_class": parent_class,
480
+ "save_path": save_path
481
+ }
482
+
483
+ response = send_to_unreal(command)
484
+ if response.get("success"):
485
+ return f"Successfully created Blueprint '{blueprint_name}' with path: {response.get('blueprint_path', save_path + '/' + blueprint_name)}"
486
+ else:
487
+ return f"Failed to create Blueprint: {response.get('error', 'Unknown error')}"
488
+
489
+ @mcp.tool()
490
+ def take_editor_screenshot(width: int = 1280, height: int = 720, quality: int = 70) -> Image:
491
+ """
492
+ Takes a screenshot of the primary monitor with compression to stay under MCP size limits.
493
+
494
+ Args:
495
+ width: Target width in pixels (default: 1280)
496
+ height: Target height in pixels (default: 720)
497
+ quality: JPEG quality 1-100 (default: 70, higher = larger file)
498
+
499
+ Returns:
500
+ Compressed screenshot image
501
+
502
+ Presets:
503
+ - Thumbnail: width=640, height=360, quality=60 (~50KB)
504
+ - Preview: width=1280, height=720, quality=70 (~200KB) [DEFAULT]
505
+ - Standard: width=1920, height=1080, quality=80 (~500KB)
506
+ """
507
+ temp_png_path = ""
508
+ temp_jpg_path = ""
509
+ try:
510
+ # 1. Capture raw screenshot to temporary PNG
511
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as fp:
512
+ temp_png_path = fp.name
513
+
514
+ with mss.mss() as sct:
515
+ sct.shot(mon=1, output=temp_png_path)
516
+
517
+ # 2. Open with PIL and resize/compress
518
+ try:
519
+ from PIL import Image as PILImage
520
+ except ImportError:
521
+ # Fallback: Return original if PIL not available
522
+ print("Warning: PIL not available, returning uncompressed screenshot")
523
+ with open(temp_png_path, "rb") as image_file:
524
+ image_bytes = image_file.read()
525
+ return Image(data=image_bytes, format="png")
526
+
527
+ # Open and resize
528
+ img = PILImage.open(temp_png_path)
529
+
530
+ # Calculate aspect-preserving resize
531
+ original_width, original_height = img.size
532
+ aspect_ratio = original_width / original_height
533
+ target_aspect = width / height
534
+
535
+ if aspect_ratio > target_aspect:
536
+ # Original is wider - fit to width
537
+ new_width = width
538
+ new_height = int(width / aspect_ratio)
539
+ else:
540
+ # Original is taller - fit to height
541
+ new_height = height
542
+ new_width = int(height * aspect_ratio)
543
+
544
+ # Resize with high-quality resampling
545
+ img_resized = img.resize((new_width, new_height), PILImage.Resampling.LANCZOS)
546
+
547
+ # Convert to RGB if needed (JPEG doesn't support transparency)
548
+ if img_resized.mode in ('RGBA', 'LA', 'P'):
549
+ background = PILImage.new('RGB', img_resized.size, (255, 255, 255))
550
+ if img_resized.mode == 'P':
551
+ img_resized = img_resized.convert('RGBA')
552
+ background.paste(img_resized, mask=img_resized.split()[-1] if img_resized.mode in ('RGBA', 'LA') else None)
553
+ img_resized = background
554
+ elif img_resized.mode != 'RGB':
555
+ img_resized = img_resized.convert('RGB')
556
+
557
+ # Save as JPEG with specified quality
558
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as fp:
559
+ temp_jpg_path = fp.name
560
+
561
+ img_resized.save(temp_jpg_path, 'JPEG', quality=quality, optimize=True)
562
+
563
+ # Read compressed image
564
+ with open(temp_jpg_path, "rb") as image_file:
565
+ image_bytes = image_file.read()
566
+
567
+ # Log size for debugging
568
+ size_kb = len(image_bytes) / 1024
569
+ print(f"Screenshot captured: {new_width}x{new_height}, {size_kb:.1f}KB, quality={quality}")
570
+
571
+ return Image(
572
+ data=image_bytes,
573
+ format="jpeg"
574
+ )
575
+
576
+ except Exception as e:
577
+ error_message = f"Screenshot compression failed: {str(e)}"
578
+ print(error_message, file=sys.stderr)
579
+ return error_message
580
+
581
+ finally:
582
+ # Clean up temporary files
583
+ for path in [temp_png_path, temp_jpg_path]:
584
+ if path and os.path.exists(path):
585
+ try:
586
+ os.remove(path)
587
+ except:
588
+ pass
589
+
590
+
591
+ @mcp.tool()
592
+ def add_component_to_blueprint(blueprint_path: str, component_class: str, component_name: str = None) -> str:
593
+ """
594
+ Add a component to a Blueprint
595
+
596
+ Args:
597
+ blueprint_path: Path to the Blueprint asset
598
+ component_class: Component class to add (e.g., "StaticMeshComponent", "PointLightComponent")
599
+ component_name: Name for the new component (optional)
600
+
601
+ Returns:
602
+ Message indicating success or failure
603
+ """
604
+ command = {
605
+ "type": "add_component",
606
+ "blueprint_path": blueprint_path,
607
+ "component_class": component_class,
608
+ "component_name": component_name
609
+ }
610
+
611
+ response = send_to_unreal(command)
612
+ if response.get("success"):
613
+ return f"Successfully added {component_class} to Blueprint at {blueprint_path}"
614
+ else:
615
+ return f"Failed to add component: {response.get('error', 'Unknown error')}"
616
+
617
+
618
+ @mcp.tool()
619
+ def add_variable_to_blueprint(blueprint_path: str, variable_name: str, variable_type: str,
620
+ default_value: str = None, category: str = "Default") -> str:
621
+ """
622
+ Add a variable to a Blueprint
623
+
624
+ Args:
625
+ blueprint_path: Path to the Blueprint asset
626
+ variable_name: Name for the new variable
627
+ variable_type: Type of the variable (e.g., "float", "vector", "boolean")
628
+ default_value: Default value for the variable (optional)
629
+ category: Category for organizing variables in the Blueprint editor (optional)
630
+
631
+ Returns:
632
+ Message indicating success or failure
633
+ """
634
+ # Convert default_value to string if it's a number
635
+ if default_value is not None and not isinstance(default_value, str):
636
+ default_value = str(default_value)
637
+
638
+ command = {
639
+ "type": "add_variable",
640
+ "blueprint_path": blueprint_path,
641
+ "variable_name": variable_name,
642
+ "variable_type": variable_type,
643
+ "default_value": default_value,
644
+ "category": category
645
+ }
646
+
647
+ response = send_to_unreal(command)
648
+ if response.get("success"):
649
+ return f"Successfully added {variable_type} variable '{variable_name}' to Blueprint at {blueprint_path}"
650
+ else:
651
+ return f"Failed to add variable: {response.get('error', 'Unknown error')}"
652
+
653
+
654
+ @mcp.tool()
655
+ def add_function_to_blueprint(blueprint_path: str, function_name: str,
656
+ inputs: list = None, outputs: list = None) -> str:
657
+ """
658
+ Add a function to a Blueprint
659
+
660
+ Args:
661
+ blueprint_path: Path to the Blueprint asset
662
+ function_name: Name for the new function
663
+ inputs: List of input parameters [{"name": "param1", "type": "float"}, ...]
664
+ outputs: List of output parameters [{"name": "return", "type": "boolean"}, ...]
665
+
666
+ Returns:
667
+ Message indicating success or failure
668
+ """
669
+ if inputs is None:
670
+ inputs = []
671
+ if outputs is None:
672
+ outputs = []
673
+
674
+ command = {
675
+ "type": "add_function",
676
+ "blueprint_path": blueprint_path,
677
+ "function_name": function_name,
678
+ "inputs": inputs,
679
+ "outputs": outputs
680
+ }
681
+
682
+ response = send_to_unreal(command)
683
+ if response.get("success"):
684
+ return f"Successfully added function '{function_name}' to Blueprint at {blueprint_path} with ID: {response.get('function_id', 'unknown')}"
685
+ else:
686
+ return f"Failed to add function: {response.get('error', 'Unknown error')}"
687
+
688
+
689
+ @mcp.tool()
690
+ def add_node_to_blueprint(blueprint_path: str, function_id: str, node_type: str,
691
+ node_position: list = [0, 0], node_properties: dict = None) -> str:
692
+ """
693
+ Add a node to a Blueprint graph
694
+
695
+ Args:
696
+ blueprint_path: Path to the Blueprint asset
697
+ function_id: ID of the function to add the node to
698
+ node_type: Type of node to add. Common supported types include:
699
+ - Basic nodes: "ReturnNode", "FunctionEntry", "Branch", "Sequence"
700
+ - Math operations: "Multiply", "Add", "Subtract", "Divide"
701
+ - Utilities: "PrintString", "Delay", "GetActorLocation", "SetActorLocation"
702
+ - For other functions, try using the exact function name from Blueprints
703
+ (e.g., "GetWorldLocation", "SpawnActorFromClass")
704
+ If the requested node type isn't found, the system will search for alternatives
705
+ and return suggestions. You can then use these suggestions in a new request.
706
+ node_position: Position of the node in the graph [X, Y]. **IMPORTANT**: Space nodes
707
+ at least 400 units apart horizontally and 300 units vertically to avoid overlap
708
+ and ensure a clean, organized graph (e.g., [0, 0], [400, 0], [800, 0] for a chain).
709
+ node_properties: Properties to set on the node (optional)
710
+
711
+ Returns:
712
+ On success: The node ID (GUID)
713
+ On failure: A response containing "SUGGESTIONS:" followed by alternative node types to try
714
+
715
+ Note:
716
+ Function libraries like KismetMathLibrary, KismetSystemLibrary, and KismetStringLibrary
717
+ contain most common Blueprint functions. If a simple node name doesn't work, try the
718
+ full function name, e.g., "Multiply_FloatFloat" instead of just "Multiply".
719
+ """
720
+ if node_properties is None:
721
+ node_properties = {}
722
+
723
+ command = {
724
+ "type": "add_node",
725
+ "blueprint_path": blueprint_path,
726
+ "function_id": function_id,
727
+ "node_type": node_type,
728
+ "node_position": node_position,
729
+ "node_properties": node_properties
730
+ }
731
+
732
+ response = send_to_unreal(command)
733
+ if response.get("success"):
734
+ return f"Successfully added {node_type} node to function {function_id} in Blueprint at {blueprint_path} with ID: {response.get('node_id', 'unknown')}"
735
+ else:
736
+ return f"Failed to add node: {response.get('error', 'Unknown error')}"
737
+
738
+
739
+ @mcp.tool()
740
+ def get_node_suggestions(node_type: str) -> str:
741
+ """
742
+ Get suggestions for a node type in Unreal Blueprints
743
+
744
+ Args:
745
+ node_type: The partial or full node type to get suggestions for (e.g., "Add", "FloatToDouble")
746
+
747
+ Returns:
748
+ A string indicating success with suggestions or an error message
749
+ """
750
+ command = {
751
+ "type": "get_node_suggestions",
752
+ "node_type": node_type
753
+ }
754
+
755
+ response = send_to_unreal(command)
756
+ if response.get("success"):
757
+ suggestions = response.get("suggestions", [])
758
+ if suggestions:
759
+ return f"Suggestions for '{node_type}': {', '.join(suggestions)}"
760
+ else:
761
+ return f"No suggestions found for '{node_type}'"
762
+ else:
763
+ error = response.get("error", "Unknown error")
764
+ return f"Failed to get suggestions for '{node_type}': {error}"
765
+
766
+
767
+ @mcp.tool()
768
+ def delete_node_from_blueprint(blueprint_path: str, function_id: str, node_id: str) -> str:
769
+ """
770
+ Delete a node from a Blueprint graph
771
+
772
+ Args:
773
+ blueprint_path: Path to the Blueprint asset
774
+ function_id: ID of the function containing the node
775
+ node_id: ID of the node to delete
776
+
777
+ Returns:
778
+ Success or failure message
779
+ """
780
+ command = {
781
+ "type": "delete_node",
782
+ "blueprint_path": blueprint_path,
783
+ "function_id": function_id,
784
+ "node_id": node_id
785
+ }
786
+
787
+ response = send_to_unreal(command)
788
+ if response.get("success"):
789
+ return f"Successfully deleted node {node_id} from function {function_id} in Blueprint at {blueprint_path}"
790
+ else:
791
+ return f"Failed to delete node: {response.get('error', 'Unknown error')}"
792
+
793
+
794
+ @mcp.tool()
795
+ def get_all_nodes_in_graph(blueprint_path: str, function_id: str) -> str:
796
+ """
797
+ Get all nodes in a Blueprint graph with their positions and types
798
+
799
+ Args:
800
+ blueprint_path: Path to the Blueprint asset
801
+ function_id: ID of the function to get nodes from
802
+
803
+ Returns:
804
+ JSON string containing all nodes with their GUIDs, types, and positions
805
+ """
806
+ command = {
807
+ "type": "get_all_nodes",
808
+ "blueprint_path": blueprint_path,
809
+ "function_id": function_id
810
+ }
811
+
812
+ response = send_to_unreal(command)
813
+ if response.get("success"):
814
+ return json.dumps(response.get("nodes", []))
815
+ else:
816
+ return f"Failed to get nodes: {response.get('error', 'Unknown error')}"
817
+
818
+
819
+ @mcp.tool()
820
+ def connect_blueprint_nodes(blueprint_path: str, function_id: str,
821
+ source_node_id: str, source_pin: str,
822
+ target_node_id: str, target_pin: str) -> str:
823
+ command = {
824
+ "type": "connect_nodes",
825
+ "blueprint_path": blueprint_path,
826
+ "function_id": function_id,
827
+ "source_node_id": source_node_id,
828
+ "source_pin": source_pin,
829
+ "target_node_id": target_node_id,
830
+ "target_pin": target_pin
831
+ }
832
+
833
+ response = send_to_unreal(command)
834
+ if response.get("success"):
835
+ return f"Successfully connected {source_node_id}.{source_pin} to {target_node_id}.{target_pin} in Blueprint at {blueprint_path}"
836
+ else:
837
+ error = response.get("error", "Unknown error")
838
+ if "source_available_pins" in response and "target_available_pins" in response:
839
+ error += f"\nAvailable pins on source ({source_node_id}): {json.dumps(response['source_available_pins'], indent=2)}"
840
+ error += f"\nAvailable pins on target ({target_node_id}): {json.dumps(response['target_available_pins'], indent=2)}"
841
+ return f"Failed to connect nodes: {error}"
842
+
843
+
844
+ @mcp.tool()
845
+ def compile_blueprint(blueprint_path: str) -> str:
846
+ """
847
+ Compile a Blueprint
848
+
849
+ Args:
850
+ blueprint_path: Path to the Blueprint asset
851
+
852
+ Returns:
853
+ Message indicating success or failure
854
+ """
855
+ command = {
856
+ "type": "compile_blueprint",
857
+ "blueprint_path": blueprint_path
858
+ }
859
+
860
+ response = send_to_unreal(command)
861
+ if response.get("success"):
862
+ return f"Successfully compiled Blueprint at {blueprint_path}"
863
+ else:
864
+ return f"Failed to compile Blueprint: {response.get('error', 'Unknown error')}"
865
+
866
+
867
+ @mcp.tool()
868
+ def spawn_blueprint_actor(blueprint_path: str, location: list = [0, 0, 0],
869
+ rotation: list = [0, 0, 0], scale: list = [1, 1, 1],
870
+ actor_label: str = None) -> str:
871
+ """
872
+ Spawn a Blueprint actor in the level
873
+
874
+ Args:
875
+ blueprint_path: Path to the Blueprint asset
876
+ location: [X, Y, Z] coordinates
877
+ rotation: [Pitch, Yaw, Roll] in degrees
878
+ scale: [X, Y, Z] scale factors
879
+ actor_label: Optional custom name for the actor
880
+
881
+ Returns:
882
+ Message indicating success or failure
883
+ """
884
+ command = {
885
+ "type": "spawn_blueprint",
886
+ "blueprint_path": blueprint_path,
887
+ "location": location,
888
+ "rotation": rotation,
889
+ "scale": scale,
890
+ "actor_label": actor_label
891
+ }
892
+
893
+ response = send_to_unreal(command)
894
+ if response.get("success"):
895
+ return f"Successfully spawned Blueprint {blueprint_path}" + (
896
+ f" with label '{actor_label}'" if actor_label else "")
897
+ else:
898
+ return f"Failed to spawn Blueprint: {response.get('error', 'Unknown error')}"
899
+
900
+
901
+ # @mcp.tool()
902
+ # def add_nodes_to_blueprint_bulk(blueprint_path: str, function_id: str, nodes: list) -> str:
903
+ # """
904
+ # Add multiple nodes to a Blueprint graph in a single operation
905
+ #
906
+ # Args:
907
+ # blueprint_path: Path to the Blueprint asset
908
+ # function_id: ID of the function to add the nodes to
909
+ # nodes: Array of node definitions, each containing:
910
+ # - id: ID for referencing the node (string) - this is important for creating connections later
911
+ # - node_type: Type of node to add (see add_node_to_blueprint for supported types)
912
+ # - node_position: Position of the node in the graph [X, Y]
913
+ # - node_properties: Properties to set on the node (optional)
914
+ #
915
+ # Returns:
916
+ # On success: Dictionary mapping your node IDs to the actual node GUIDs created in Unreal
917
+ # On partial success: Dictionary with successful nodes and suggestions for failed nodes
918
+ # On failure: Error message with suggestions
919
+ #
920
+ # Example success response:
921
+ # {
922
+ # "success": true,
923
+ # "nodes": {
924
+ # "function_entry": "425E7A3949D7420A461175A4733BBA5C",
925
+ # "multiply_node": "70354A7E444BB68EEF31718DC50CF89C",
926
+ # "return_node": "6436796645ED674F3C64A8A94CBA416C"
927
+ # }
928
+ # }
929
+ #
930
+ # Example partial success with suggestions:
931
+ # {
932
+ # "success": true,
933
+ # "partial_success": true,
934
+ # "nodes": {
935
+ # "function_entry": "425E7A3949D7420A461175A4733BBA5C",
936
+ # "return_node": "6436796645ED674F3C64A8A94CBA416C"
937
+ # },
938
+ # "suggestions": {
939
+ # "multiply_node": {
940
+ # "requested_type": "Multiply_Float",
941
+ # "suggestions": ["KismetMathLibrary.Multiply_FloatFloat", "KismetMathLibrary.MultiplyByFloat"]
942
+ # }
943
+ # }
944
+ # }
945
+ #
946
+ # When you receive suggestions, you can retry adding those nodes using the suggested node types.
947
+ # """
948
+ # command = {
949
+ # "type": "add_nodes_bulk",
950
+ # "blueprint_path": blueprint_path,
951
+ # "function_id": function_id,
952
+ # "nodes": nodes
953
+ # }
954
+ #
955
+ # response = send_to_unreal(command)
956
+ # if response.get("success"):
957
+ # node_mapping = response.get("nodes", {})
958
+ # return f"Successfully added {len(node_mapping)} nodes to function {function_id} in Blueprint at {blueprint_path}\nNode mapping: {json.dumps(node_mapping, indent=2)}"
959
+ # else:
960
+ # return f"Failed to add nodes: {response.get('error', 'Unknown error')}"
961
+
962
+ @mcp.tool()
963
+ def add_component_with_events(blueprint_path: str, component_name: str, component_class: str) -> str:
964
+ """
965
+ Add a component to a Blueprint with overlap events if applicable.
966
+
967
+ Args:
968
+ blueprint_path: Path to the Blueprint (e.g., "/Game/FlappyBird/BP_FlappyBird")
969
+ component_name: Name of the new component (e.g., "TriggerBox")
970
+ component_class: Class of the component (e.g., "BoxComponent")
971
+
972
+ Returns:
973
+ Message with success, error, and event GUIDs if created
974
+ """
975
+ command = {
976
+ "type": "add_component_with_events",
977
+ "blueprint_path": blueprint_path,
978
+ "component_name": component_name,
979
+ "component_class": component_class
980
+ }
981
+ response = send_to_unreal(command)
982
+ if response.get("success"):
983
+ msg = response.get("message", f"Added component {component_name}")
984
+ # Include event GUIDs in response if available
985
+ if "begin_overlap_guid" in response and "end_overlap_guid" in response:
986
+ msg += f"\nOverlap Events - Begin GUID: {response['begin_overlap_guid']}, End GUID: {response['end_overlap_guid']}"
987
+ return msg
988
+ return f"Failed: {response.get('error', 'Unknown error')}"
989
+
990
+
991
+ @mcp.tool()
992
+ def connect_blueprint_nodes_bulk(blueprint_path: str, function_id: str, connections: list) -> str:
993
+ """
994
+ Connect multiple pairs of nodes in a Blueprint graph
995
+
996
+ Args:
997
+ blueprint_path: Path to the Blueprint asset
998
+ function_id: ID of the function containing the nodes
999
+ connections: Array of connection definitions, each containing:
1000
+ - source_node_id: ID of the source node
1001
+ - source_pin: Name of the source pin
1002
+ - target_node_id: ID of the target node
1003
+ - target_pin: Name of the target pin
1004
+
1005
+ Returns:
1006
+ Message indicating success or failure, with details on which connections succeeded or failed
1007
+ """
1008
+ command = {
1009
+ "type": "connect_nodes_bulk",
1010
+ "blueprint_path": blueprint_path,
1011
+ "function_id": function_id,
1012
+ "connections": connections
1013
+ }
1014
+
1015
+ response = send_to_unreal(command)
1016
+
1017
+ # Handle the new detailed response format
1018
+ if response.get("success"):
1019
+ successful = response.get("successful_connections", 0)
1020
+ total = response.get("total_connections", 0)
1021
+ return f"Successfully connected {successful}/{total} node pairs in Blueprint at {blueprint_path}"
1022
+ else:
1023
+ # Extract detailed error information
1024
+ error_message = response.get("error", "Unknown error")
1025
+ failed_connections = []
1026
+
1027
+ # Look for detailed results in the response
1028
+ if "results" in response:
1029
+ for result in response.get("results", []):
1030
+ if not result.get("success", False):
1031
+ idx = result.get("connection_index", -1)
1032
+ src = result.get("source_node", "unknown")
1033
+ tgt = result.get("target_node", "unknown")
1034
+ err = result.get("error", "unknown error")
1035
+ failed_connections.append(f"Connection {idx}: {src} to {tgt} - {err}")
1036
+
1037
+ # Format error message with details
1038
+ if failed_connections:
1039
+ detailed_errors = "\n- " + "\n- ".join(failed_connections)
1040
+ return f"Failed to connect nodes: {error_message}{detailed_errors}"
1041
+ else:
1042
+ return f"Failed to connect nodes: {error_message}"
1043
+
1044
+
1045
+ @mcp.tool()
1046
+ def get_blueprint_node_guid(blueprint_path: str, graph_type: str = "EventGraph", node_name: str = None,
1047
+ function_id: str = None) -> str:
1048
+ """
1049
+ Retrieve the GUID of a pre-existing node in a Blueprint graph.
1050
+
1051
+ Args:
1052
+ blueprint_path: Path to the Blueprint asset (e.g., "/Game/Blueprints/TestBulkBlueprint")
1053
+ graph_type: Type of graph to query ("EventGraph" or "FunctionGraph", default: "EventGraph")
1054
+ node_name: Name of the node to find (e.g., "BeginPlay" for EventGraph, optional if using function_id)
1055
+ function_id: ID of the function to get the FunctionEntry node for (optional, used with graph_type="FunctionGraph")
1056
+
1057
+ Returns:
1058
+ Message with the node's GUID or an error if not found
1059
+ """
1060
+ command = {
1061
+ "type": "get_node_guid",
1062
+ "blueprint_path": blueprint_path,
1063
+ "graph_type": graph_type,
1064
+ "node_name": node_name if node_name else "",
1065
+ "function_id": function_id if function_id else ""
1066
+ }
1067
+
1068
+ response = send_to_unreal(command)
1069
+ if response.get("success"):
1070
+ guid = response.get("node_guid")
1071
+ return f"Node GUID for {node_name or 'FunctionEntry'} in {graph_type} of {blueprint_path}: {guid}"
1072
+ else:
1073
+ return f"Failed to get node GUID: {response.get('error', 'Unknown error')}"
1074
+
1075
+
1076
+ # Safety check for potentially destructive actions
1077
+ def is_potentially_destructive(script: str) -> bool:
1078
+ """
1079
+ Check if the script contains potentially destructive actions like deleting or saving files.
1080
+ Returns True if such actions are detected and not explicitly requested.
1081
+ """
1082
+ destructive_keywords = [
1083
+ r'unreal\.EditorAssetLibrary\.delete_asset',
1084
+ r'unreal\.EditorLevelLibrary\.destroy_actor',
1085
+ r'unreal\.save_package',
1086
+ r'os\.remove',
1087
+ r'shutil\.rmtree',
1088
+ r'file\.write',
1089
+ r'unreal\.EditorAssetLibrary\.save_asset'
1090
+ ]
1091
+ for keyword in destructive_keywords:
1092
+ if re.search(keyword, script, re.IGNORECASE):
1093
+ return True
1094
+ return False
1095
+
1096
+
1097
+ # Scene Control
1098
+ @mcp.tool()
1099
+ def get_all_scene_objects() -> str:
1100
+ """
1101
+ Retrieve all actors in the current Unreal Engine level, categorized by type.
1102
+
1103
+ Returns a comprehensive scene summary including:
1104
+ - Total actor count
1105
+ - Actors organized by class type (LandscapeStreamingProxy, WorldPartitionHLOD, etc.)
1106
+ - Example actor names and locations for each category
1107
+ - Sorted by most common actor types first
1108
+
1109
+ This is perfect for understanding what's in your level and finding specific actors.
1110
+
1111
+ Returns:
1112
+ JSON with categorized actors, summary text, and total count
1113
+ """
1114
+ command = {"type": "get_all_scene_objects"}
1115
+ response = send_to_unreal(command)
1116
+
1117
+ if response.get("success"):
1118
+ # Return the formatted summary for easy reading
1119
+ return response.get("summary", json.dumps(response))
1120
+ else:
1121
+ return f"Failed: {response.get('error', 'Unknown error')}"
1122
+
1123
+
1124
+ # Project Control
1125
+ @mcp.tool()
1126
+ def create_project_folder(folder_path: str) -> str:
1127
+ """
1128
+ Create a new folder in the Unreal project content directory.
1129
+
1130
+ Args:
1131
+ folder_path: Path relative to /Game (e.g., "FlappyBird/Assets")
1132
+ """
1133
+ command = {"type": "create_project_folder", "folder_path": folder_path}
1134
+ response = send_to_unreal(command)
1135
+ return response.get("message", f"Failed: {response.get('error', 'Unknown error')}")
1136
+
1137
+
1138
+ @mcp.tool()
1139
+ def get_files_in_folder(folder_path: str) -> str:
1140
+ """
1141
+ List all files in a specified project folder.
1142
+
1143
+ Args:
1144
+ folder_path: Path relative to /Game (e.g., "FlappyBird/Assets")
1145
+ """
1146
+ command = {"type": "get_files_in_folder", "folder_path": folder_path}
1147
+ response = send_to_unreal(command)
1148
+ return json.dumps(response.get("files", [])) if response.get("success") else f"Failed: {response.get('error')}"
1149
+
1150
+
1151
+ @mcp.tool()
1152
+ def create_game_mode(game_mode_path: str, pawn_blueprint_path: str, base_class: str = "GameModeBase") -> str:
1153
+ """Create a game mode Blueprint, set its default pawn, and assign it as the current sceneโ€™s default game mode.
1154
+ Args:
1155
+ game_mode_path: Path for new game mode (e.g., "/Game/MyGameMode")
1156
+ pawn_blueprint_path: Path to pawn Blueprint (e.g., "/Game/Blueprints/BP_Player")
1157
+ base_class: Base class for game mode (default: "GameModeBase")
1158
+ """
1159
+ command = {
1160
+ "type": "create_game_mode",
1161
+ "game_mode_path": game_mode_path,
1162
+ "pawn_blueprint_path": pawn_blueprint_path,
1163
+ "base_class": base_class
1164
+ }
1165
+ response = send_to_unreal(command)
1166
+ if response.get("success"):
1167
+ return response.get("message", f"Created game mode at {game_mode_path}")
1168
+ else:
1169
+ return f"Failed: {response.get('error', 'Unknown error')}"
1170
+
1171
+
1172
+ @mcp.tool()
1173
+ def add_widget_to_user_widget(user_widget_path: str, widget_type: str, widget_name: str,
1174
+ parent_widget_name: str = "") -> str:
1175
+ """
1176
+ Adds a new widget (like TextBlock, Button, Image, CanvasPanel, VerticalBox) to a User Widget Blueprint.
1177
+
1178
+ Args:
1179
+ user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
1180
+ widget_type: Class name of the widget to add (e.g., "TextBlock", "Button", "Image", "CanvasPanel", "VerticalBox", "HorizontalBox", "SizeBox", "Border"). Case-sensitive.
1181
+ widget_name: A unique desired name for the new widget variable (e.g., "TitleText", "StartButton", "PlayerHealthBar"). The actual name might get adjusted for uniqueness.
1182
+ 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.
1183
+
1184
+ Returns:
1185
+ JSON string indicating success (with actual widget name) or failure with an error message.
1186
+ """
1187
+ command = {
1188
+ "type": "add_widget_to_user_widget",
1189
+ "user_widget_path": user_widget_path,
1190
+ "widget_type": widget_type,
1191
+ "widget_name": widget_name,
1192
+ "parent_widget_name": parent_widget_name
1193
+ }
1194
+ # send_to_unreal already returns a dict
1195
+ response = send_to_unreal(command)
1196
+ # Return a user-friendly string summary
1197
+ if response.get("success"):
1198
+ actual_name = response.get("widget_name", widget_name)
1199
+ return response.get("message",
1200
+ f"Successfully added widget '{actual_name}' of type '{widget_type}' to '{user_widget_path}'.")
1201
+ else:
1202
+ return f"Failed to add widget: {response.get('error', 'Unknown error')}"
1203
+
1204
+
1205
+ @mcp.tool()
1206
+ def edit_widget_property(user_widget_path: str, widget_name: str, property_name: str, value: str) -> str:
1207
+ """
1208
+ Edits a property of a specific widget within a User Widget Blueprint.
1209
+
1210
+ Args:
1211
+ user_widget_path: Path to the User Widget Blueprint (e.g., "/Game/UI/WBP_MainMenu").
1212
+ widget_name: The name of the widget inside the User Widget whose property you want to change (e.g., "TitleText", "StartButton").
1213
+ 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"). Case-sensitive.
1214
+ value: The new value for the property, formatted as a string EXACTLY as Unreal expects for ImportText. Examples:
1215
+ - Text: '"Hello World!"' (Note: String literal requires inner quotes)
1216
+ - Float: '150.0'
1217
+ - Integer: '10'
1218
+ - Boolean: 'true' or 'false'
1219
+ - LinearColor: '(R=1.0,G=0.0,B=0.0,A=1.0)'
1220
+ - Vector2D (for Size, Position): '(X=200.0,Y=50.0)'
1221
+ - Anchors: '(Minimum=(X=0.5,Y=0.0),Maximum=(X=0.5,Y=0.0))' (Top Center Anchor)
1222
+ - Alignment (Vector2D): '(X=0.5,Y=0.5)' (Center Alignment)
1223
+ - Font (FSlateFontInfo): "(FontObject=Font'/Engine/EngineFonts/Roboto.Roboto',Size=24)"
1224
+ - Texture (Object Path): "Texture2D'/Game/Textures/MyIcon.MyIcon'"
1225
+ - Enum (e.g., Stretch): 'ScaleToFit'
1226
+
1227
+ Returns:
1228
+ JSON string indicating success or failure with an error message.
1229
+ """
1230
+ command = {
1231
+ "type": "edit_widget_property",
1232
+ "user_widget_path": user_widget_path,
1233
+ "widget_name": widget_name,
1234
+ "property_name": property_name,
1235
+ "value": value # Pass the string value directly
1236
+ }
1237
+ # Use json.loads to parse the JSON string returned by send_to_unreal
1238
+ response_str = send_to_unreal(command)
1239
+ try:
1240
+ response_dict = json.loads(response_str)
1241
+ # Return a user-friendly string summary
1242
+ if response_dict.get("success"):
1243
+ return response_dict.get("message",
1244
+ f"Successfully set property '{property_name}' on widget '{widget_name}'.")
1245
+ else:
1246
+ return f"Failed to edit widget property: {response_dict.get('error', 'Unknown C++ error')}"
1247
+ except json.JSONDecodeError:
1248
+ return f"Failed to parse response from Unreal: {response_str}"
1249
+ except Exception as e:
1250
+ # Catch potential errors if send_to_unreal itself failed before returning JSON
1251
+ if isinstance(response_str, dict) and not response_str.get("success"):
1252
+ return f"Failed to send command: {response_str.get('error', 'Unknown MCP error')}"
1253
+ return f"An unexpected error occurred: {str(e)} Response: {response_str}"
1254
+
1255
+
1256
+ # Input
1257
+ @mcp.tool()
1258
+ def add_input_binding(action_name: str, key: str) -> str:
1259
+ """
1260
+ Add an input action binding to Project Settings.
1261
+
1262
+ Args:
1263
+ action_name: Name of the action (e.g., "Flap")
1264
+ key: Key to bind (e.g., "Space Bar")
1265
+ """
1266
+ command = {"type": "add_input_binding", "action_name": action_name, "key": key}
1267
+ response = send_to_unreal(command)
1268
+ return json.dumps(response) if not response.get("success") else response.get("message", "Success")
1269
+
1270
+ @mcp.tool()
1271
+ def create_enhanced_input_action(action_name: str, action_path: str = "/Game/Input", value_type: str = "BOOLEAN") -> str:
1272
+ """
1273
+ Create an Enhanced Input Action asset (UE 5.6+).
1274
+
1275
+ Args:
1276
+ action_name: Name of the action (e.g., "IA_Jump")
1277
+ action_path: Path to save the asset (default: "/Game/Input")
1278
+ value_type: "BOOLEAN", "AXIS_1D", "AXIS_2D", or "AXIS_3D" (default: "BOOLEAN")
1279
+
1280
+ Returns:
1281
+ Message indicating success with asset path or error details
1282
+ """
1283
+ command = {
1284
+ "type": "create_enhanced_input_action",
1285
+ "action_name": action_name,
1286
+ "action_path": action_path,
1287
+ "value_type": value_type
1288
+ }
1289
+ response = send_to_unreal(command)
1290
+ if response.get("success"):
1291
+ return f"Created Input Action '{action_name}' at {response.get('asset_path')}"
1292
+ return json.dumps(response)
1293
+
1294
+ @mcp.tool()
1295
+ def create_enhanced_input_mapping_context(context_name: str, context_path: str = "/Game/Input") -> str:
1296
+ """
1297
+ Create an Enhanced Input Mapping Context asset (UE 5.6+).
1298
+
1299
+ Args:
1300
+ context_name: Name of the mapping context (e.g., "IMC_Default")
1301
+ context_path: Path to save the asset (default: "/Game/Input")
1302
+
1303
+ Returns:
1304
+ Message indicating success with asset path or error details
1305
+ """
1306
+ command = {
1307
+ "type": "create_enhanced_input_mapping_context",
1308
+ "context_name": context_name,
1309
+ "context_path": context_path
1310
+ }
1311
+ response = send_to_unreal(command)
1312
+ if response.get("success"):
1313
+ return f"Created Mapping Context '{context_name}' at {response.get('asset_path')}\n{response.get('next_step', '')}"
1314
+ return json.dumps(response)
1315
+
1316
+
1317
+ # --- ORGANIZATION TOOLS ---
1318
+
1319
+ @mcp.tool()
1320
+ def create_folder_structure(base_path: str = "/Game", folder_structure: str = "{}") -> str:
1321
+ """
1322
+ Create a folder hierarchy in Content Browser.
1323
+
1324
+ Args:
1325
+ base_path: Base path for folder creation (default: "/Game")
1326
+ folder_structure: JSON string - dict or list defining folder hierarchy
1327
+ Examples:
1328
+ - List: '["Blueprints", "Materials", "Meshes"]'
1329
+ - Dict: '{"Blueprints": ["Characters", "Weapons"], "Materials": []}'
1330
+
1331
+ Returns:
1332
+ JSON with created folders and status
1333
+
1334
+ Example:
1335
+ create_folder_structure("/Game/MyProject", '{"Blueprints": ["Characters", "Weapons"], "Input": ["Actions", "Contexts"]}')
1336
+ """
1337
+ try:
1338
+ folder_struct = json.loads(folder_structure) if folder_structure else None
1339
+ except json.JSONDecodeError:
1340
+ return json.dumps({"success": False, "error": "Invalid JSON in folder_structure parameter"})
1341
+
1342
+ command = {
1343
+ "type": "create_folder_structure",
1344
+ "base_path": base_path,
1345
+ "folder_structure": folder_struct
1346
+ }
1347
+ response = send_to_unreal(command)
1348
+ return json.dumps(response, indent=2)
1349
+
1350
+
1351
+ @mcp.tool()
1352
+ def organize_assets_by_type(source_folder: str, target_base: str = "", organization_rules: str = "{}", dry_run: bool = False) -> str:
1353
+ """
1354
+ Automatically organize assets into folders by type.
1355
+
1356
+ Args:
1357
+ source_folder: Folder to scan for assets (e.g., "/Game/Input")
1358
+ target_base: Base path for organized folders (default: source + "_Organized")
1359
+ organization_rules: JSON string - custom rules {"ClassName": "target/path"} (optional)
1360
+ dry_run: If true, don't move assets, just report what would happen (default: false)
1361
+
1362
+ Returns:
1363
+ JSON with organization results
1364
+
1365
+ Example:
1366
+ organize_assets_by_type("/Game/Input", dry_run=True)
1367
+ organize_assets_by_type("/Game/MyAssets", "/Game/Organized")
1368
+ """
1369
+ try:
1370
+ rules = json.loads(organization_rules) if organization_rules else None
1371
+ except json.JSONDecodeError:
1372
+ return json.dumps({"success": False, "error": "Invalid JSON in organization_rules parameter"})
1373
+
1374
+ command = {
1375
+ "type": "organize_assets_by_type",
1376
+ "source_folder": source_folder,
1377
+ "target_base": target_base if target_base else None,
1378
+ "organization_rules": rules,
1379
+ "dry_run": dry_run
1380
+ }
1381
+ response = send_to_unreal(command)
1382
+ return json.dumps(response, indent=2)
1383
+
1384
+
1385
+ @mcp.tool()
1386
+ def organize_world_outliner(organization_rules: str = "{}", target_actors: str = "[]") -> str:
1387
+ """
1388
+ Organize actors in World Outliner into folder hierarchy.
1389
+
1390
+ Args:
1391
+ organization_rules: JSON string - dict mapping actor classes to folder paths (optional)
1392
+ Example: '{"Light": "/Environment/Lighting", "StaticMesh": "/Props/Static"}'
1393
+ target_actors: JSON string - list of specific actor names to organize (optional)
1394
+ Example: '["Cube_1", "Sphere_2"]'
1395
+
1396
+ Returns:
1397
+ JSON with organization results
1398
+
1399
+ Example:
1400
+ organize_world_outliner() # Auto-organize all actors
1401
+ organize_world_outliner(target_actors='["Cube_1", "Sphere_2"]')
1402
+ """
1403
+ try:
1404
+ rules = json.loads(organization_rules) if organization_rules else None
1405
+ actors = json.loads(target_actors) if target_actors else None
1406
+ except json.JSONDecodeError:
1407
+ return json.dumps({"success": False, "error": "Invalid JSON in parameters"})
1408
+
1409
+ command = {
1410
+ "type": "organize_world_outliner",
1411
+ "organization_rules": rules,
1412
+ "target_actors": actors
1413
+ }
1414
+ response = send_to_unreal(command)
1415
+ return json.dumps(response, indent=2)
1416
+
1417
+
1418
+ @mcp.tool()
1419
+ def tag_assets(asset_paths: str, tags: str, auto_tag: bool = False) -> str:
1420
+ """
1421
+ Add metadata tags to assets for organization and search.
1422
+
1423
+ Args:
1424
+ asset_paths: JSON string - list of asset paths to tag
1425
+ Example: '["/Game/Input/IA_Jump", "/Game/Input/IA_Fire"]'
1426
+ tags: JSON string - list of tag strings to apply
1427
+ Example: '["Movement", "Character"]'
1428
+ auto_tag: If true, automatically generate tags based on asset name/type (default: false)
1429
+
1430
+ Returns:
1431
+ JSON with tagging results
1432
+
1433
+ Example:
1434
+ tag_assets('["/Game/Input/IA_Jump"]', '["Movement", "Character"]')
1435
+ tag_assets('["/Game/Input/IA_FPS_Fire"]', '[]', auto_tag=True) # Auto-generates tags
1436
+ """
1437
+ try:
1438
+ paths = json.loads(asset_paths)
1439
+ tag_list = json.loads(tags)
1440
+ except json.JSONDecodeError:
1441
+ return json.dumps({"success": False, "error": "Invalid JSON in parameters"})
1442
+
1443
+ command = {
1444
+ "type": "tag_assets",
1445
+ "asset_paths": paths,
1446
+ "tags": tag_list,
1447
+ "auto_tag": auto_tag
1448
+ }
1449
+ response = send_to_unreal(command)
1450
+ return json.dumps(response, indent=2)
1451
+
1452
+
1453
+ @mcp.tool()
1454
+ def search_assets_by_tag(folder_path: str, tags: str, match_all: bool = False) -> str:
1455
+ """
1456
+ Search for assets with specific metadata tags.
1457
+
1458
+ Args:
1459
+ folder_path: Folder to search in (e.g., "/Game/Input")
1460
+ tags: JSON string - list of tags to search for
1461
+ Example: '["Combat", "Movement"]'
1462
+ match_all: If true, asset must have ALL tags. If false, ANY tag matches (default: false)
1463
+
1464
+ Returns:
1465
+ JSON with search results
1466
+
1467
+ Example:
1468
+ search_assets_by_tag("/Game/Input", '["Combat"]')
1469
+ search_assets_by_tag("/Game", '["FPS", "Combat"]', match_all=True)
1470
+ """
1471
+ try:
1472
+ tag_list = json.loads(tags)
1473
+ except json.JSONDecodeError:
1474
+ return json.dumps({"success": False, "error": "Invalid JSON in tags parameter"})
1475
+
1476
+ command = {
1477
+ "type": "search_assets_by_tag",
1478
+ "folder_path": folder_path,
1479
+ "tags": tag_list,
1480
+ "match_all": match_all
1481
+ }
1482
+ response = send_to_unreal(command)
1483
+ return json.dumps(response, indent=2)
1484
+
1485
+
1486
+ @mcp.tool()
1487
+ def generate_organization_report(content_browser_paths: str = "[]", include_world_outliner: bool = True) -> str:
1488
+ """
1489
+ Generate a comprehensive organization report.
1490
+
1491
+ Args:
1492
+ content_browser_paths: JSON string - list of paths to analyze (default: ["/Game"])
1493
+ Example: '["/Game/Blueprints", "/Game/Materials"]'
1494
+ include_world_outliner: Include World Outliner statistics (default: true)
1495
+
1496
+ Returns:
1497
+ JSON with comprehensive organization report including asset counts, types, and folder statistics
1498
+
1499
+ Example:
1500
+ generate_organization_report() # Full report
1501
+ generate_organization_report('["/Game/Blueprints"]', include_world_outliner=False)
1502
+ """
1503
+ try:
1504
+ paths = json.loads(content_browser_paths) if content_browser_paths else None
1505
+ except json.JSONDecodeError:
1506
+ return json.dumps({"success": False, "error": "Invalid JSON in content_browser_paths parameter"})
1507
+
1508
+ command = {
1509
+ "type": "generate_organization_report",
1510
+ "content_browser_paths": paths,
1511
+ "include_world_outliner": include_world_outliner
1512
+ }
1513
+ response = send_to_unreal(command)
1514
+ return json.dumps(response, indent=2)
1515
+
1516
+
1517
+ @mcp.tool()
1518
+ def get_component_names(blueprint_path: str = "", actor_name: str = "", is_scene_actor: bool = False) -> str:
1519
+ """
1520
+ Get all component names for a Blueprint or scene actor.
1521
+ Helps discover correct component names before editing.
1522
+
1523
+ Args:
1524
+ blueprint_path: Path to Blueprint (e.g., "/Game/BP_Player") - for Blueprints
1525
+ actor_name: Name of actor in scene (e.g., "Cube_1") - for scene actors
1526
+ is_scene_actor: Set to true if targeting a scene actor (default: false)
1527
+
1528
+ Returns:
1529
+ JSON list of components with their names and classes
1530
+
1531
+ Example:
1532
+ get_component_names(actor_name="Cube_1", is_scene_actor=True)
1533
+ get_component_names(blueprint_path="/Game/BP_Player")
1534
+ """
1535
+ command = {
1536
+ "type": "get_component_names",
1537
+ "blueprint_path": blueprint_path,
1538
+ "actor_name": actor_name,
1539
+ "is_scene_actor": is_scene_actor
1540
+ }
1541
+ response = send_to_unreal(command)
1542
+ if response.get("success"):
1543
+ return json.dumps(response.get("components", []), indent=2)
1544
+ return json.dumps(response)
1545
+
1546
+ @mcp.tool()
1547
+ def get_node_pin_names(blueprint_path: str, function_id: str, node_id: str) -> str:
1548
+ """
1549
+ Get all pin names for a specific Blueprint node.
1550
+ Helps with connecting nodes correctly.
1551
+
1552
+ Args:
1553
+ blueprint_path: Path to the Blueprint
1554
+ function_id: GUID of the function graph
1555
+ node_id: GUID of the node
1556
+
1557
+ Returns:
1558
+ JSON with input and output pin names and types
1559
+
1560
+ Example:
1561
+ After adding a node, use its returned GUID to get pin names:
1562
+ get_node_pin_names("/Game/BP_Test", "FUNC_GUID", "NODE_GUID")
1563
+ """
1564
+ command = {
1565
+ "type": "get_node_pin_names",
1566
+ "blueprint_path": blueprint_path,
1567
+ "function_id": function_id,
1568
+ "node_id": node_id
1569
+ }
1570
+ response = send_to_unreal(command)
1571
+ if response.get("success"):
1572
+ result = {
1573
+ "node_type": response.get("node_type"),
1574
+ "input_pins": response.get("input_pins", []),
1575
+ "output_pins": response.get("output_pins", [])
1576
+ }
1577
+ return json.dumps(result, indent=2)
1578
+ return json.dumps(response)
1579
+
1580
+
1581
+ # ===== PHYSICS & SELECTION COMMANDS =====
1582
+
1583
+ @mcp.tool()
1584
+ def get_selected_actors() -> str:
1585
+ """
1586
+ Get all currently selected actors in the Unreal Editor viewport.
1587
+
1588
+ Returns actor names, classes, locations, and physics status for each selected actor.
1589
+ Useful for checking what's selected before performing operations.
1590
+
1591
+ Returns:
1592
+ JSON with list of selected actors and their details
1593
+
1594
+ Example:
1595
+ # See what actors are currently selected
1596
+ get_selected_actors()
1597
+
1598
+ # Response includes: name, class, location, has_static_mesh, physics_enabled
1599
+ """
1600
+ command = {
1601
+ "type": "get_selected_actors"
1602
+ }
1603
+ response = send_to_unreal(command)
1604
+ return json.dumps(response, indent=2)
1605
+
1606
+
1607
+ @mcp.tool()
1608
+ def enable_physics_on_selected(enable_collision: bool = True, mass_override: float = None) -> str:
1609
+ """
1610
+ Enable physics simulation on all currently selected actors in the viewport.
1611
+
1612
+ This command:
1613
+ 1. Sets mobility to Movable
1614
+ 2. Enables physics simulation
1615
+ 3. Sets collision to QUERY_AND_PHYSICS (if enable_collision=True)
1616
+ 4. Optionally overrides mass
1617
+
1618
+ Perfect for quickly making objects physics-enabled for testing or gameplay.
1619
+
1620
+ Args:
1621
+ enable_collision: Enable collision for physics (default: True)
1622
+ mass_override: Override mass in kg (optional, default uses calculated mass)
1623
+
1624
+ Returns:
1625
+ JSON with results for each processed actor
1626
+
1627
+ Examples:
1628
+ # Enable physics on selected actors with default settings
1629
+ enable_physics_on_selected()
1630
+
1631
+ # Enable physics with custom mass (100 kg)
1632
+ enable_physics_on_selected(mass_override=100.0)
1633
+
1634
+ # Enable physics without collision
1635
+ enable_physics_on_selected(enable_collision=False)
1636
+ """
1637
+ command = {
1638
+ "type": "enable_physics_on_selected",
1639
+ "enable_collision": enable_collision
1640
+ }
1641
+
1642
+ # Only add mass_override if it's not None
1643
+ if mass_override is not None:
1644
+ command["mass_override"] = mass_override
1645
+
1646
+ response = send_to_unreal(command)
1647
+ return json.dumps(response, indent=2)
1648
+
1649
+
1650
+ @mcp.tool()
1651
+ def enable_physics_on_actor(actor_name: str, enable_collision: bool = True, mass_override: float = None) -> str:
1652
+ """
1653
+ Enable physics simulation on a specific actor by name (no selection required).
1654
+
1655
+ Useful for scripted physics setup where you know the actor name.
1656
+ Same physics settings as enable_physics_on_selected but targets a named actor.
1657
+
1658
+ Args:
1659
+ actor_name: Name of the actor (e.g., "SM_Stairs_632", "Cube_1", "PhysicsBox_3")
1660
+ enable_collision: Enable collision for physics (default: True)
1661
+ mass_override: Override mass in kg (optional, default uses calculated mass)
1662
+
1663
+ Returns:
1664
+ JSON with results for the actor
1665
+
1666
+ Examples:
1667
+ # Enable physics on specific actor
1668
+ enable_physics_on_actor("SM_Chair_1")
1669
+
1670
+ # Enable physics with custom mass (50 kg)
1671
+ enable_physics_on_actor("Barrel_2", mass_override=50.0)
1672
+
1673
+ # Enable physics on multiple actors (call multiple times)
1674
+ enable_physics_on_actor("Crate_1")
1675
+ enable_physics_on_actor("Crate_2")
1676
+ enable_physics_on_actor("Barrel_1")
1677
+ """
1678
+ command = {
1679
+ "type": "enable_physics_on_actor",
1680
+ "actor_name": actor_name,
1681
+ "enable_collision": enable_collision
1682
+ }
1683
+
1684
+ # Only add mass_override if it's not None
1685
+ if mass_override is not None:
1686
+ command["mass_override"] = mass_override
1687
+
1688
+ response = send_to_unreal(command)
1689
+ return json.dumps(response, indent=2)
1690
+
1691
+
1692
+ # ===== BLUEPRINT CONNECTION COMMANDS =====
1693
+
1694
+ @mcp.tool()
1695
+ def get_blueprint_node_pins(blueprint_path: str, function_id: str, node_id: str, use_cache: bool = True) -> str:
1696
+ """
1697
+ Get all pins for a Blueprint node - discover what pins are available before connecting.
1698
+
1699
+ This tool reveals all input and output pins for a specific node, including their types,
1700
+ default values, and connection status. Essential for correct node connections.
1701
+
1702
+ Args:
1703
+ blueprint_path: Path to the Blueprint (e.g., "/Game/Blueprints/BP_Test")
1704
+ function_id: GUID of the function graph
1705
+ node_id: GUID of the node
1706
+ use_cache: Use cached results for performance (default: True)
1707
+
1708
+ Returns:
1709
+ JSON with node_info, input_pins, and output_pins
1710
+
1711
+ Example:
1712
+ # After adding a node, discover its pins
1713
+ get_blueprint_node_pins("/Game/BP_Test", "FUNC_GUID", "NODE_GUID")
1714
+
1715
+ # Returns pin names like "ReturnValue", "A", "B" with types
1716
+ """
1717
+ command = {
1718
+ "type": "get_node_pins",
1719
+ "blueprint_path": blueprint_path,
1720
+ "function_id": function_id,
1721
+ "node_id": node_id,
1722
+ "use_cache": use_cache
1723
+ }
1724
+ response = send_to_unreal(command)
1725
+ return json.dumps(response, indent=2)
1726
+
1727
+
1728
+ @mcp.tool()
1729
+ def validate_blueprint_connection(blueprint_path: str, function_id: str,
1730
+ source_node_id: str, source_pin_name: str,
1731
+ target_node_id: str, target_pin_name: str) -> str:
1732
+ """
1733
+ Validate a Blueprint node connection before making it - prevents connection errors.
1734
+
1735
+ This tool checks if a connection is valid (type compatibility, pin existence, etc.)
1736
+ BEFORE you try to connect. It returns helpful error messages and suggestions if invalid.
1737
+
1738
+ Args:
1739
+ blueprint_path: Path to the Blueprint
1740
+ function_id: GUID of the function graph
1741
+ source_node_id: GUID of the source node
1742
+ source_pin_name: Name of the source pin (case-sensitive!)
1743
+ target_node_id: GUID of the target node
1744
+ target_pin_name: Name of the target pin (case-sensitive!)
1745
+
1746
+ Returns:
1747
+ JSON with validation result:
1748
+ - valid: true/false
1749
+ - error_type: TYPE_MISMATCH, PIN_NOT_FOUND, etc.
1750
+ - suggestions: Helpful suggestions if invalid
1751
+ - warnings: Non-blocking warnings
1752
+
1753
+ Example:
1754
+ # Always validate before connecting
1755
+ result = validate_blueprint_connection(
1756
+ "/Game/BP_Test", "FUNC_GUID",
1757
+ "ADD_NODE_GUID", "ReturnValue",
1758
+ "MULT_NODE_GUID", "A"
1759
+ )
1760
+
1761
+ # If invalid, shows why and suggests fixes like "Use a String To Int conversion node"
1762
+ """
1763
+ command = {
1764
+ "type": "validate_connection",
1765
+ "blueprint_path": blueprint_path,
1766
+ "function_id": function_id,
1767
+ "source_node_id": source_node_id,
1768
+ "source_pin_name": source_pin_name,
1769
+ "target_node_id": target_node_id,
1770
+ "target_pin_name": target_pin_name
1771
+ }
1772
+ response = send_to_unreal(command)
1773
+ return json.dumps(response, indent=2)
1774
+
1775
+
1776
+ @mcp.tool()
1777
+ def auto_connect_blueprint_chain(blueprint_path: str, function_id: str,
1778
+ node_chain: list, validate_before_connect: bool = True) -> str:
1779
+ """
1780
+ Intelligently auto-wire a chain of Blueprint nodes - let the system figure out the best connections.
1781
+
1782
+ This tool analyzes a chain of nodes and automatically makes the smartest connections between them,
1783
+ with confidence scoring. Perfect for quickly wiring up node chains.
1784
+
1785
+ Args:
1786
+ blueprint_path: Path to the Blueprint
1787
+ function_id: GUID of the function graph
1788
+ node_chain: List of node GUIDs in order (e.g., ["ENTRY_GUID", "ADD_GUID", "MULT_GUID", "RETURN_GUID"])
1789
+ validate_before_connect: Validate connections before making them (default: True, recommended!)
1790
+
1791
+ Returns:
1792
+ JSON with:
1793
+ - connections_made: Number of successful connections
1794
+ - connections: Array of connections with confidence scores (0.0-1.0)
1795
+ - failed_connections: Connections that couldn't be made
1796
+ - warnings: Any warnings
1797
+
1798
+ Confidence scores:
1799
+ - 1.0: Perfect match (execution pins, exact name match)
1800
+ - 0.7-0.9: Strong match (compatible types, similar names)
1801
+ - 0.4-0.6: Moderate match (compatible types, different names)
1802
+ - <0.3: Not connected automatically
1803
+
1804
+ Example:
1805
+ # Auto-wire a math chain
1806
+ auto_connect_blueprint_chain(
1807
+ "/Game/BP_Calculator", "FUNC_GUID",
1808
+ ["ENTRY_GUID", "ADD_GUID", "MULTIPLY_GUID", "RETURN_GUID"],
1809
+ validate_before_connect=True
1810
+ )
1811
+
1812
+ # Reviews confidence scores to see quality of connections
1813
+ """
1814
+ command = {
1815
+ "type": "auto_connect_chain",
1816
+ "blueprint_path": blueprint_path,
1817
+ "function_id": function_id,
1818
+ "node_chain": node_chain,
1819
+ "validate_before_connect": validate_before_connect
1820
+ }
1821
+ response = send_to_unreal(command)
1822
+ return json.dumps(response, indent=2)
1823
+
1824
+
1825
+ @mcp.tool()
1826
+ def suggest_blueprint_connections(blueprint_path: str, function_id: str,
1827
+ source_node_id: str, target_node_id: str) -> str:
1828
+ """
1829
+ Get ranked connection suggestions between two Blueprint nodes - explore possibilities before connecting.
1830
+
1831
+ This tool analyzes two nodes and suggests the best connections between them, ranked by confidence.
1832
+ Great for understanding connection options without committing to one.
1833
+
1834
+ Args:
1835
+ blueprint_path: Path to the Blueprint
1836
+ function_id: GUID of the function graph
1837
+ source_node_id: GUID of the source node
1838
+ target_node_id: GUID of the target node
1839
+
1840
+ Returns:
1841
+ JSON with:
1842
+ - execution_connection: Recommended execution pin connection (then โ†’ execute)
1843
+ - data_connections: High-confidence data pin connections
1844
+ - all_possibilities: All compatible connections ranked by confidence
1845
+
1846
+ Example:
1847
+ # Explore connection options
1848
+ suggestions = suggest_blueprint_connections(
1849
+ "/Game/BP_Test", "FUNC_GUID",
1850
+ "MULTIPLY_GUID", "CLAMP_GUID"
1851
+ )
1852
+
1853
+ # Shows top suggestions like:
1854
+ # 1. ReturnValue โ†’ A (0.95 confidence)
1855
+ # 2. ReturnValue โ†’ Min (0.60 confidence)
1856
+ """
1857
+ command = {
1858
+ "type": "suggest_connections",
1859
+ "blueprint_path": blueprint_path,
1860
+ "function_id": function_id,
1861
+ "source_node_id": source_node_id,
1862
+ "target_node_id": target_node_id
1863
+ }
1864
+ response = send_to_unreal(command)
1865
+ return json.dumps(response, indent=2)
1866
+
1867
+
1868
+ @mcp.tool()
1869
+ def get_blueprint_graph_connections(blueprint_path: str, function_id: str) -> str:
1870
+ """
1871
+ Get all connections in a Blueprint function graph - analyze existing logic.
1872
+
1873
+ This tool lists every connection in a function graph, showing the complete wiring.
1874
+ Useful for understanding existing Blueprint logic or documenting connections.
1875
+
1876
+ Args:
1877
+ blueprint_path: Path to the Blueprint
1878
+ function_id: GUID of the function graph
1879
+
1880
+ Returns:
1881
+ JSON with:
1882
+ - connections: Array of all connections (source/target node and pin names)
1883
+ - total_connections: Count of connections
1884
+
1885
+ Example:
1886
+ # Analyze existing Blueprint logic
1887
+ connections = get_blueprint_graph_connections("/Game/BP_Complex", "FUNC_GUID")
1888
+
1889
+ # Shows all connections like:
1890
+ # NodeA.then โ†’ NodeB.execute
1891
+ # NodeB.ReturnValue โ†’ NodeC.Input
1892
+ """
1893
+ command = {
1894
+ "type": "get_graph_connections",
1895
+ "blueprint_path": blueprint_path,
1896
+ "function_id": function_id
1897
+ }
1898
+ response = send_to_unreal(command)
1899
+ return json.dumps(response, indent=2)
1900
+
1901
+
1902
+ if __name__ == "__main__":
1903
+ import traceback
1904
+
1905
+ try:
1906
+ print("Server starting...", file=sys.stderr)
1907
+ mcp.run()
1908
+ except Exception as e:
1909
+ print(f"Server crashed with error: {e}", file=sys.stderr)
1910
+ traceback.print_exc(file=sys.stderr)
1911
+ raise
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/__init__.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """An ultra fast cross-platform multiple screenshots module in pure python
2
+ using ctypes.
3
+
4
+ This module is maintained by Mickaรซl Schoentgen <contact@tiger-222.fr>.
5
+
6
+ You can always get the latest version of this module at:
7
+ https://github.com/BoboTiG/python-mss
8
+ If that URL should fail, try contacting the author.
9
+ """
10
+
11
+ from mss.exception import ScreenShotError
12
+ from mss.factory import mss
13
+
14
+ __version__ = "10.1.0.dev0"
15
+ __author__ = "Mickaรซl Schoentgen"
16
+ __date__ = "2013-2025"
17
+ __copyright__ = f"""
18
+ Copyright (c) {__date__}, {__author__}
19
+
20
+ Permission to use, copy, modify, and distribute this software and its
21
+ documentation for any purpose and without fee or royalty is hereby
22
+ granted, provided that the above copyright notice appear in all copies
23
+ and that both that copyright notice and this permission notice appear
24
+ in supporting documentation or portions thereof, including
25
+ modifications, that you make.
26
+ """
27
+ __all__ = ("ScreenShotError", "mss")
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/__main__.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ import os.path
6
+ import sys
7
+ from argparse import ArgumentParser
8
+
9
+ from mss import __version__
10
+ from mss.exception import ScreenShotError
11
+ from mss.factory import mss
12
+ from mss.tools import to_png
13
+
14
+
15
+ def main(*args: str) -> int:
16
+ """Main logic."""
17
+ cli_args = ArgumentParser(prog="mss")
18
+ cli_args.add_argument(
19
+ "-c",
20
+ "--coordinates",
21
+ default="",
22
+ type=str,
23
+ help="the part of the screen to capture: top, left, width, height",
24
+ )
25
+ cli_args.add_argument(
26
+ "-l",
27
+ "--level",
28
+ default=6,
29
+ type=int,
30
+ choices=list(range(10)),
31
+ help="the PNG compression level",
32
+ )
33
+ cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screenshot")
34
+ cli_args.add_argument("-o", "--output", default="monitor-{mon}.png", help="the output file name")
35
+ cli_args.add_argument("--with-cursor", default=False, action="store_true", help="include the cursor")
36
+ cli_args.add_argument(
37
+ "-q",
38
+ "--quiet",
39
+ default=False,
40
+ action="store_true",
41
+ help="do not print created files",
42
+ )
43
+ cli_args.add_argument("-v", "--version", action="version", version=__version__)
44
+
45
+ options = cli_args.parse_args(args or None)
46
+ kwargs = {"mon": options.monitor, "output": options.output}
47
+ if options.coordinates:
48
+ try:
49
+ top, left, width, height = options.coordinates.split(",")
50
+ except ValueError:
51
+ print("Coordinates syntax: top, left, width, height")
52
+ return 2
53
+
54
+ kwargs["mon"] = {
55
+ "top": int(top),
56
+ "left": int(left),
57
+ "width": int(width),
58
+ "height": int(height),
59
+ }
60
+ if options.output == "monitor-{mon}.png":
61
+ kwargs["output"] = "sct-{top}x{left}_{width}x{height}.png"
62
+
63
+ try:
64
+ with mss(with_cursor=options.with_cursor) as sct:
65
+ if options.coordinates:
66
+ output = kwargs["output"].format(**kwargs["mon"])
67
+ sct_img = sct.grab(kwargs["mon"])
68
+ to_png(sct_img.rgb, sct_img.size, level=options.level, output=output)
69
+ if not options.quiet:
70
+ print(os.path.realpath(output))
71
+ else:
72
+ for file_name in sct.save(**kwargs):
73
+ if not options.quiet:
74
+ print(os.path.realpath(file_name))
75
+ return 0
76
+ except ScreenShotError:
77
+ if options.quiet:
78
+ return 1
79
+ raise
80
+
81
+
82
+ if __name__ == "__main__": # pragma: nocover
83
+ sys.exit(main())
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/base.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from abc import ABCMeta, abstractmethod
8
+ from datetime import datetime
9
+ from threading import Lock
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from mss.exception import ScreenShotError
13
+ from mss.screenshot import ScreenShot
14
+ from mss.tools import to_png
15
+
16
+ if TYPE_CHECKING: # pragma: nocover
17
+ from collections.abc import Callable, Iterator
18
+
19
+ from mss.models import Monitor, Monitors
20
+
21
+ try:
22
+ from datetime import UTC
23
+ except ImportError: # pragma: nocover
24
+ # Python < 3.11
25
+ from datetime import timezone
26
+
27
+ UTC = timezone.utc
28
+
29
+ lock = Lock()
30
+
31
+ OPAQUE = 255
32
+
33
+
34
+ class MSSBase(metaclass=ABCMeta):
35
+ """This class will be overloaded by a system specific one."""
36
+
37
+ __slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor"}
38
+
39
+ def __init__(
40
+ self,
41
+ /,
42
+ *,
43
+ compression_level: int = 6,
44
+ with_cursor: bool = False,
45
+ # Linux only
46
+ display: bytes | str | None = None, # noqa: ARG002
47
+ # Mac only
48
+ max_displays: int = 32, # noqa: ARG002
49
+ ) -> None:
50
+ self.cls_image: type[ScreenShot] = ScreenShot
51
+ self.compression_level = compression_level
52
+ self.with_cursor = with_cursor
53
+ self._monitors: Monitors = []
54
+
55
+ def __enter__(self) -> MSSBase: # noqa:PYI034
56
+ """For the cool call `with MSS() as mss:`."""
57
+ return self
58
+
59
+ def __exit__(self, *_: object) -> None:
60
+ """For the cool call `with MSS() as mss:`."""
61
+ self.close()
62
+
63
+ @abstractmethod
64
+ def _cursor_impl(self) -> ScreenShot | None:
65
+ """Retrieve all cursor data. Pixels have to be RGB."""
66
+
67
+ @abstractmethod
68
+ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
69
+ """Retrieve all pixels from a monitor. Pixels have to be RGB.
70
+ That method has to be run using a threading lock.
71
+ """
72
+
73
+ @abstractmethod
74
+ def _monitors_impl(self) -> None:
75
+ """Get positions of monitors (has to be run using a threading lock).
76
+ It must populate self._monitors.
77
+ """
78
+
79
+ def close(self) -> None: # noqa:B027
80
+ """Clean-up."""
81
+
82
+ def grab(self, monitor: Monitor | tuple[int, int, int, int], /) -> ScreenShot:
83
+ """Retrieve screen pixels for a given monitor.
84
+
85
+ Note: *monitor* can be a tuple like the one PIL.Image.grab() accepts.
86
+
87
+ :param monitor: The coordinates and size of the box to capture.
88
+ See :meth:`monitors <monitors>` for object details.
89
+ :return :class:`ScreenShot <ScreenShot>`.
90
+ """
91
+ # Convert PIL bbox style
92
+ if isinstance(monitor, tuple):
93
+ monitor = {
94
+ "left": monitor[0],
95
+ "top": monitor[1],
96
+ "width": monitor[2] - monitor[0],
97
+ "height": monitor[3] - monitor[1],
98
+ }
99
+
100
+ with lock:
101
+ screenshot = self._grab_impl(monitor)
102
+ if self.with_cursor and (cursor := self._cursor_impl()):
103
+ return self._merge(screenshot, cursor)
104
+ return screenshot
105
+
106
+ @property
107
+ def monitors(self) -> Monitors:
108
+ """Get positions of all monitors.
109
+ If the monitor has rotation, you have to deal with it
110
+ inside this method.
111
+
112
+ This method has to fill self._monitors with all information
113
+ and use it as a cache:
114
+ self._monitors[0] is a dict of all monitors together
115
+ self._monitors[N] is a dict of the monitor N (with N > 0)
116
+
117
+ Each monitor is a dict with:
118
+ {
119
+ 'left': the x-coordinate of the upper-left corner,
120
+ 'top': the y-coordinate of the upper-left corner,
121
+ 'width': the width,
122
+ 'height': the height
123
+ }
124
+ """
125
+ if not self._monitors:
126
+ with lock:
127
+ self._monitors_impl()
128
+
129
+ return self._monitors
130
+
131
+ def save(
132
+ self,
133
+ /,
134
+ *,
135
+ mon: int = 0,
136
+ output: str = "monitor-{mon}.png",
137
+ callback: Callable[[str], None] | None = None,
138
+ ) -> Iterator[str]:
139
+ """Grab a screenshot and save it to a file.
140
+
141
+ :param int mon: The monitor to screenshot (default=0).
142
+ -1: grab one screenshot of all monitors
143
+ 0: grab one screenshot by monitor
144
+ N: grab the screenshot of the monitor N
145
+
146
+ :param str output: The output filename.
147
+
148
+ It can take several keywords to customize the filename:
149
+ - `{mon}`: the monitor number
150
+ - `{top}`: the screenshot y-coordinate of the upper-left corner
151
+ - `{left}`: the screenshot x-coordinate of the upper-left corner
152
+ - `{width}`: the screenshot's width
153
+ - `{height}`: the screenshot's height
154
+ - `{date}`: the current date using the default formatter
155
+
156
+ As it is using the `format()` function, you can specify
157
+ formatting options like `{date:%Y-%m-%s}`.
158
+
159
+ :param callable callback: Callback called before saving the
160
+ screenshot to a file. Take the `output` argument as parameter.
161
+
162
+ :return generator: Created file(s).
163
+ """
164
+ monitors = self.monitors
165
+ if not monitors:
166
+ msg = "No monitor found."
167
+ raise ScreenShotError(msg)
168
+
169
+ if mon == 0:
170
+ # One screenshot by monitor
171
+ for idx, monitor in enumerate(monitors[1:], 1):
172
+ fname = output.format(mon=idx, date=datetime.now(UTC) if "{date" in output else None, **monitor)
173
+ if callable(callback):
174
+ callback(fname)
175
+ sct = self.grab(monitor)
176
+ to_png(sct.rgb, sct.size, level=self.compression_level, output=fname)
177
+ yield fname
178
+ else:
179
+ # A screenshot of all monitors together or
180
+ # a screenshot of the monitor N.
181
+ mon = 0 if mon == -1 else mon
182
+ try:
183
+ monitor = monitors[mon]
184
+ except IndexError as exc:
185
+ msg = f"Monitor {mon!r} does not exist."
186
+ raise ScreenShotError(msg) from exc
187
+
188
+ output = output.format(mon=mon, date=datetime.now(UTC) if "{date" in output else None, **monitor)
189
+ if callable(callback):
190
+ callback(output)
191
+ sct = self.grab(monitor)
192
+ to_png(sct.rgb, sct.size, level=self.compression_level, output=output)
193
+ yield output
194
+
195
+ def shot(self, /, **kwargs: Any) -> str:
196
+ """Helper to save the screenshot of the 1st monitor, by default.
197
+ You can pass the same arguments as for ``save``.
198
+ """
199
+ kwargs["mon"] = kwargs.get("mon", 1)
200
+ return next(self.save(**kwargs))
201
+
202
+ @staticmethod
203
+ def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot:
204
+ """Create composite image by blending screenshot and mouse cursor."""
205
+ (cx, cy), (cw, ch) = cursor.pos, cursor.size
206
+ (x, y), (w, h) = screenshot.pos, screenshot.size
207
+
208
+ cx2, cy2 = cx + cw, cy + ch
209
+ x2, y2 = x + w, y + h
210
+
211
+ overlap = cx < x2 and cx2 > x and cy < y2 and cy2 > y
212
+ if not overlap:
213
+ return screenshot
214
+
215
+ screen_raw = screenshot.raw
216
+ cursor_raw = cursor.raw
217
+
218
+ cy, cy2 = (cy - y) * 4, (cy2 - y2) * 4
219
+ cx, cx2 = (cx - x) * 4, (cx2 - x2) * 4
220
+ start_count_y = -cy if cy < 0 else 0
221
+ start_count_x = -cx if cx < 0 else 0
222
+ stop_count_y = ch * 4 - max(cy2, 0)
223
+ stop_count_x = cw * 4 - max(cx2, 0)
224
+ rgb = range(3)
225
+
226
+ for count_y in range(start_count_y, stop_count_y, 4):
227
+ pos_s = (count_y + cy) * w + cx
228
+ pos_c = count_y * cw
229
+
230
+ for count_x in range(start_count_x, stop_count_x, 4):
231
+ spos = pos_s + count_x
232
+ cpos = pos_c + count_x
233
+ alpha = cursor_raw[cpos + 3]
234
+
235
+ if not alpha:
236
+ continue
237
+
238
+ if alpha == OPAQUE:
239
+ screen_raw[spos : spos + 3] = cursor_raw[cpos : cpos + 3]
240
+ else:
241
+ alpha2 = alpha / 255
242
+ for i in rgb:
243
+ screen_raw[spos + i] = int(cursor_raw[cpos + i] * alpha2 + screen_raw[spos + i] * (1 - alpha2))
244
+
245
+ return screenshot
246
+
247
+ @staticmethod
248
+ def _cfactory(
249
+ attr: Any,
250
+ func: str,
251
+ argtypes: list[Any],
252
+ restype: Any,
253
+ /,
254
+ errcheck: Callable | None = None,
255
+ ) -> None:
256
+ """Factory to create a ctypes function and automatically manage errors."""
257
+ meth = getattr(attr, func)
258
+ meth.argtypes = argtypes
259
+ meth.restype = restype
260
+ if errcheck:
261
+ meth.errcheck = errcheck
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/darwin.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import ctypes
8
+ import ctypes.util
9
+ import sys
10
+ from ctypes import POINTER, Structure, c_double, c_float, c_int32, c_ubyte, c_uint32, c_uint64, c_void_p
11
+ from platform import mac_ver
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from mss.base import MSSBase
15
+ from mss.exception import ScreenShotError
16
+ from mss.screenshot import ScreenShot, Size
17
+
18
+ if TYPE_CHECKING: # pragma: nocover
19
+ from mss.models import CFunctions, Monitor
20
+
21
+ __all__ = ("MSS",)
22
+
23
+ MAC_VERSION_CATALINA = 10.16
24
+
25
+ kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816
26
+ kCGWindowImageNominalResolution = 1 << 4 # noqa: N816
27
+ kCGWindowImageShouldBeOpaque = 1 << 1 # noqa: N816
28
+ # Note: set `IMAGE_OPTIONS = 0` to turn on scaling (see issue #257 for more information)
29
+ IMAGE_OPTIONS = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution
30
+
31
+
32
+ def cgfloat() -> type[c_double | c_float]:
33
+ """Get the appropriate value for a float."""
34
+ return c_double if sys.maxsize > 2**32 else c_float
35
+
36
+
37
+ class CGPoint(Structure):
38
+ """Structure that contains coordinates of a rectangle."""
39
+
40
+ _fields_ = (("x", cgfloat()), ("y", cgfloat()))
41
+
42
+ def __repr__(self) -> str:
43
+ return f"{type(self).__name__}(left={self.x} top={self.y})"
44
+
45
+
46
+ class CGSize(Structure):
47
+ """Structure that contains dimensions of an rectangle."""
48
+
49
+ _fields_ = (("width", cgfloat()), ("height", cgfloat()))
50
+
51
+ def __repr__(self) -> str:
52
+ return f"{type(self).__name__}(width={self.width} height={self.height})"
53
+
54
+
55
+ class CGRect(Structure):
56
+ """Structure that contains information about a rectangle."""
57
+
58
+ _fields_ = (("origin", CGPoint), ("size", CGSize))
59
+
60
+ def __repr__(self) -> str:
61
+ return f"{type(self).__name__}<{self.origin} {self.size}>"
62
+
63
+
64
+ # C functions that will be initialised later.
65
+ #
66
+ # Available attr: core.
67
+ #
68
+ # Note: keep it sorted by cfunction.
69
+ CFUNCTIONS: CFunctions = {
70
+ # Syntax: cfunction: (attr, argtypes, restype)
71
+ "CGDataProviderCopyData": ("core", [c_void_p], c_void_p),
72
+ "CGDisplayBounds": ("core", [c_uint32], CGRect),
73
+ "CGDisplayRotation": ("core", [c_uint32], c_float),
74
+ "CFDataGetBytePtr": ("core", [c_void_p], c_void_p),
75
+ "CFDataGetLength": ("core", [c_void_p], c_uint64),
76
+ "CFRelease": ("core", [c_void_p], c_void_p),
77
+ "CGDataProviderRelease": ("core", [c_void_p], c_void_p),
78
+ "CGGetActiveDisplayList": ("core", [c_uint32, POINTER(c_uint32), POINTER(c_uint32)], c_int32),
79
+ "CGImageGetBitsPerPixel": ("core", [c_void_p], int),
80
+ "CGImageGetBytesPerRow": ("core", [c_void_p], int),
81
+ "CGImageGetDataProvider": ("core", [c_void_p], c_void_p),
82
+ "CGImageGetHeight": ("core", [c_void_p], int),
83
+ "CGImageGetWidth": ("core", [c_void_p], int),
84
+ "CGRectStandardize": ("core", [CGRect], CGRect),
85
+ "CGRectUnion": ("core", [CGRect, CGRect], CGRect),
86
+ "CGWindowListCreateImage": ("core", [CGRect, c_uint32, c_uint32, c_uint32], c_void_p),
87
+ }
88
+
89
+
90
+ class MSS(MSSBase):
91
+ """Multiple ScreenShots implementation for macOS.
92
+ It uses intensively the CoreGraphics library.
93
+ """
94
+
95
+ __slots__ = {"core", "max_displays"}
96
+
97
+ def __init__(self, /, **kwargs: Any) -> None:
98
+ """MacOS initialisations."""
99
+ super().__init__(**kwargs)
100
+
101
+ self.max_displays = kwargs.get("max_displays", 32)
102
+
103
+ self._init_library()
104
+ self._set_cfunctions()
105
+
106
+ def _init_library(self) -> None:
107
+ """Load the CoreGraphics library."""
108
+ version = float(".".join(mac_ver()[0].split(".")[:2]))
109
+ if version < MAC_VERSION_CATALINA:
110
+ coregraphics = ctypes.util.find_library("CoreGraphics")
111
+ else:
112
+ # macOS Big Sur and newer
113
+ coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics"
114
+
115
+ if not coregraphics:
116
+ msg = "No CoreGraphics library found."
117
+ raise ScreenShotError(msg)
118
+ self.core = ctypes.cdll.LoadLibrary(coregraphics)
119
+
120
+ def _set_cfunctions(self) -> None:
121
+ """Set all ctypes functions and attach them to attributes."""
122
+ cfactory = self._cfactory
123
+ attrs = {"core": self.core}
124
+ for func, (attr, argtypes, restype) in CFUNCTIONS.items():
125
+ cfactory(attrs[attr], func, argtypes, restype)
126
+
127
+ def _monitors_impl(self) -> None:
128
+ """Get positions of monitors. It will populate self._monitors."""
129
+ int_ = int
130
+ core = self.core
131
+
132
+ # All monitors
133
+ # We need to update the value with every single monitor found
134
+ # using CGRectUnion. Else we will end with infinite values.
135
+ all_monitors = CGRect()
136
+ self._monitors.append({})
137
+
138
+ # Each monitor
139
+ display_count = c_uint32(0)
140
+ active_displays = (c_uint32 * self.max_displays)()
141
+ core.CGGetActiveDisplayList(self.max_displays, active_displays, ctypes.byref(display_count))
142
+ for idx in range(display_count.value):
143
+ display = active_displays[idx]
144
+ rect = core.CGDisplayBounds(display)
145
+ rect = core.CGRectStandardize(rect)
146
+ width, height = rect.size.width, rect.size.height
147
+
148
+ # 0.0: normal
149
+ # 90.0: right
150
+ # -90.0: left
151
+ if core.CGDisplayRotation(display) in {90.0, -90.0}:
152
+ width, height = height, width
153
+
154
+ self._monitors.append(
155
+ {
156
+ "left": int_(rect.origin.x),
157
+ "top": int_(rect.origin.y),
158
+ "width": int_(width),
159
+ "height": int_(height),
160
+ },
161
+ )
162
+
163
+ # Update AiO monitor's values
164
+ all_monitors = core.CGRectUnion(all_monitors, rect)
165
+
166
+ # Set the AiO monitor's values
167
+ self._monitors[0] = {
168
+ "left": int_(all_monitors.origin.x),
169
+ "top": int_(all_monitors.origin.y),
170
+ "width": int_(all_monitors.size.width),
171
+ "height": int_(all_monitors.size.height),
172
+ }
173
+
174
+ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
175
+ """Retrieve all pixels from a monitor. Pixels have to be RGB."""
176
+ core = self.core
177
+ rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"]))
178
+
179
+ image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS)
180
+ if not image_ref:
181
+ msg = "CoreGraphics.CGWindowListCreateImage() failed."
182
+ raise ScreenShotError(msg)
183
+
184
+ width = core.CGImageGetWidth(image_ref)
185
+ height = core.CGImageGetHeight(image_ref)
186
+ prov = copy_data = None
187
+ try:
188
+ prov = core.CGImageGetDataProvider(image_ref)
189
+ copy_data = core.CGDataProviderCopyData(prov)
190
+ data_ref = core.CFDataGetBytePtr(copy_data)
191
+ buf_len = core.CFDataGetLength(copy_data)
192
+ raw = ctypes.cast(data_ref, POINTER(c_ubyte * buf_len))
193
+ data = bytearray(raw.contents)
194
+
195
+ # Remove padding per row
196
+ bytes_per_row = core.CGImageGetBytesPerRow(image_ref)
197
+ bytes_per_pixel = core.CGImageGetBitsPerPixel(image_ref)
198
+ bytes_per_pixel = (bytes_per_pixel + 7) // 8
199
+
200
+ if bytes_per_pixel * width != bytes_per_row:
201
+ cropped = bytearray()
202
+ for row in range(height):
203
+ start = row * bytes_per_row
204
+ end = start + width * bytes_per_pixel
205
+ cropped.extend(data[start:end])
206
+ data = cropped
207
+ finally:
208
+ if prov:
209
+ core.CGDataProviderRelease(prov)
210
+ if copy_data:
211
+ core.CFRelease(copy_data)
212
+
213
+ return self.cls_image(data, monitor, size=Size(width, height))
214
+
215
+ def _cursor_impl(self) -> ScreenShot | None:
216
+ """Retrieve all cursor data. Pixels have to be RGB."""
217
+ return None
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/exception.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+
10
+ class ScreenShotError(Exception):
11
+ """Error handling class."""
12
+
13
+ def __init__(self, message: str, /, *, details: dict[str, Any] | None = None) -> None:
14
+ super().__init__(message)
15
+ self.details = details or {}
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/factory.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ import platform
6
+ from typing import Any
7
+
8
+ from mss.base import MSSBase
9
+ from mss.exception import ScreenShotError
10
+
11
+
12
+ def mss(**kwargs: Any) -> MSSBase:
13
+ """Factory returning a proper MSS class instance.
14
+
15
+ It detects the platform we are running on
16
+ and chooses the most adapted mss_class to take
17
+ screenshots.
18
+
19
+ It then proxies its arguments to the class for
20
+ instantiation.
21
+ """
22
+ os_ = platform.system().lower()
23
+
24
+ if os_ == "darwin":
25
+ from mss import darwin
26
+
27
+ return darwin.MSS(**kwargs)
28
+
29
+ if os_ == "linux":
30
+ from mss import linux
31
+
32
+ return linux.MSS(**kwargs)
33
+
34
+ if os_ == "windows":
35
+ from mss import windows
36
+
37
+ return windows.MSS(**kwargs)
38
+
39
+ msg = f"System {os_!r} not (yet?) implemented."
40
+ raise ScreenShotError(msg)
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/linux.py ADDED
@@ -0,0 +1,481 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ from contextlib import suppress
9
+ from ctypes import (
10
+ CFUNCTYPE,
11
+ POINTER,
12
+ Structure,
13
+ byref,
14
+ c_char_p,
15
+ c_int,
16
+ c_int32,
17
+ c_long,
18
+ c_short,
19
+ c_ubyte,
20
+ c_uint,
21
+ c_uint32,
22
+ c_ulong,
23
+ c_ushort,
24
+ c_void_p,
25
+ cast,
26
+ cdll,
27
+ create_string_buffer,
28
+ )
29
+ from ctypes.util import find_library
30
+ from threading import current_thread, local
31
+ from typing import TYPE_CHECKING, Any
32
+
33
+ from mss.base import MSSBase, lock
34
+ from mss.exception import ScreenShotError
35
+
36
+ if TYPE_CHECKING: # pragma: nocover
37
+ from mss.models import CFunctions, Monitor
38
+ from mss.screenshot import ScreenShot
39
+
40
+ __all__ = ("MSS",)
41
+
42
+
43
+ PLAINMASK = 0x00FFFFFF
44
+ ZPIXMAP = 2
45
+ BITS_PER_PIXELS_32 = 32
46
+ SUPPORTED_BITS_PER_PIXELS = {
47
+ BITS_PER_PIXELS_32,
48
+ }
49
+
50
+
51
+ class Display(Structure):
52
+ """Structure that serves as the connection to the X server
53
+ and that contains all the information about that X server.
54
+ https://github.com/garrybodsworth/pyxlib-ctypes/blob/master/pyxlib/xlib.py#L831.
55
+ """
56
+
57
+
58
+ class XErrorEvent(Structure):
59
+ """XErrorEvent to debug eventual errors.
60
+ https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html.
61
+ """
62
+
63
+ _fields_ = (
64
+ ("type", c_int),
65
+ ("display", POINTER(Display)), # Display the event was read from
66
+ ("serial", c_ulong), # serial number of failed request
67
+ ("error_code", c_ubyte), # error code of failed request
68
+ ("request_code", c_ubyte), # major op-code of failed request
69
+ ("minor_code", c_ubyte), # minor op-code of failed request
70
+ ("resourceid", c_void_p), # resource ID
71
+ )
72
+
73
+
74
+ class XFixesCursorImage(Structure):
75
+ """Cursor structure.
76
+ /usr/include/X11/extensions/Xfixes.h
77
+ https://github.com/freedesktop/xorg-libXfixes/blob/libXfixes-6.0.0/include/X11/extensions/Xfixes.h#L96.
78
+ """
79
+
80
+ _fields_ = (
81
+ ("x", c_short),
82
+ ("y", c_short),
83
+ ("width", c_ushort),
84
+ ("height", c_ushort),
85
+ ("xhot", c_ushort),
86
+ ("yhot", c_ushort),
87
+ ("cursor_serial", c_ulong),
88
+ ("pixels", POINTER(c_ulong)),
89
+ ("atom", c_ulong),
90
+ ("name", c_char_p),
91
+ )
92
+
93
+
94
+ class XImage(Structure):
95
+ """Description of an image as it exists in the client's memory.
96
+ https://tronche.com/gui/x/xlib/graphics/images.html.
97
+ """
98
+
99
+ _fields_ = (
100
+ ("width", c_int), # size of image
101
+ ("height", c_int), # size of image
102
+ ("xoffset", c_int), # number of pixels offset in X direction
103
+ ("format", c_int), # XYBitmap, XYPixmap, ZPixmap
104
+ ("data", c_void_p), # pointer to image data
105
+ ("byte_order", c_int), # data byte order, LSBFirst, MSBFirst
106
+ ("bitmap_unit", c_int), # quant. of scanline 8, 16, 32
107
+ ("bitmap_bit_order", c_int), # LSBFirst, MSBFirst
108
+ ("bitmap_pad", c_int), # 8, 16, 32 either XY or ZPixmap
109
+ ("depth", c_int), # depth of image
110
+ ("bytes_per_line", c_int), # accelerator to next line
111
+ ("bits_per_pixel", c_int), # bits per pixel (ZPixmap)
112
+ ("red_mask", c_ulong), # bits in z arrangement
113
+ ("green_mask", c_ulong), # bits in z arrangement
114
+ ("blue_mask", c_ulong), # bits in z arrangement
115
+ )
116
+
117
+
118
+ class XRRCrtcInfo(Structure):
119
+ """Structure that contains CRTC information.
120
+ https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L360.
121
+ """
122
+
123
+ _fields_ = (
124
+ ("timestamp", c_ulong),
125
+ ("x", c_int),
126
+ ("y", c_int),
127
+ ("width", c_uint),
128
+ ("height", c_uint),
129
+ ("mode", c_long),
130
+ ("rotation", c_int),
131
+ ("noutput", c_int),
132
+ ("outputs", POINTER(c_long)),
133
+ ("rotations", c_ushort),
134
+ ("npossible", c_int),
135
+ ("possible", POINTER(c_long)),
136
+ )
137
+
138
+
139
+ class XRRModeInfo(Structure):
140
+ """https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L248."""
141
+
142
+
143
+ class XRRScreenResources(Structure):
144
+ """Structure that contains arrays of XIDs that point to the
145
+ available outputs and associated CRTCs.
146
+ https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L265.
147
+ """
148
+
149
+ _fields_ = (
150
+ ("timestamp", c_ulong),
151
+ ("configTimestamp", c_ulong),
152
+ ("ncrtc", c_int),
153
+ ("crtcs", POINTER(c_long)),
154
+ ("noutput", c_int),
155
+ ("outputs", POINTER(c_long)),
156
+ ("nmode", c_int),
157
+ ("modes", POINTER(XRRModeInfo)),
158
+ )
159
+
160
+
161
+ class XWindowAttributes(Structure):
162
+ """Attributes for the specified window."""
163
+
164
+ _fields_ = (
165
+ ("x", c_int32), # location of window
166
+ ("y", c_int32), # location of window
167
+ ("width", c_int32), # width of window
168
+ ("height", c_int32), # height of window
169
+ ("border_width", c_int32), # border width of window
170
+ ("depth", c_int32), # depth of window
171
+ ("visual", c_ulong), # the associated visual structure
172
+ ("root", c_ulong), # root of screen containing window
173
+ ("class", c_int32), # InputOutput, InputOnly
174
+ ("bit_gravity", c_int32), # one of bit gravity values
175
+ ("win_gravity", c_int32), # one of the window gravity values
176
+ ("backing_store", c_int32), # NotUseful, WhenMapped, Always
177
+ ("backing_planes", c_ulong), # planes to be preserved if possible
178
+ ("backing_pixel", c_ulong), # value to be used when restoring planes
179
+ ("save_under", c_int32), # boolean, should bits under be saved?
180
+ ("colormap", c_ulong), # color map to be associated with window
181
+ ("mapinstalled", c_uint32), # boolean, is color map currently installed
182
+ ("map_state", c_uint32), # IsUnmapped, IsUnviewable, IsViewable
183
+ ("all_event_masks", c_ulong), # set of events all people have interest in
184
+ ("your_event_mask", c_ulong), # my event mask
185
+ ("do_not_propagate_mask", c_ulong), # set of events that should not propagate
186
+ ("override_redirect", c_int32), # boolean value for override-redirect
187
+ ("screen", c_ulong), # back pointer to correct screen
188
+ )
189
+
190
+
191
+ _ERROR = {}
192
+ _X11 = find_library("X11")
193
+ _XFIXES = find_library("Xfixes")
194
+ _XRANDR = find_library("Xrandr")
195
+
196
+
197
+ @CFUNCTYPE(c_int, POINTER(Display), POINTER(XErrorEvent))
198
+ def _error_handler(display: Display, event: XErrorEvent) -> int:
199
+ """Specifies the program's supplied error handler."""
200
+ # Get the specific error message
201
+ xlib = cdll.LoadLibrary(_X11) # type: ignore[arg-type]
202
+ get_error = xlib.XGetErrorText
203
+ get_error.argtypes = [POINTER(Display), c_int, c_char_p, c_int]
204
+ get_error.restype = c_void_p
205
+
206
+ evt = event.contents
207
+ error = create_string_buffer(1024)
208
+ get_error(display, evt.error_code, error, len(error))
209
+
210
+ _ERROR[current_thread()] = {
211
+ "error": error.value.decode("utf-8"),
212
+ "error_code": evt.error_code,
213
+ "minor_code": evt.minor_code,
214
+ "request_code": evt.request_code,
215
+ "serial": evt.serial,
216
+ "type": evt.type,
217
+ }
218
+
219
+ return 0
220
+
221
+
222
+ def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, Any]:
223
+ """Validate the returned value of a C function call."""
224
+ thread = current_thread()
225
+ if retval != 0 and thread not in _ERROR:
226
+ return args
227
+
228
+ details = _ERROR.pop(thread, {})
229
+ msg = f"{func.__name__}() failed"
230
+ raise ScreenShotError(msg, details=details)
231
+
232
+
233
+ # C functions that will be initialised later.
234
+ # See https://tronche.com/gui/x/xlib/function-index.html for details.
235
+ #
236
+ # Available attr: xfixes, xlib, xrandr.
237
+ #
238
+ # Note: keep it sorted by cfunction.
239
+ CFUNCTIONS: CFunctions = {
240
+ # Syntax: cfunction: (attr, argtypes, restype)
241
+ "XCloseDisplay": ("xlib", [POINTER(Display)], c_void_p),
242
+ "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)),
243
+ "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p),
244
+ "XFixesGetCursorImage": ("xfixes", [POINTER(Display)], POINTER(XFixesCursorImage)),
245
+ "XGetImage": (
246
+ "xlib",
247
+ [POINTER(Display), POINTER(Display), c_int, c_int, c_uint, c_uint, c_ulong, c_int],
248
+ POINTER(XImage),
249
+ ),
250
+ "XGetWindowAttributes": ("xlib", [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], c_int),
251
+ "XOpenDisplay": ("xlib", [c_char_p], POINTER(Display)),
252
+ "XQueryExtension": ("xlib", [POINTER(Display), c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_uint),
253
+ "XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], c_void_p),
254
+ "XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], c_void_p),
255
+ "XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), c_long], POINTER(XRRCrtcInfo)),
256
+ "XRRGetScreenResources": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)),
257
+ "XRRGetScreenResourcesCurrent": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)),
258
+ "XSetErrorHandler": ("xlib", [c_void_p], c_void_p),
259
+ }
260
+
261
+
262
+ class MSS(MSSBase):
263
+ """Multiple ScreenShots implementation for GNU/Linux.
264
+ It uses intensively the Xlib and its Xrandr extension.
265
+ """
266
+
267
+ __slots__ = {"_handles", "xfixes", "xlib", "xrandr"}
268
+
269
+ def __init__(self, /, **kwargs: Any) -> None:
270
+ """GNU/Linux initialisations."""
271
+ super().__init__(**kwargs)
272
+
273
+ # Available thread-specific variables
274
+ self._handles = local()
275
+ self._handles.display = None
276
+ self._handles.drawable = None
277
+ self._handles.original_error_handler = None
278
+ self._handles.root = None
279
+
280
+ display = kwargs.get("display", b"")
281
+ if not display:
282
+ try:
283
+ display = os.environ["DISPLAY"].encode("utf-8")
284
+ except KeyError:
285
+ msg = "$DISPLAY not set."
286
+ raise ScreenShotError(msg) from None
287
+
288
+ if not isinstance(display, bytes):
289
+ display = display.encode("utf-8")
290
+
291
+ if b":" not in display:
292
+ msg = f"Bad display value: {display!r}."
293
+ raise ScreenShotError(msg)
294
+
295
+ if not _X11:
296
+ msg = "No X11 library found."
297
+ raise ScreenShotError(msg)
298
+ self.xlib = cdll.LoadLibrary(_X11)
299
+
300
+ if not _XRANDR:
301
+ msg = "No Xrandr extension found."
302
+ raise ScreenShotError(msg)
303
+ self.xrandr = cdll.LoadLibrary(_XRANDR)
304
+
305
+ if self.with_cursor:
306
+ if _XFIXES:
307
+ self.xfixes = cdll.LoadLibrary(_XFIXES)
308
+ else:
309
+ self.with_cursor = False
310
+
311
+ self._set_cfunctions()
312
+
313
+ # Install the error handler to prevent interpreter crashes: any error will raise a ScreenShotError exception
314
+ self._handles.original_error_handler = self.xlib.XSetErrorHandler(_error_handler)
315
+
316
+ self._handles.display = self.xlib.XOpenDisplay(display)
317
+ if not self._handles.display:
318
+ msg = f"Unable to open display: {display!r}."
319
+ raise ScreenShotError(msg)
320
+
321
+ if not self._is_extension_enabled("RANDR"):
322
+ msg = "Xrandr not enabled."
323
+ raise ScreenShotError(msg)
324
+
325
+ self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display)
326
+
327
+ # Fix for XRRGetScreenResources and XGetImage:
328
+ # expected LP_Display instance instead of LP_XWindowAttributes
329
+ self._handles.drawable = cast(self._handles.root, POINTER(Display))
330
+
331
+ def close(self) -> None:
332
+ # Clean-up
333
+ if self._handles.display:
334
+ with lock:
335
+ self.xlib.XCloseDisplay(self._handles.display)
336
+ self._handles.display = None
337
+ self._handles.drawable = None
338
+ self._handles.root = None
339
+
340
+ # Remove our error handler
341
+ if self._handles.original_error_handler:
342
+ # It's required when exiting MSS to prevent letting `_error_handler()` as default handler.
343
+ # Doing so would crash when using Tk/Tkinter, see issue #220.
344
+ # Interesting technical stuff can be found here:
345
+ # https://core.tcl-lang.org/tk/file?name=generic/tkError.c&ci=a527ef995862cb50
346
+ # https://github.com/tcltk/tk/blob/b9cdafd83fe77499ff47fa373ce037aff3ae286a/generic/tkError.c
347
+ self.xlib.XSetErrorHandler(self._handles.original_error_handler)
348
+ self._handles.original_error_handler = None
349
+
350
+ # Also empty the error dict
351
+ _ERROR.clear()
352
+
353
+ def _is_extension_enabled(self, name: str, /) -> bool:
354
+ """Return True if the given *extension* is enabled on the server."""
355
+ major_opcode_return = c_int()
356
+ first_event_return = c_int()
357
+ first_error_return = c_int()
358
+
359
+ try:
360
+ with lock:
361
+ self.xlib.XQueryExtension(
362
+ self._handles.display,
363
+ name.encode("latin1"),
364
+ byref(major_opcode_return),
365
+ byref(first_event_return),
366
+ byref(first_error_return),
367
+ )
368
+ except ScreenShotError:
369
+ return False
370
+ return True
371
+
372
+ def _set_cfunctions(self) -> None:
373
+ """Set all ctypes functions and attach them to attributes."""
374
+ cfactory = self._cfactory
375
+ attrs = {
376
+ "xfixes": getattr(self, "xfixes", None),
377
+ "xlib": self.xlib,
378
+ "xrandr": self.xrandr,
379
+ }
380
+ for func, (attr, argtypes, restype) in CFUNCTIONS.items():
381
+ with suppress(AttributeError):
382
+ errcheck = None if func == "XSetErrorHandler" else _validate
383
+ cfactory(attrs[attr], func, argtypes, restype, errcheck=errcheck)
384
+
385
+ def _monitors_impl(self) -> None:
386
+ """Get positions of monitors. It will populate self._monitors."""
387
+ display = self._handles.display
388
+ int_ = int
389
+ xrandr = self.xrandr
390
+
391
+ # All monitors
392
+ gwa = XWindowAttributes()
393
+ self.xlib.XGetWindowAttributes(display, self._handles.root, byref(gwa))
394
+ self._monitors.append(
395
+ {"left": int_(gwa.x), "top": int_(gwa.y), "width": int_(gwa.width), "height": int_(gwa.height)},
396
+ )
397
+
398
+ # Each monitor
399
+ # A simple benchmark calling 10 times those 2 functions:
400
+ # XRRGetScreenResources(): 0.1755971429956844 s
401
+ # XRRGetScreenResourcesCurrent(): 0.0039125580078689 s
402
+ # The second is faster by a factor of 44! So try to use it first.
403
+ try:
404
+ mon = xrandr.XRRGetScreenResourcesCurrent(display, self._handles.drawable).contents
405
+ except AttributeError:
406
+ mon = xrandr.XRRGetScreenResources(display, self._handles.drawable).contents
407
+
408
+ crtcs = mon.crtcs
409
+ for idx in range(mon.ncrtc):
410
+ crtc = xrandr.XRRGetCrtcInfo(display, mon, crtcs[idx]).contents
411
+ if crtc.noutput == 0:
412
+ xrandr.XRRFreeCrtcInfo(crtc)
413
+ continue
414
+
415
+ self._monitors.append(
416
+ {
417
+ "left": int_(crtc.x),
418
+ "top": int_(crtc.y),
419
+ "width": int_(crtc.width),
420
+ "height": int_(crtc.height),
421
+ },
422
+ )
423
+ xrandr.XRRFreeCrtcInfo(crtc)
424
+ xrandr.XRRFreeScreenResources(mon)
425
+
426
+ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
427
+ """Retrieve all pixels from a monitor. Pixels have to be RGB."""
428
+ ximage = self.xlib.XGetImage(
429
+ self._handles.display,
430
+ self._handles.drawable,
431
+ monitor["left"],
432
+ monitor["top"],
433
+ monitor["width"],
434
+ monitor["height"],
435
+ PLAINMASK,
436
+ ZPIXMAP,
437
+ )
438
+
439
+ try:
440
+ bits_per_pixel = ximage.contents.bits_per_pixel
441
+ if bits_per_pixel not in SUPPORTED_BITS_PER_PIXELS:
442
+ msg = f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}."
443
+ raise ScreenShotError(msg)
444
+
445
+ raw_data = cast(
446
+ ximage.contents.data,
447
+ POINTER(c_ubyte * monitor["height"] * monitor["width"] * 4),
448
+ )
449
+ data = bytearray(raw_data.contents)
450
+ finally:
451
+ # Free
452
+ self.xlib.XDestroyImage(ximage)
453
+
454
+ return self.cls_image(data, monitor)
455
+
456
+ def _cursor_impl(self) -> ScreenShot:
457
+ """Retrieve all cursor data. Pixels have to be RGB."""
458
+ # Read data of cursor/mouse-pointer
459
+ ximage = self.xfixes.XFixesGetCursorImage(self._handles.display)
460
+ if not (ximage and ximage.contents):
461
+ msg = "Cannot read XFixesGetCursorImage()"
462
+ raise ScreenShotError(msg)
463
+
464
+ cursor_img: XFixesCursorImage = ximage.contents
465
+ region = {
466
+ "left": cursor_img.x - cursor_img.xhot,
467
+ "top": cursor_img.y - cursor_img.yhot,
468
+ "width": cursor_img.width,
469
+ "height": cursor_img.height,
470
+ }
471
+
472
+ raw_data = cast(cursor_img.pixels, POINTER(c_ulong * region["height"] * region["width"]))
473
+ raw = bytearray(raw_data.contents)
474
+
475
+ data = bytearray(region["height"] * region["width"] * 4)
476
+ data[3::4] = raw[3::8]
477
+ data[2::4] = raw[2::8]
478
+ data[1::4] = raw[1::8]
479
+ data[::4] = raw[::8]
480
+
481
+ return self.cls_image(data, region)
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/models.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ from typing import Any, NamedTuple
6
+
7
+ Monitor = dict[str, int]
8
+ Monitors = list[Monitor]
9
+
10
+ Pixel = tuple[int, int, int]
11
+ Pixels = list[tuple[Pixel, ...]]
12
+
13
+ CFunctions = dict[str, tuple[str, list[Any], Any]]
14
+
15
+
16
+ class Pos(NamedTuple):
17
+ left: int
18
+ top: int
19
+
20
+
21
+ class Size(NamedTuple):
22
+ width: int
23
+ height: int
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/py.typed ADDED
File without changes
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/screenshot.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from mss.exception import ScreenShotError
10
+ from mss.models import Monitor, Pixel, Pixels, Pos, Size
11
+
12
+ if TYPE_CHECKING: # pragma: nocover
13
+ from collections.abc import Iterator
14
+
15
+
16
+ class ScreenShot:
17
+ """Screenshot object.
18
+
19
+ .. note::
20
+
21
+ A better name would have been *Image*, but to prevent collisions
22
+ with PIL.Image, it has been decided to use *ScreenShot*.
23
+ """
24
+
25
+ __slots__ = {"__pixels", "__rgb", "pos", "raw", "size"}
26
+
27
+ def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Size | None = None) -> None:
28
+ self.__pixels: Pixels | None = None
29
+ self.__rgb: bytes | None = None
30
+
31
+ #: Bytearray of the raw BGRA pixels retrieved by ctypes
32
+ #: OS independent implementations.
33
+ self.raw = data
34
+
35
+ #: NamedTuple of the screenshot coordinates.
36
+ self.pos = Pos(monitor["left"], monitor["top"])
37
+
38
+ #: NamedTuple of the screenshot size.
39
+ self.size = Size(monitor["width"], monitor["height"]) if size is None else size
40
+
41
+ def __repr__(self) -> str:
42
+ return f"<{type(self).__name__} pos={self.left},{self.top} size={self.width}x{self.height}>"
43
+
44
+ @property
45
+ def __array_interface__(self) -> dict[str, Any]:
46
+ """Numpy array interface support.
47
+ It uses raw data in BGRA form.
48
+
49
+ See https://docs.scipy.org/doc/numpy/reference/arrays.interface.html
50
+ """
51
+ return {
52
+ "version": 3,
53
+ "shape": (self.height, self.width, 4),
54
+ "typestr": "|u1",
55
+ "data": self.raw,
56
+ }
57
+
58
+ @classmethod
59
+ def from_size(cls: type[ScreenShot], data: bytearray, width: int, height: int, /) -> ScreenShot:
60
+ """Instantiate a new class given only screenshot's data and size."""
61
+ monitor = {"left": 0, "top": 0, "width": width, "height": height}
62
+ return cls(data, monitor)
63
+
64
+ @property
65
+ def bgra(self) -> bytes:
66
+ """BGRA values from the BGRA raw pixels."""
67
+ return bytes(self.raw)
68
+
69
+ @property
70
+ def height(self) -> int:
71
+ """Convenient accessor to the height size."""
72
+ return self.size.height
73
+
74
+ @property
75
+ def left(self) -> int:
76
+ """Convenient accessor to the left position."""
77
+ return self.pos.left
78
+
79
+ @property
80
+ def pixels(self) -> Pixels:
81
+ """:return list: RGB tuples."""
82
+ if not self.__pixels:
83
+ rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4])
84
+ self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width))
85
+
86
+ return self.__pixels
87
+
88
+ @property
89
+ def rgb(self) -> bytes:
90
+ """Compute RGB values from the BGRA raw pixels.
91
+
92
+ :return bytes: RGB pixels.
93
+ """
94
+ if not self.__rgb:
95
+ rgb = bytearray(self.height * self.width * 3)
96
+ raw = self.raw
97
+ rgb[::3] = raw[2::4]
98
+ rgb[1::3] = raw[1::4]
99
+ rgb[2::3] = raw[::4]
100
+ self.__rgb = bytes(rgb)
101
+
102
+ return self.__rgb
103
+
104
+ @property
105
+ def top(self) -> int:
106
+ """Convenient accessor to the top position."""
107
+ return self.pos.top
108
+
109
+ @property
110
+ def width(self) -> int:
111
+ """Convenient accessor to the width size."""
112
+ return self.size.width
113
+
114
+ def pixel(self, coord_x: int, coord_y: int) -> Pixel:
115
+ """Returns the pixel value at a given position.
116
+
117
+ :param int coord_x: The x coordinate.
118
+ :param int coord_y: The y coordinate.
119
+ :return tuple: The pixel value as (R, G, B).
120
+ """
121
+ try:
122
+ return self.pixels[coord_y][coord_x]
123
+ except IndexError as exc:
124
+ msg = f"Pixel location ({coord_x}, {coord_y}) is out of range."
125
+ raise ScreenShotError(msg) from exc
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/tools.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import struct
9
+ import zlib
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: Path | str | None = None) -> bytes | None:
17
+ """Dump data to a PNG file. If `output` is `None`, create no file but return
18
+ the whole PNG data.
19
+
20
+ :param bytes data: RGBRGB...RGB data.
21
+ :param tuple size: The (width, height) pair.
22
+ :param int level: PNG compression level.
23
+ :param str output: Output file name.
24
+ """
25
+ pack = struct.pack
26
+ crc32 = zlib.crc32
27
+
28
+ width, height = size
29
+ line = width * 3
30
+ png_filter = pack(">B", 0)
31
+ scanlines = b"".join([png_filter + data[y * line : y * line + line] for y in range(height)])
32
+
33
+ magic = pack(">8B", 137, 80, 78, 71, 13, 10, 26, 10)
34
+
35
+ # Header: size, marker, data, CRC32
36
+ ihdr = [b"", b"IHDR", b"", b""]
37
+ ihdr[2] = pack(">2I5B", width, height, 8, 2, 0, 0, 0)
38
+ ihdr[3] = pack(">I", crc32(b"".join(ihdr[1:3])) & 0xFFFFFFFF)
39
+ ihdr[0] = pack(">I", len(ihdr[2]))
40
+
41
+ # Data: size, marker, data, CRC32
42
+ idat = [b"", b"IDAT", zlib.compress(scanlines, level), b""]
43
+ idat[3] = pack(">I", crc32(b"".join(idat[1:3])) & 0xFFFFFFFF)
44
+ idat[0] = pack(">I", len(idat[2]))
45
+
46
+ # Footer: size, marker, None, CRC32
47
+ iend = [b"", b"IEND", b"", b""]
48
+ iend[3] = pack(">I", crc32(iend[1]) & 0xFFFFFFFF)
49
+ iend[0] = pack(">I", len(iend[2]))
50
+
51
+ if not output:
52
+ # Returns raw bytes of the whole PNG data
53
+ return magic + b"".join(ihdr + idat + iend)
54
+
55
+ with open(output, "wb") as fileh: # noqa: PTH123
56
+ fileh.write(magic)
57
+ fileh.write(b"".join(ihdr))
58
+ fileh.write(b"".join(idat))
59
+ fileh.write(b"".join(iend))
60
+
61
+ # Force write of file to disk
62
+ fileh.flush()
63
+ os.fsync(fileh.fileno())
64
+
65
+ return None
UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/windows.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This is part of the MSS Python's module.
2
+ Source: https://github.com/BoboTiG/python-mss.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import ctypes
8
+ import sys
9
+ from ctypes import POINTER, WINFUNCTYPE, Structure, c_int, c_void_p
10
+ from ctypes.wintypes import (
11
+ BOOL,
12
+ DOUBLE,
13
+ DWORD,
14
+ HBITMAP,
15
+ HDC,
16
+ HGDIOBJ,
17
+ HWND,
18
+ INT,
19
+ LONG,
20
+ LPARAM,
21
+ LPRECT,
22
+ RECT,
23
+ UINT,
24
+ WORD,
25
+ )
26
+ from threading import local
27
+ from typing import TYPE_CHECKING, Any
28
+
29
+ from mss.base import MSSBase
30
+ from mss.exception import ScreenShotError
31
+
32
+ if TYPE_CHECKING: # pragma: nocover
33
+ from mss.models import CFunctions, Monitor
34
+ from mss.screenshot import ScreenShot
35
+
36
+ __all__ = ("MSS",)
37
+
38
+
39
+ CAPTUREBLT = 0x40000000
40
+ DIB_RGB_COLORS = 0
41
+ SRCCOPY = 0x00CC0020
42
+
43
+
44
+ class BITMAPINFOHEADER(Structure):
45
+ """Information about the dimensions and color format of a DIB."""
46
+
47
+ _fields_ = (
48
+ ("biSize", DWORD),
49
+ ("biWidth", LONG),
50
+ ("biHeight", LONG),
51
+ ("biPlanes", WORD),
52
+ ("biBitCount", WORD),
53
+ ("biCompression", DWORD),
54
+ ("biSizeImage", DWORD),
55
+ ("biXPelsPerMeter", LONG),
56
+ ("biYPelsPerMeter", LONG),
57
+ ("biClrUsed", DWORD),
58
+ ("biClrImportant", DWORD),
59
+ )
60
+
61
+
62
+ class BITMAPINFO(Structure):
63
+ """Structure that defines the dimensions and color information for a DIB."""
64
+
65
+ _fields_ = (("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3))
66
+
67
+
68
+ MONITORNUMPROC = WINFUNCTYPE(INT, DWORD, DWORD, POINTER(RECT), DOUBLE)
69
+
70
+
71
+ # C functions that will be initialised later.
72
+ #
73
+ # Available attr: gdi32, user32.
74
+ #
75
+ # Note: keep it sorted by cfunction.
76
+ CFUNCTIONS: CFunctions = {
77
+ # Syntax: cfunction: (attr, argtypes, restype)
78
+ "BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL),
79
+ "CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP),
80
+ "CreateCompatibleDC": ("gdi32", [HDC], HDC),
81
+ "DeleteDC": ("gdi32", [HDC], HDC),
82
+ "DeleteObject": ("gdi32", [HGDIOBJ], INT),
83
+ "EnumDisplayMonitors": ("user32", [HDC, c_void_p, MONITORNUMPROC, LPARAM], BOOL),
84
+ "GetDeviceCaps": ("gdi32", [HWND, INT], INT),
85
+ "GetDIBits": ("gdi32", [HDC, HBITMAP, UINT, UINT, c_void_p, POINTER(BITMAPINFO), UINT], BOOL),
86
+ "GetSystemMetrics": ("user32", [INT], INT),
87
+ "GetWindowDC": ("user32", [HWND], HDC),
88
+ "ReleaseDC": ("user32", [HWND, HDC], c_int),
89
+ "SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ),
90
+ }
91
+
92
+
93
+ class MSS(MSSBase):
94
+ """Multiple ScreenShots implementation for Microsoft Windows."""
95
+
96
+ __slots__ = {"_handles", "gdi32", "user32"}
97
+
98
+ def __init__(self, /, **kwargs: Any) -> None:
99
+ """Windows initialisations."""
100
+ super().__init__(**kwargs)
101
+
102
+ self.user32 = ctypes.WinDLL("user32")
103
+ self.gdi32 = ctypes.WinDLL("gdi32")
104
+ self._set_cfunctions()
105
+ self._set_dpi_awareness()
106
+
107
+ # Available thread-specific variables
108
+ self._handles = local()
109
+ self._handles.region_width_height = (0, 0)
110
+ self._handles.bmp = None
111
+ self._handles.srcdc = self.user32.GetWindowDC(0)
112
+ self._handles.memdc = self.gdi32.CreateCompatibleDC(self._handles.srcdc)
113
+
114
+ bmi = BITMAPINFO()
115
+ bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
116
+ bmi.bmiHeader.biPlanes = 1 # Always 1
117
+ bmi.bmiHeader.biBitCount = 32 # See grab.__doc__ [2]
118
+ bmi.bmiHeader.biCompression = 0 # 0 = BI_RGB (no compression)
119
+ bmi.bmiHeader.biClrUsed = 0 # See grab.__doc__ [3]
120
+ bmi.bmiHeader.biClrImportant = 0 # See grab.__doc__ [3]
121
+ self._handles.bmi = bmi
122
+
123
+ def close(self) -> None:
124
+ # Clean-up
125
+ if self._handles.bmp:
126
+ self.gdi32.DeleteObject(self._handles.bmp)
127
+ self._handles.bmp = None
128
+
129
+ if self._handles.memdc:
130
+ self.gdi32.DeleteDC(self._handles.memdc)
131
+ self._handles.memdc = None
132
+
133
+ if self._handles.srcdc:
134
+ self.user32.ReleaseDC(0, self._handles.srcdc)
135
+ self._handles.srcdc = None
136
+
137
+ def _set_cfunctions(self) -> None:
138
+ """Set all ctypes functions and attach them to attributes."""
139
+ cfactory = self._cfactory
140
+ attrs = {
141
+ "gdi32": self.gdi32,
142
+ "user32": self.user32,
143
+ }
144
+ for func, (attr, argtypes, restype) in CFUNCTIONS.items():
145
+ cfactory(attrs[attr], func, argtypes, restype)
146
+
147
+ def _set_dpi_awareness(self) -> None:
148
+ """Set DPI awareness to capture full screen on Hi-DPI monitors."""
149
+ version = sys.getwindowsversion()[:2]
150
+ if version >= (6, 3):
151
+ # Windows 8.1+
152
+ # Here 2 = PROCESS_PER_MONITOR_DPI_AWARE, which means:
153
+ # per monitor DPI aware. This app checks for the DPI when it is
154
+ # created and adjusts the scale factor whenever the DPI changes.
155
+ # These applications are not automatically scaled by the system.
156
+ ctypes.windll.shcore.SetProcessDpiAwareness(2)
157
+ elif (6, 0) <= version < (6, 3):
158
+ # Windows Vista, 7, 8, and Server 2012
159
+ self.user32.SetProcessDPIAware()
160
+
161
+ def _monitors_impl(self) -> None:
162
+ """Get positions of monitors. It will populate self._monitors."""
163
+ int_ = int
164
+ user32 = self.user32
165
+ get_system_metrics = user32.GetSystemMetrics
166
+
167
+ # All monitors
168
+ self._monitors.append(
169
+ {
170
+ "left": int_(get_system_metrics(76)), # SM_XVIRTUALSCREEN
171
+ "top": int_(get_system_metrics(77)), # SM_YVIRTUALSCREEN
172
+ "width": int_(get_system_metrics(78)), # SM_CXVIRTUALSCREEN
173
+ "height": int_(get_system_metrics(79)), # SM_CYVIRTUALSCREEN
174
+ },
175
+ )
176
+
177
+ # Each monitor
178
+ def _callback(_monitor: int, _data: HDC, rect: LPRECT, _dc: LPARAM) -> int:
179
+ """Callback for monitorenumproc() function, it will return
180
+ a RECT with appropriate values.
181
+ """
182
+ rct = rect.contents
183
+ self._monitors.append(
184
+ {
185
+ "left": int_(rct.left),
186
+ "top": int_(rct.top),
187
+ "width": int_(rct.right) - int_(rct.left),
188
+ "height": int_(rct.bottom) - int_(rct.top),
189
+ },
190
+ )
191
+ return 1
192
+
193
+ callback = MONITORNUMPROC(_callback)
194
+ user32.EnumDisplayMonitors(0, 0, callback, 0)
195
+
196
+ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
197
+ """Retrieve all pixels from a monitor. Pixels have to be RGB.
198
+
199
+ In the code, there are a few interesting things:
200
+
201
+ [1] bmi.bmiHeader.biHeight = -height
202
+
203
+ A bottom-up DIB is specified by setting the height to a
204
+ positive number, while a top-down DIB is specified by
205
+ setting the height to a negative number.
206
+ https://msdn.microsoft.com/en-us/library/ms787796.aspx
207
+ https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx
208
+
209
+
210
+ [2] bmi.bmiHeader.biBitCount = 32
211
+ image_data = create_string_buffer(height * width * 4)
212
+
213
+ We grab the image in RGBX mode, so that each word is 32bit
214
+ and we have no striding.
215
+ Inspired by https://github.com/zoofIO/flexx
216
+
217
+
218
+ [3] bmi.bmiHeader.biClrUsed = 0
219
+ bmi.bmiHeader.biClrImportant = 0
220
+
221
+ When biClrUsed and biClrImportant are set to zero, there
222
+ is "no" color table, so we can read the pixels of the bitmap
223
+ retrieved by gdi32.GetDIBits() as a sequence of RGB values.
224
+ Thanks to http://stackoverflow.com/a/3688682
225
+ """
226
+ srcdc, memdc = self._handles.srcdc, self._handles.memdc
227
+ gdi = self.gdi32
228
+ width, height = monitor["width"], monitor["height"]
229
+
230
+ if self._handles.region_width_height != (width, height):
231
+ self._handles.region_width_height = (width, height)
232
+ self._handles.bmi.bmiHeader.biWidth = width
233
+ self._handles.bmi.bmiHeader.biHeight = -height # Why minus? [1]
234
+ self._handles.data = ctypes.create_string_buffer(width * height * 4) # [2]
235
+ if self._handles.bmp:
236
+ gdi.DeleteObject(self._handles.bmp)
237
+ self._handles.bmp = gdi.CreateCompatibleBitmap(srcdc, width, height)
238
+ gdi.SelectObject(memdc, self._handles.bmp)
239
+
240
+ gdi.BitBlt(memdc, 0, 0, width, height, srcdc, monitor["left"], monitor["top"], SRCCOPY | CAPTUREBLT)
241
+ bits = gdi.GetDIBits(memdc, self._handles.bmp, 0, height, self._handles.data, self._handles.bmi, DIB_RGB_COLORS)
242
+ if bits != height:
243
+ msg = "gdi32.GetDIBits() failed."
244
+ raise ScreenShotError(msg)
245
+
246
+ return self.cls_image(bytearray(self._handles.data), monitor)
247
+
248
+ def _cursor_impl(self) -> ScreenShot | None:
249
+ """Retrieve all cursor data. Pixels have to be RGB."""
250
+ return None
UMCP.it-Unreal-Organizer-Assistant/Content/Python/socket_server_config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "auto_start_socket_server": false
3
+ }
UMCP.it-Unreal-Organizer-Assistant/Content/Python/unreal_socket_server.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import socket
2
+ import json
3
+ import unreal
4
+ import threading
5
+ import time
6
+ import importlib
7
+ from typing import Dict, Any, Tuple, List, Optional
8
+
9
+ # Import handlers
10
+ from handlers import basic_commands, actor_commands, blueprint_commands, python_commands
11
+ from handlers import ui_commands, organization_commands, physics_commands, blueprint_connection_commands
12
+ from utils import logging as log
13
+
14
+ # Force reload of all handler modules to pick up code changes
15
+ importlib.reload(basic_commands)
16
+ importlib.reload(actor_commands)
17
+ importlib.reload(blueprint_commands)
18
+ importlib.reload(python_commands)
19
+ importlib.reload(ui_commands)
20
+ importlib.reload(organization_commands)
21
+ importlib.reload(physics_commands)
22
+ importlib.reload(blueprint_connection_commands)
23
+
24
+ # Global queues and state
25
+ command_queue = []
26
+ response_dict = {}
27
+
28
+
29
+ class CommandDispatcher:
30
+ """
31
+ Dispatches commands to appropriate handlers based on command type
32
+ """
33
+ def __init__(self):
34
+ # Register command handlers
35
+ self.handlers = {
36
+ "handshake": self._handle_handshake,
37
+
38
+ # Basic object commands
39
+ "spawn": basic_commands.handle_spawn,
40
+ "create_material": basic_commands.handle_create_material,
41
+ "modify_object": actor_commands.handle_modify_object,
42
+ "take_screenshot": basic_commands.handle_take_screenshot,
43
+
44
+ # Blueprint commands
45
+ "create_blueprint": blueprint_commands.handle_create_blueprint,
46
+ "add_component": blueprint_commands.handle_add_component,
47
+ "add_variable": blueprint_commands.handle_add_variable,
48
+ "add_function": blueprint_commands.handle_add_function,
49
+ "add_node": blueprint_commands.handle_add_node,
50
+ "connect_nodes": blueprint_commands.handle_connect_nodes,
51
+ "compile_blueprint": blueprint_commands.handle_compile_blueprint,
52
+ "spawn_blueprint": blueprint_commands.handle_spawn_blueprint,
53
+ "delete_node": blueprint_commands.handle_delete_node,
54
+
55
+ # Getters
56
+ "get_node_guid": blueprint_commands.handle_get_node_guid,
57
+ "get_all_nodes": blueprint_commands.handle_get_all_nodes,
58
+ "get_node_suggestions": blueprint_commands.handle_get_node_suggestions,
59
+
60
+
61
+ # Bulk commands
62
+ "add_nodes_bulk": blueprint_commands.handle_add_nodes_bulk,
63
+ "connect_nodes_bulk": blueprint_commands.handle_connect_nodes_bulk,
64
+
65
+ # Python and console
66
+ "execute_python": python_commands.handle_execute_python,
67
+ "execute_unreal_command": python_commands.handle_execute_unreal_command,
68
+
69
+ # Actor and Blueprint creation
70
+ "edit_component_property": actor_commands.handle_edit_component_property,
71
+ "add_component_with_events": actor_commands.handle_add_component_with_events,
72
+ "create_game_mode": actor_commands.handle_create_game_mode,
73
+
74
+ # Scene
75
+ "get_all_scene_objects": basic_commands.handle_get_all_scene_objects,
76
+ "create_project_folder": basic_commands.handle_create_project_folder,
77
+ "get_files_in_folder": basic_commands.handle_get_files_in_folder,
78
+
79
+ # Input
80
+ "add_input_binding": basic_commands.handle_add_input_binding,
81
+ "create_enhanced_input_action": basic_commands.handle_create_enhanced_input_action,
82
+ "create_enhanced_input_mapping_context": basic_commands.handle_create_enhanced_input_mapping_context,
83
+
84
+ # Component helpers
85
+ "get_component_names": actor_commands.handle_get_component_names,
86
+
87
+ # Blueprint helpers
88
+ "get_node_pin_names": blueprint_commands.handle_get_node_pin_names,
89
+
90
+ # --- NEW UI COMMANDS ---
91
+ "add_widget_to_user_widget": ui_commands.handle_add_widget_to_user_widget,
92
+ "edit_widget_property": ui_commands.handle_edit_widget_property,
93
+
94
+ # --- ORGANIZATION COMMANDS ---
95
+ "create_folder_structure": organization_commands.handle_create_folder_structure,
96
+ "organize_assets_by_type": organization_commands.handle_organize_assets_by_type,
97
+ "organize_world_outliner": organization_commands.handle_organize_world_outliner,
98
+ "tag_assets": organization_commands.handle_tag_assets,
99
+ "search_assets_by_tag": organization_commands.handle_search_assets_by_tag,
100
+ "generate_organization_report": organization_commands.handle_generate_organization_report,
101
+
102
+ # --- PHYSICS & SELECTION COMMANDS ---
103
+ "get_selected_actors": physics_commands.handle_get_selected_actors,
104
+ "enable_physics_on_selected": physics_commands.handle_enable_physics_on_selected,
105
+ "enable_physics_on_actor": physics_commands.handle_enable_physics_on_actor,
106
+
107
+ # --- BLUEPRINT CONNECTION COMMANDS ---
108
+ "get_node_pins": blueprint_connection_commands.handle_get_node_pins,
109
+ "validate_connection": blueprint_connection_commands.handle_validate_connection,
110
+ "auto_connect_chain": blueprint_connection_commands.handle_auto_connect_chain,
111
+ "suggest_connections": blueprint_connection_commands.handle_suggest_connections,
112
+ "get_graph_connections": blueprint_connection_commands.handle_get_graph_connections,
113
+ }
114
+
115
+ def dispatch(self, command: Dict[str, Any]) -> Dict[str, Any]:
116
+ """Dispatch command to appropriate handler"""
117
+ command_type = command.get("type")
118
+ if command_type not in self.handlers:
119
+ return {"success": False, "error": f"Unknown command type: {command_type}"}
120
+
121
+ try:
122
+ handler = self.handlers[command_type]
123
+ return handler(command)
124
+ except Exception as e:
125
+ log.log_error(f"Error processing command: {str(e)}")
126
+ return {"success": False, "error": str(e)}
127
+
128
+ def _handle_handshake(self, command: Dict[str, Any]) -> Dict[str, Any]:
129
+ """Built-in handler for handshake command"""
130
+ message = command.get("message", "")
131
+ log.log_info(f"Handshake received: {message}")
132
+
133
+ # Get Unreal Engine version
134
+ engine_version = unreal.SystemLibrary.get_engine_version()
135
+
136
+ # Add connection and session information
137
+ connection_info = {
138
+ "status": "Connected",
139
+ "engine_version": engine_version,
140
+ "timestamp": time.time(),
141
+ "session_id": f"UE-{int(time.time())}"
142
+ }
143
+
144
+ return {
145
+ "success": True,
146
+ "message": f"Received: {message}",
147
+ "connection_info": connection_info
148
+ }
149
+
150
+
151
+ # Create global dispatcher instance
152
+ dispatcher = CommandDispatcher()
153
+
154
+
155
+ def process_commands(delta_time=None):
156
+ """Process commands on the main thread"""
157
+ if not command_queue:
158
+ return
159
+
160
+ command_id, command = command_queue.pop(0)
161
+ log.log_info(f"Processing command on main thread: {command}")
162
+
163
+ try:
164
+ response = dispatcher.dispatch(command)
165
+ response_dict[command_id] = response
166
+ except Exception as e:
167
+ log.log_error(f"Error processing command: {str(e)}", include_traceback=True)
168
+ response_dict[command_id] = {"success": False, "error": str(e)}
169
+
170
+
171
+ def receive_all_data(conn, buffer_size=4096):
172
+ """
173
+ Receive all data from socket until complete JSON is received
174
+
175
+ Args:
176
+ conn: Socket connection
177
+ buffer_size: Initial buffer size for receiving data
178
+
179
+ Returns:
180
+ Decoded complete data
181
+ """
182
+ data = b""
183
+ while True:
184
+ try:
185
+ # Receive chunk of data
186
+ chunk = conn.recv(buffer_size)
187
+ if not chunk:
188
+ break
189
+
190
+ data += chunk
191
+
192
+ # Try to parse as JSON to check if we received complete data
193
+ try:
194
+ json.loads(data.decode('utf-8'))
195
+ # If we get here, JSON is valid and complete
196
+ return data.decode('utf-8')
197
+ except json.JSONDecodeError as json_err:
198
+ # Check if the error indicates an unterminated string or incomplete JSON
199
+ if "Unterminated string" in str(json_err) or "Expecting" in str(json_err):
200
+ # Need more data, continue receiving
201
+ continue
202
+ else:
203
+ # JSON is malformed in some other way, not just incomplete
204
+ log.log_error(f"Malformed JSON received: {str(json_err)}", include_traceback=True)
205
+ return None
206
+
207
+ except socket.timeout:
208
+ # Socket timeout, return what we have so far
209
+ log.log_warning("Socket timeout while receiving data")
210
+ return data.decode('utf-8')
211
+ except Exception as e:
212
+ log.log_error(f"Error receiving data: {str(e)}", include_traceback=True)
213
+ return None
214
+
215
+ return data.decode('utf-8')
216
+
217
+
218
+ def socket_server_thread():
219
+ """Socket server running in a separate thread"""
220
+ server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
221
+ server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
222
+ server_socket.bind(('localhost', 9877))
223
+ server_socket.listen(1)
224
+ log.log_info("Unreal Engine socket server started on port 9877")
225
+
226
+ command_counter = 0
227
+
228
+ while True:
229
+ try:
230
+ conn, addr = server_socket.accept()
231
+ # Set a timeout to prevent hanging
232
+ conn.settimeout(5) # 5-second timeout
233
+
234
+ # Receive complete data, handling potential incomplete JSON
235
+ data_str = receive_all_data(conn)
236
+
237
+ if data_str:
238
+ try:
239
+ command = json.loads(data_str)
240
+ log.log_info(f"Received command: {command}")
241
+
242
+ # All commands must be processed on main thread, including handshake
243
+ command_id = command_counter
244
+ command_counter += 1
245
+ command_queue.append((command_id, command))
246
+
247
+ # Wait for the response with a timeout
248
+ timeout = 10 # seconds
249
+ start_time = time.time()
250
+ while command_id not in response_dict and time.time() - start_time < timeout:
251
+ time.sleep(0.1)
252
+
253
+ if command_id in response_dict:
254
+ response = response_dict.pop(command_id)
255
+ conn.sendall(json.dumps(response).encode())
256
+ else:
257
+ error_response = {"success": False, "error": "Command timed out"}
258
+ conn.sendall(json.dumps(error_response).encode())
259
+ except json.JSONDecodeError as json_err:
260
+ log.log_error(f"Error parsing JSON: {str(json_err)}", include_traceback=True)
261
+ error_response = {"success": False, "error": f"Invalid JSON: {str(json_err)}"}
262
+ conn.sendall(json.dumps(error_response).encode())
263
+ else:
264
+ # No data or error receiving data
265
+ error_response = {"success": False, "error": "No data received or error parsing data"}
266
+ conn.sendall(json.dumps(error_response).encode())
267
+
268
+ conn.close()
269
+ except Exception as e:
270
+ log.log_error(f"Error in socket server: {str(e)}", include_traceback=True)
271
+ try:
272
+ # Try to close the connection if it's still open
273
+ conn.close()
274
+ except:
275
+ pass
276
+
277
+
278
+ # Register tick function to process commands on main thread
279
+ def register_command_processor():
280
+ """Register the command processor with Unreal's tick system"""
281
+ unreal.register_slate_post_tick_callback(process_commands)
282
+ log.log_info("Command processor registered")
283
+
284
+
285
+ # Initialize the server
286
+ def initialize_server():
287
+ """Initialize and start the socket server"""
288
+ # Start the server thread
289
+ thread = threading.Thread(target=socket_server_thread)
290
+ thread.daemon = True
291
+ thread.start()
292
+ log.log_info("Socket server thread started")
293
+
294
+ # Register the command processor on the main thread
295
+ register_command_processor()
296
+
297
+ log.log_info("Unreal Engine AI command server initialized successfully")
298
+ log.log_info("Available commands:")
299
+ log.log_info(" - Basic: handshake, spawn, create_material, modify_object")
300
+ log.log_info(" - Blueprint: create_blueprint, add_component, add_variable, add_function, add_node, connect_nodes, compile_blueprint, spawn_blueprint, add_nodes_bulk, connect_nodes_bulk")
301
+
302
+ # Auto-start the server when this module is imported
303
+ initialize_server()
304
+
UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # utils package
UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/asset_validation.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Asset Validation Utilities
3
+
4
+ Fixes for asset reference timing issues identified in LM_STUDIO_DIAGNOSTIC_REPORT.md
5
+ Prevents "Failed to find object" errors by validating assets before referencing.
6
+ """
7
+
8
+ import unreal
9
+ import time
10
+ from typing import Optional, List, Dict
11
+
12
+
13
+ def validate_asset_exists(asset_path: str) -> bool:
14
+ """
15
+ Check if an asset exists before referencing it.
16
+
17
+ Args:
18
+ asset_path: Unreal asset path (e.g., '/Game/Blueprints/BP_Test')
19
+
20
+ Returns:
21
+ True if asset exists and is valid, False otherwise
22
+
23
+ Example:
24
+ >>> validate_asset_exists('/Game/Blueprints/BP_Test')
25
+ True
26
+ """
27
+ try:
28
+ asset = unreal.EditorAssetLibrary.find_asset_data(asset_path)
29
+ return asset.is_valid()
30
+ except Exception as e:
31
+ unreal.log_warning(f"Asset validation error for '{asset_path}': {e}")
32
+ return False
33
+
34
+
35
+ def discover_blueprints(folder_path: str = "/Game", recursive: bool = True) -> List[str]:
36
+ """
37
+ List all available blueprints in the project.
38
+
39
+ Args:
40
+ folder_path: Root folder to search (default: '/Game')
41
+ recursive: Search subfolders (default: True)
42
+
43
+ Returns:
44
+ List of blueprint asset paths
45
+
46
+ Example:
47
+ >>> discover_blueprints('/Game/Blueprints')
48
+ ['/Game/Blueprints/BP_Test', '/Game/Blueprints/BP_Player']
49
+ """
50
+ try:
51
+ all_assets = unreal.EditorAssetLibrary.list_assets(
52
+ folder_path,
53
+ recursive=recursive,
54
+ include_folder=False
55
+ )
56
+
57
+ # Filter for blueprints only
58
+ blueprints = []
59
+ for asset_path in all_assets:
60
+ if asset_path.endswith('_C') or 'Blueprint' in asset_path:
61
+ blueprints.append(asset_path)
62
+
63
+ return blueprints
64
+ except Exception as e:
65
+ unreal.log_error(f"Blueprint discovery error: {e}")
66
+ return []
67
+
68
+
69
+ def normalize_blueprint_path(path: str) -> str:
70
+ """
71
+ Normalize blueprint path to standard format.
72
+
73
+ Handles various path formats and fixes common issues:
74
+ - Removes .uasset extensions
75
+ - Fixes double references (BP_Test.BP_Test)
76
+ - Ensures /Game prefix
77
+
78
+ Args:
79
+ path: Blueprint path in any format
80
+
81
+ Returns:
82
+ Normalized path
83
+
84
+ Example:
85
+ >>> normalize_blueprint_path('/Game/Blueprints/BP_Test.BP_Test')
86
+ '/Game/Blueprints/BP_Test'
87
+ """
88
+ # Remove .uasset extension
89
+ path = path.replace('.uasset', '')
90
+
91
+ # Fix double reference (e.g., BP_Test.BP_Test -> BP_Test)
92
+ parts = path.split('/')
93
+ if parts and '.' in parts[-1]:
94
+ base_name = parts[-1].split('.')[0]
95
+ parts[-1] = base_name
96
+ path = '/'.join(parts)
97
+
98
+ # Ensure /Game prefix
99
+ if not path.startswith('/Game'):
100
+ path = f'/Game/{path.lstrip("/")}'
101
+
102
+ return path
103
+
104
+
105
+ def wait_for_asset_save(asset_path: str, timeout: float = 2.0, check_interval: float = 0.1) -> bool:
106
+ """
107
+ Wait for an asset to be saved and become available.
108
+
109
+ Args:
110
+ asset_path: Path to the asset
111
+ timeout: Maximum time to wait in seconds (default: 2.0)
112
+ check_interval: How often to check in seconds (default: 0.1)
113
+
114
+ Returns:
115
+ True if asset became available, False if timeout
116
+
117
+ Example:
118
+ >>> wait_for_asset_save('/Game/Blueprints/BP_NewBlueprint')
119
+ True
120
+ """
121
+ elapsed = 0.0
122
+ while elapsed < timeout:
123
+ if validate_asset_exists(asset_path):
124
+ return True
125
+ time.sleep(check_interval)
126
+ elapsed += check_interval
127
+
128
+ unreal.log_warning(f"Timeout waiting for asset: {asset_path}")
129
+ return False
130
+
131
+
132
+ def get_available_blueprints_message(folder_path: str = "/Game/Blueprints") -> str:
133
+ """
134
+ Generate a helpful error message listing available blueprints.
135
+
136
+ Args:
137
+ folder_path: Folder to search for blueprints
138
+
139
+ Returns:
140
+ Formatted message with available blueprints
141
+
142
+ Example:
143
+ >>> print(get_available_blueprints_message())
144
+ Available blueprints in /Game/Blueprints:
145
+ - BP_TestActor
146
+ - BP_PlayerCharacter
147
+ """
148
+ blueprints = discover_blueprints(folder_path)
149
+
150
+ if not blueprints:
151
+ return f"No blueprints found in {folder_path}"
152
+
153
+ message = f"Available blueprints in {folder_path}:\n"
154
+ for bp in blueprints[:10]: # Limit to first 10
155
+ bp_name = bp.split('/')[-1]
156
+ message += f" - {bp_name}\n"
157
+
158
+ if len(blueprints) > 10:
159
+ message += f" ... and {len(blueprints) - 10} more"
160
+
161
+ return message
162
+
163
+
164
+ def suggest_similar_blueprint(target_name: str, folder_path: str = "/Game") -> Optional[str]:
165
+ """
166
+ Suggest a similar blueprint name if exact match not found.
167
+
168
+ Args:
169
+ target_name: The blueprint name being searched for
170
+ folder_path: Folder to search in
171
+
172
+ Returns:
173
+ Suggested blueprint path or None
174
+
175
+ Example:
176
+ >>> suggest_similar_blueprint('BP_Test', '/Game/Blueprints')
177
+ '/Game/Blueprints/BP_TestActor'
178
+ """
179
+ blueprints = discover_blueprints(folder_path)
180
+ target_lower = target_name.lower()
181
+
182
+ # Look for partial matches
183
+ for bp in blueprints:
184
+ bp_name = bp.split('/')[-1].lower()
185
+ if target_lower in bp_name or bp_name in target_lower:
186
+ return bp
187
+
188
+ return None
189
+
190
+
191
+ def validate_asset_with_suggestion(asset_path: str) -> Dict[str, any]:
192
+ """
193
+ Validate asset and provide helpful error message with suggestions.
194
+
195
+ Args:
196
+ asset_path: Asset path to validate
197
+
198
+ Returns:
199
+ Dictionary with 'valid', 'message', and optional 'suggestion' keys
200
+
201
+ Example:
202
+ >>> result = validate_asset_with_suggestion('/Game/Blueprints/BP_NotFound')
203
+ >>> print(result['message'])
204
+ Blueprint '/Game/Blueprints/BP_NotFound' not found.
205
+ Did you mean: /Game/Blueprints/BP_TestActor?
206
+ """
207
+ normalized_path = normalize_blueprint_path(asset_path)
208
+
209
+ if validate_asset_exists(normalized_path):
210
+ return {
211
+ 'valid': True,
212
+ 'message': f"Asset found: {normalized_path}"
213
+ }
214
+
215
+ # Asset not found - provide helpful error
216
+ folder_path = '/'.join(normalized_path.split('/')[:-1])
217
+ asset_name = normalized_path.split('/')[-1]
218
+
219
+ suggestion = suggest_similar_blueprint(asset_name, folder_path)
220
+
221
+ message = f"Blueprint '{normalized_path}' not found.\n"
222
+
223
+ if suggestion:
224
+ message += f"Did you mean: {suggestion}?\n"
225
+ else:
226
+ message += get_available_blueprints_message(folder_path)
227
+
228
+ return {
229
+ 'valid': False,
230
+ 'message': message,
231
+ 'suggestion': suggestion
232
+ }
UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/logging.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unreal
2
+ import traceback
3
+ from typing import Any
4
+
5
+
6
+ def log_info(message: str) -> None:
7
+ """
8
+ Log an informational message to the Unreal log
9
+
10
+ Args:
11
+ message: The message to log
12
+ """
13
+ unreal.log(f"[AI Plugin] {message}")
14
+
15
+
16
+ def log_warning(message: str) -> None:
17
+ """
18
+ Log a warning message to the Unreal log
19
+
20
+ Args:
21
+ message: The message to log
22
+ """
23
+ unreal.log_warning(f"[AI Plugin] {message}")
24
+
25
+
26
+ def log_error(message: str, include_traceback: bool = False) -> None:
27
+ """
28
+ Log an error message to the Unreal log
29
+
30
+ Args:
31
+ message: The message to log
32
+ include_traceback: Whether to include the traceback in the log
33
+ """
34
+ error_message = f"[AI Plugin] ERROR: {message}"
35
+ unreal.log_error(error_message)
36
+
37
+ if include_traceback:
38
+ tb = traceback.format_exc()
39
+ unreal.log_error(f"[AI Plugin] Traceback:\n{tb}")
40
+
41
+
42
+ def log_command(command_type: str, details: Any = None) -> None:
43
+ """
44
+ Log a command being processed
45
+
46
+ Args:
47
+ command_type: The type of command being processed
48
+ details: Optional details about the command
49
+ """
50
+ if details:
51
+ unreal.log(f"[AI Plugin] Processing {command_type} command: {details}")
52
+ else:
53
+ unreal.log(f"[AI Plugin] Processing {command_type} command")
54
+
55
+
56
+ def log_result(command_type: str, success: bool, details: Any = None) -> None:
57
+ """
58
+ Log the result of a command
59
+
60
+ Args:
61
+ command_type: The type of command that was processed
62
+ success: Whether the command was successful
63
+ details: Optional details about the result
64
+ """
65
+ status = "successful" if success else "failed"
66
+
67
+ if details:
68
+ unreal.log(f"[AI Plugin] {command_type} command {status}: {details}")
69
+ else:
70
+ unreal.log(f"[AI Plugin] {command_type} command {status}")
UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/unreal_conversions.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unreal
2
+ from typing import Tuple, List, Union, Optional
3
+
4
+
5
+ def to_unreal_vector(vector_data: Union[List[float], Tuple[float, float, float]]) -> unreal.Vector:
6
+ """
7
+ Convert a list or tuple of 3 floats to an Unreal Vector
8
+
9
+ Args:
10
+ vector_data: A list or tuple containing 3 floats [x, y, z]
11
+
12
+ Returns:
13
+ An unreal.Vector object
14
+ """
15
+ if not isinstance(vector_data, (list, tuple)) or len(vector_data) != 3:
16
+ raise ValueError("Vector data must be a list or tuple of 3 floats")
17
+
18
+ return unreal.Vector(vector_data[0], vector_data[1], vector_data[2])
19
+
20
+
21
+ def to_unreal_rotator(rotation_data: Union[List[float], Tuple[float, float, float]]) -> unreal.Rotator:
22
+ """
23
+ Convert a list or tuple of 3 floats to an Unreal Rotator
24
+
25
+ Args:
26
+ rotation_data: A list or tuple containing 3 floats [pitch, yaw, roll]
27
+
28
+ Returns:
29
+ An unreal.Rotator object
30
+ """
31
+ if not isinstance(rotation_data, (list, tuple)) or len(rotation_data) != 3:
32
+ raise ValueError("Rotation data must be a list or tuple of 3 floats")
33
+
34
+ return unreal.Rotator(rotation_data[0], rotation_data[1], rotation_data[2])
35
+
36
+
37
+ def to_unreal_color(color_data: Union[List[float], Tuple[float, float, float]]) -> unreal.LinearColor:
38
+ """
39
+ Convert a list or tuple of 3 floats to an Unreal LinearColor
40
+
41
+ Args:
42
+ color_data: A list or tuple containing 3 floats [r, g, b]
43
+
44
+ Returns:
45
+ An unreal.LinearColor object with alpha=1.0
46
+ """
47
+ if not isinstance(color_data, (list, tuple)) or len(color_data) < 3:
48
+ raise ValueError("Color data must be a list or tuple of at least 3 floats")
49
+
50
+ alpha = 1.0
51
+ if len(color_data) > 3:
52
+ alpha = color_data[3]
53
+
54
+ return unreal.LinearColor(color_data[0], color_data[1], color_data[2], alpha)
UMCP.it-Unreal-Organizer-Assistant/Content/unreal_server_init.py ADDED
@@ -0,0 +1,1357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unreal
2
+ import json
3
+ import sys
4
+ import traceback
5
+
6
+ class MCPUnrealBridge:
7
+
8
+ @staticmethod
9
+ def get_actors():
10
+ """Get all actors in the current level"""
11
+ result = []
12
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) #unreal.EditorActorSubsystem().get_editor_subsystem()
13
+ actors = actor_subsystem.get_all_level_actors()
14
+
15
+ for actor in actors:
16
+ result.append({
17
+ "name": actor.get_name(),
18
+ "class": actor.get_class().get_name(),
19
+ "location": str(actor.get_actor_location())
20
+ })
21
+
22
+ return json.dumps({"status": "success", "result": result})
23
+
24
+ @staticmethod
25
+ def get_actor_details(actor_name):
26
+ """Get details for a specific actor by name."""
27
+ result = {}
28
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) #unreal.EditorActorSubsystem().get_editor_subsystem()
29
+ actors = actor_subsystem.get_all_level_actors()
30
+
31
+ for actor in actors:
32
+ if actor.get_name() == actor_name:
33
+ result = {
34
+ "name": actor.get_name(),
35
+ "class": actor.get_class().get_name(),
36
+ "location": str(actor.get_actor_location())
37
+ }
38
+ break
39
+
40
+ if not result:
41
+ result = f"Actor not found: {actor_name}"
42
+
43
+ return json.dumps({"status": "success", "result": result})
44
+
45
+ @staticmethod
46
+ def spawn_actor(asset_path, location_x=0, location_y=0, location_z=0, rotation_x=0, rotation_y=0, rotation_z=0, scale_x=0, scale_y=0, scale_z=0):
47
+ """Spawn a new actor in the level"""
48
+ try:
49
+
50
+ # Find the class reference
51
+ class_obj = unreal.load_asset(asset_path)
52
+ if not class_obj:
53
+ return json.dumps({"status": "error", "message": f"Asset '{asset_path}' not found"})
54
+
55
+ # Create the actor
56
+ location = unreal.Vector(float(location_x), float(location_y), float(location_z))
57
+ rotation = unreal.Rotator(rotation_x, rotation_y, rotation_z)
58
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
59
+ actor = actor_subsystem.spawn_actor_from_object(class_obj, location, rotation)
60
+
61
+ if actor:
62
+ actor.set_actor_scale3d(unreal.Vector(scale_x, scale_y, scale_z))
63
+ return json.dumps({
64
+ "status": "success",
65
+ "result": f"Created {asset_path} actor named '{actor.get_name()}' at location ({location_x}, {location_y}, {location_z})"
66
+ })
67
+ else:
68
+ return json.dumps({ "status": "error", "message" : "Failed to create actor" })
69
+ except Exception as e :
70
+ return json.dumps({ "status": "error", "message" : str(e) })
71
+
72
+ @staticmethod
73
+ def modify_actor(actor_name, property_name, property_value):
74
+ """Modify a property of an existing actor"""
75
+ try:
76
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) #unreal.EditorActorSubsystem().get_editor_subsystem()
77
+ actors = actor_subsystem.get_all_level_actors()
78
+
79
+ for actor in actors:
80
+ if actor.get_name() == actor_name:
81
+ # Try to determine property type and convert value
82
+ current_value = getattr(actor, property_name, None)
83
+ if current_value is not None:
84
+ if isinstance(current_value, float):
85
+ setattr(actor, property_name, float(property_value))
86
+ elif isinstance(current_value, int):
87
+ setattr(actor, property_name, int(property_value))
88
+ elif isinstance(current_value, bool):
89
+ setattr(actor, property_name, property_value.lower() in['true', 'yes', '1'])
90
+ elif isinstance(current_value, unreal.Vector):
91
+ # Assuming format like "X,Y,Z"
92
+ x, y, z = map(float, property_value.split(','))
93
+ setattr(actor, property_name, unreal.Vector(x, y, z))
94
+ else:
95
+ # Default to string
96
+ setattr(actor, property_name, property_value)
97
+
98
+ return json.dumps({
99
+ "status": "success",
100
+ "result" : f"Modified {property_name} on {actor_name} to {property_value}"
101
+ })
102
+ else:
103
+ return json.dumps({
104
+ "status": "error",
105
+ "message" : f"Property {property_name} not found on {actor_name}"
106
+ })
107
+
108
+ return json.dumps({ "status": "error", "message" : f"Actor '{actor_name}' not found" })
109
+ except Exception as e :
110
+ return json.dumps({ "status": "error", "message" : str(e) })
111
+
112
+ @staticmethod
113
+ def get_selected_actors():
114
+ """Get the currently selected actors in the editor"""
115
+ try:
116
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) #unreal.EditorActorSubsystem().get_editor_subsystem()
117
+ selected_actors = actor_subsystem.get_selected_level_actors()
118
+
119
+ result = []
120
+ for actor in selected_actors:
121
+ result.append({
122
+ "name": actor.get_name(),
123
+ "class" : actor.get_class().get_name(),
124
+ "location" : str(actor.get_actor_location())
125
+ })
126
+
127
+ return json.dumps({ "status": "success", "result": result })
128
+ except Exception as e:
129
+ return json.dumps({ "status": "error", "message": str(e) })
130
+
131
+ @staticmethod
132
+ def set_material(actor_name, material_path):
133
+ """Apply a material to a static mesh actor"""
134
+ try:
135
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) #unreal.EditorActorSubsystem().get_editor_subsystem()
136
+ actors = actor_subsystem.get_all_level_actors()
137
+
138
+ # Find the actor
139
+ target_actor = None
140
+ for actor in actors :
141
+ if actor.get_name() == actor_name :
142
+ target_actor = actor
143
+ break
144
+
145
+ if not target_actor:
146
+ return json.dumps({ "status": "error", "message" : f"Actor '{actor_name}' not found" })
147
+
148
+ # Check if it's a static mesh actor
149
+ if not target_actor.is_a(unreal.StaticMeshActor) :
150
+ return json.dumps({
151
+ "status": "error",
152
+ "message": f"Actor '{actor_name}' is not a StaticMeshActor"
153
+ })
154
+
155
+ # Load the material
156
+ material = unreal.load_object(None, material_path)
157
+ if not material:
158
+ return json.dumps({
159
+ "status": "error",
160
+ "message": f"Material '{material_path}' not found"
161
+ })
162
+
163
+ # Get the static mesh component
164
+ static_mesh_component = target_actor.get_component_by_class(unreal.StaticMeshComponent)
165
+ if not static_mesh_component:
166
+ return json.dumps({
167
+ "status": "error",
168
+ "message" : f"No StaticMeshComponent found on actor '{actor_name}'"
169
+ })
170
+
171
+ # Set the material
172
+ static_mesh_component.set_material(0, material)
173
+ return json.dumps({
174
+ "status": "success",
175
+ "result" : f"Applied material '{material_path}' to actor '{actor_name}'"
176
+ })
177
+ except Exception as e :
178
+ return json.dumps({ "status": "error", "message" : str(e) })
179
+
180
+ @staticmethod
181
+ def delete_all_static_mesh_actors():
182
+ """Delete all static mesh actors in the scene"""
183
+
184
+ try:
185
+
186
+ # Get the editor subsystem
187
+ editor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
188
+
189
+ # Get all actors in the current level
190
+ try:
191
+ all_actors = editor_subsystem.get_all_level_actors()
192
+ except DeprecationWarning as dw:
193
+ # do nothing
194
+ pass
195
+
196
+ # Filter for StaticMeshActors
197
+ static_mesh_actors = [actor for actor in all_actors if isinstance(actor, unreal.StaticMeshActor)]
198
+
199
+ # Print how many StaticMeshActors were found
200
+ #print(f"Found {len(static_mesh_actors)} StaticMeshActors")
201
+
202
+ # Delete all StaticMeshActors
203
+ delete_count = 0
204
+ for actor in static_mesh_actors:
205
+ actor_name = actor.get_actor_label()
206
+ success = editor_subsystem.destroy_actor(actor)
207
+ if success:
208
+ delete_count = delete_count + 1
209
+ #print(f"Deleted StaticMeshActor: {actor_name}")
210
+ #else:
211
+ #print(f"Failed to delete StaticMeshActor: {actor_name}")
212
+
213
+ return json.dumps({
214
+ "status": "success",
215
+ "result" : f"Found {len(static_mesh_actors)} StaticMeshActors. Deleted {delete_count} StaticMeshActors."
216
+ })
217
+
218
+ except Exception as e:
219
+ return json.dumps({ "status": "error", "message": str(e) })
220
+
221
+ @staticmethod
222
+ def get_project_dir():
223
+ """Get the top level project directory"""
224
+ try:
225
+ project_dir = unreal.Paths.project_dir()
226
+ return json.dumps({
227
+ "status": "success",
228
+ "result" : f"{project_dir}"
229
+ })
230
+ except Exception as e:
231
+ return json.dumps({ "status": "error", "message": str(e) })
232
+
233
+ @staticmethod
234
+ def get_content_dir():
235
+ """Get the content directory"""
236
+ try:
237
+ #plugins_dir = unreal.Paths.project_plugins_dir()
238
+ #saved_dir = unreal.Paths.project_saved_dir()
239
+ #config_dir = unreal.Paths.project_config_dir()
240
+ content_dir = unreal.Paths.project_content_dir()
241
+ return json.dumps({
242
+ "status": "success",
243
+ "result" : f"{content_dir}"
244
+ })
245
+ except Exception as e:
246
+ return json.dumps({ "status": "error", "message": str(e) })
247
+
248
+ @staticmethod
249
+ def find_basic_shapes():
250
+ """Search for basic shapes for building"""
251
+
252
+ try:
253
+ # Search for the asset
254
+ asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
255
+ assets = asset_registry.get_assets_by_path('/Engine/BasicShapes', recursive=True)
256
+
257
+ # Find
258
+ tile_asset_paths = []
259
+ for asset in assets:
260
+ if asset.get_class().get_name() == 'StaticMesh':
261
+ tile_asset_paths.append(str(asset.package_name))
262
+
263
+ if not tile_asset_paths:
264
+ return json.dumps({ "status": "error", "message": f"Could not find basic shapes." })
265
+ else:
266
+ return json.dumps({
267
+ "status": "success",
268
+ "result" : tile_asset_paths
269
+ })
270
+
271
+ except Exception as e:
272
+ return json.dumps({ "status": "error", "message": str(e) })
273
+
274
+ @staticmethod
275
+ def find_assets(asset_name):
276
+ """Search for specific assets by name, like Floor, Wall, Door"""
277
+
278
+ try:
279
+ # Search for the asset
280
+ asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
281
+ assets = asset_registry.get_assets_by_path('/Game', recursive=True)
282
+
283
+ # Find
284
+ tile_asset_paths = []
285
+ asset_name_lower = asset_name.lower()
286
+ for asset in assets:
287
+ this_name = str(asset.asset_name).lower()
288
+ if asset_name_lower in this_name:
289
+ asset_data = { "asset_class": str(asset.asset_class_path.asset_name), "asset_path": str(asset.package_name)}
290
+ tile_asset_paths.append(asset_data)
291
+
292
+ if not tile_asset_paths:
293
+ return json.dumps({ "status": "error", "message": f"Could not find {asset_name} asset." })
294
+ else:
295
+ return json.dumps({
296
+ "status": "success",
297
+ "result" : tile_asset_paths
298
+ })
299
+
300
+ except Exception as e:
301
+ return json.dumps({ "status": "error", "message": str(e) })
302
+
303
+ @staticmethod
304
+ def get_asset(asset_path):
305
+ # Define the correct path to the asset
306
+
307
+ # Try to load the asset
308
+ try:
309
+ #print("Loading asset...")
310
+ static_mesh = unreal.EditorAssetLibrary.load_asset(asset_path)
311
+ #print("Asset loaded: " + str(static_mesh != None))
312
+
313
+ if static_mesh:
314
+ #print("Asset class: " + static_mesh.__class__.__name__)
315
+
316
+ # Try to spawn an actor
317
+ try:
318
+ bounds = static_mesh.get_bounds()
319
+ bounds_min = bounds.origin - bounds.box_extent
320
+ bounds_max = bounds.origin + bounds.box_extent
321
+ width = bounds.box_extent.x * 2
322
+ depth = bounds.box_extent.y * 2
323
+ height = bounds.box_extent.z * 2
324
+
325
+ # Print dimensions
326
+ #print(f"Asset: SM_Env_Tiles_05")
327
+ #print(f"Bounds Min: {bounds_min}")
328
+ #print(f"Bounds Max: {bounds_max}")
329
+ #print(f"Dimensions (width ร— depth ร— height): {width} ร— {depth} ร— {height}")
330
+ #print(f"Volume: {bounds.box_extent.x * 2 * bounds.box_extent.y * 2 * bounds.box_extent.z * 2}")
331
+
332
+ result = {"width": width, "depth": depth, "height": height, "origin_x": bounds.origin.x, "origin_y": bounds.origin.y, "origin_z": bounds.origin.z}
333
+
334
+ return json.dumps({
335
+ "status": "success",
336
+ "result" : result
337
+ })
338
+
339
+ except Exception as e:
340
+ return json.dumps({ "status": "error", "message": str(e) })
341
+ else:
342
+ return json.dumps({ "status": "error", "message": f"Asset could not be loaded." })
343
+ except Exception as e:
344
+ return json.dumps({ "status": "error", "message": f"Error loading asset: {str(e)}" })
345
+
346
+ @staticmethod
347
+ def create_grid(asset_path, grid_width, grid_length):
348
+ import math
349
+
350
+ try:
351
+
352
+ # Load the static mesh
353
+ floor_asset = unreal.EditorAssetLibrary.load_asset(asset_path)
354
+ if not floor_asset:
355
+ return json.dumps({ "status": "error", "message": f"Failed to load static mesh: {asset_path}" })
356
+
357
+ # Grid dimensions
358
+ width = int(grid_width)
359
+ length = int(grid_length)
360
+
361
+ # Get asset dimensions
362
+ bounds = floor_asset.get_bounds()
363
+ tile_width = bounds.box_extent.x * 2
364
+ tile_length = bounds.box_extent.y * 2
365
+
366
+ # Create grid of floor tiles
367
+ tiles_created = 0
368
+ for x in range(width):
369
+ for y in range(length):
370
+ # Calculate position
371
+ location = unreal.Vector(x * tile_width, y * tile_length, 0)
372
+
373
+ # Create actor using EditorLevelLibrary
374
+ try:
375
+ actor = unreal.EditorLevelLibrary.spawn_actor_from_object(
376
+ floor_asset,
377
+ location,
378
+ unreal.Rotator(0, 0, 0)
379
+ )
380
+ except DeprecationWarning as dw:
381
+ # do nothing
382
+ pass
383
+
384
+ if actor:
385
+ tiles_created += 1
386
+ actor.set_actor_label(f"FloorTile_{x}_{y}")
387
+
388
+ center_x = width // 2
389
+ center_y = length // 2
390
+ position_x = center_x * tile_width
391
+ position_y = center_y * tile_length
392
+ return json.dumps({
393
+ "status": "success",
394
+ "result" : f"Successfully created grid centered at tile location: ({center_x}, {center_y}) and world location: ({position_x}, {position_y}, 0.0))."
395
+ })
396
+
397
+ except Exception as e:
398
+ return json.dumps({ "status": "error", "message": f"Error loading asset: {str(e)}" })
399
+
400
+ @staticmethod
401
+ def create_town(town_center_x=1250, town_center_y=1250, town_width=7000, town_height=7000):
402
+ """
403
+ Create a town using supplied assets with customizable size and position
404
+
405
+ Args:
406
+ town_center_x (float): X coordinate of the town center
407
+ town_center_y (float): Y coordinate of the town center
408
+ town_width (float): Total width of the town area
409
+ town_height (float): Total height of the town area
410
+ """
411
+
412
+ import random
413
+ import math
414
+
415
+ #print(f"Starting to build fantasy town at ({town_center_x}, {town_center_y}) with size {town_width}x{town_height}...")
416
+
417
+ # Base paths for our assets
418
+ model_base_path = "/Game/HandPaintedEnvironment/Assets/Models"
419
+
420
+ # Helper function to load an asset
421
+ def load_asset(asset_name):
422
+ full_path = f"{model_base_path}/{asset_name}.{asset_name}"
423
+ return unreal.EditorAssetLibrary.load_asset(full_path)
424
+
425
+ # Track placed objects for collision detection
426
+ placed_objects = []
427
+
428
+ # Helper function to check for collision with existing objects
429
+ def check_collision(new_bounds, tolerance=10.0):
430
+ """Check if the new object bounds overlap with any existing objects"""
431
+ for obj_bounds in placed_objects:
432
+ # Check if the bounds overlap in X, Y dimensions with some tolerance
433
+ if (new_bounds["min_x"] - tolerance <= obj_bounds["max_x"] and
434
+ new_bounds["max_x"] + tolerance >= obj_bounds["min_x"] and
435
+ new_bounds["min_y"] - tolerance <= obj_bounds["max_y"] and
436
+ new_bounds["max_y"] + tolerance >= obj_bounds["min_y"]):
437
+ return True
438
+ return False
439
+
440
+ # Helper function to place an actor with collision detection
441
+ def place_actor(static_mesh, x, y, z=0, rotation_z=0, scale=(1.0, 1.0, 1.0), name=None, max_attempts=5):
442
+ if not static_mesh:
443
+ #print(f"Cannot place actor: Static mesh is invalid")
444
+ return None
445
+
446
+ editor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
447
+
448
+ # Get the bounds of the mesh
449
+ bounds = static_mesh.get_bounds()
450
+ bounds_min = bounds.origin - bounds.box_extent
451
+ bounds_max = bounds.origin + bounds.box_extent
452
+
453
+ # Calculate width, depth, height
454
+ width = bounds.box_extent.x * scale[0]
455
+ depth = bounds.box_extent.y * scale[1]
456
+ height = bounds.box_extent.z * scale[2]
457
+
458
+ # Try multiple positions if collision occurs
459
+ for attempt in range(max_attempts):
460
+ # Add small variation to position for retry attempts after the first
461
+ x_offset = 0
462
+ y_offset = 0
463
+ if attempt > 0:
464
+ jitter_range = 20.0 * attempt # Increase jitter with each attempt
465
+ x_offset = random.uniform(-jitter_range, jitter_range)
466
+ y_offset = random.uniform(-jitter_range, jitter_range)
467
+
468
+ pos_x = x + x_offset
469
+ pos_y = y + y_offset
470
+
471
+ # Calculate the bounds of the actor at this position with rotation
472
+ rad_rotation = math.radians(rotation_z)
473
+ sin_rot = math.sin(rad_rotation)
474
+ cos_rot = math.cos(rad_rotation)
475
+
476
+ # Calculate the 4 corners of the bounding box after rotation
477
+ corners = [
478
+ (pos_x - width/2, pos_y - depth/2), # Bottom-left
479
+ (pos_x + width/2, pos_y - depth/2), # Bottom-right
480
+ (pos_x + width/2, pos_y + depth/2), # Top-right
481
+ (pos_x - width/2, pos_y + depth/2) # Top-left
482
+ ]
483
+
484
+ # Rotate the corners
485
+ rotated_corners = []
486
+ for corner_x, corner_y in corners:
487
+ dx = corner_x - pos_x
488
+ dy = corner_y - pos_y
489
+ rotated_x = pos_x + dx * cos_rot - dy * sin_rot
490
+ rotated_y = pos_y + dx * sin_rot + dy * cos_rot
491
+ rotated_corners.append((rotated_x, rotated_y))
492
+
493
+ # Find the extents of the rotated box
494
+ x_values = [p[0] for p in rotated_corners]
495
+ y_values = [p[1] for p in rotated_corners]
496
+
497
+ min_x = min(x_values)
498
+ max_x = max(x_values)
499
+ min_y = min(y_values)
500
+ max_y = max(y_values)
501
+
502
+ new_bounds = {
503
+ "min_x": min_x,
504
+ "max_x": max_x,
505
+ "min_y": min_y,
506
+ "max_y": max_y,
507
+ "center_x": pos_x,
508
+ "center_y": pos_y
509
+ }
510
+
511
+ # Check if this position would cause a collision
512
+ if not check_collision(new_bounds):
513
+ # No collision, so place the actor
514
+ location = unreal.Vector(pos_x, pos_y, z)
515
+ rotation = unreal.Rotator(0, 0, rotation_z)
516
+ actor = editor_subsystem.spawn_actor_from_object(static_mesh, location, rotation)
517
+
518
+ if actor:
519
+ if name:
520
+ actor.set_actor_label(name)
521
+
522
+ # Apply scale if needed
523
+ if scale != (1.0, 1.0, 1.0):
524
+ actor.set_actor_scale3d(unreal.Vector(scale[0], scale[1], scale[2]))
525
+
526
+ # Record the placed object
527
+ placed_objects.append(new_bounds)
528
+
529
+ #if attempt > 0:
530
+ # print(f"Placed {name} at alternate position after {attempt+1} attempts")
531
+ return actor
532
+
533
+ #print(f"Failed to place {name} after {max_attempts} attempts due to collisions")
534
+ return None
535
+
536
+ # Helper function to get mesh dimensions
537
+ def get_mesh_dimensions(static_mesh, scale=(1.0, 1.0, 1.0)):
538
+ if not static_mesh:
539
+ return 0, 0, 0
540
+
541
+ bounds = static_mesh.get_bounds()
542
+ width = bounds.box_extent.x * scale[0]
543
+ depth = bounds.box_extent.y * scale[1]
544
+ height = bounds.box_extent.z * scale[2]
545
+
546
+ return width, depth, height
547
+
548
+ # New helper for placing continuous walls or fences
549
+ def place_continuous_segments(mesh, start_point, end_point, z=0, rotation_offset=0, scale=(1.0, 1.0, 1.0), name_prefix="Segment", skip_indices=None):
550
+ if not mesh:
551
+ #print(f"Cannot place continuous segments: Mesh is invalid")
552
+ return []
553
+
554
+ if skip_indices is None:
555
+ skip_indices = []
556
+
557
+ # Get the dimensions of the mesh
558
+ mesh_width, mesh_depth, mesh_height = get_mesh_dimensions(mesh, scale)
559
+
560
+ # For walls/fences, we assume they extend primarily along one dimension (width)
561
+ segment_length = mesh_width
562
+
563
+ # Calculate distance and direction
564
+ start_x, start_y = start_point
565
+ end_x, end_y = end_point
566
+ dx = end_x - start_x
567
+ dy = end_y - start_y
568
+ distance = math.sqrt(dx*dx + dy*dy)
569
+
570
+ # Calculate the angle of the line
571
+ angle = math.degrees(math.atan2(dy, dx))
572
+
573
+ # Calculate number of segments needed to cover the distance
574
+ # Subtract a small overlap percentage to ensure segments connect properly
575
+ overlap_factor = 0.05 # 5% overlap to ensure no gaps
576
+ effective_segment_length = segment_length * (1 - overlap_factor)
577
+ num_segments = max(1, math.ceil(distance / effective_segment_length))
578
+
579
+ # Place segments along the line
580
+ placed_actors = []
581
+ for i in range(num_segments):
582
+ if i in skip_indices:
583
+ continue
584
+
585
+ # Calculate position along the line
586
+ t = i / num_segments
587
+ pos_x = start_x + t * dx
588
+ pos_y = start_y + t * dy
589
+
590
+ # Place the segment
591
+ actor = place_actor(mesh, pos_x, pos_y, z, angle + rotation_offset, scale, f"{name_prefix}_{i}")
592
+ if actor:
593
+ placed_actors.append(actor)
594
+
595
+ return placed_actors
596
+
597
+ try:
598
+ # Calculate the scale factor based on town size compared to the original
599
+ # Original size was approximately 700x700
600
+ width_scale = 1.0 #town_width / 700.0
601
+ height_scale = 1.0 #town_height / 700.0
602
+
603
+ # We'll use this to scale distances and positions
604
+ scale_factor = min(width_scale, height_scale) # Use the smaller scale to ensure everything fits
605
+
606
+ # STEP 1: PLACE BUILDINGS
607
+ #print("STEP 1: Placing buildings...")
608
+
609
+ # Load building assets
610
+ buildings = {
611
+ "town_hall": load_asset("Town_Hall"),
612
+ "large_house": load_asset("Large_house"),
613
+ "small_house": load_asset("Small_house"),
614
+ "baker_house": load_asset("Baker_house"),
615
+ "tavern": load_asset("Tavern"),
616
+ "witch_house": load_asset("Witch_house"),
617
+ "tower": load_asset("Tower"),
618
+ "mill": load_asset("Mill"),
619
+ "woodmill": load_asset("Woodmill"),
620
+ "forge": load_asset("Forge"),
621
+ "mine": load_asset("Mine")
622
+ }
623
+
624
+ # Check if assets loaded correctly
625
+ #for name, asset in buildings.items():
626
+ # if not asset:
627
+ # print(f"Failed to load {name}")
628
+
629
+ # Place central buildings - town hall
630
+ place_actor(buildings["town_hall"], town_center_x, town_center_y, 0, 0, name="Central_TownHall")
631
+
632
+ # Place tavern near the town center - scale the offset by our scale factor
633
+ place_actor(buildings["tavern"],
634
+ town_center_x + 800 * scale_factor,
635
+ town_center_y + 600 * scale_factor,
636
+ 0, 135, name="Tavern")
637
+
638
+ # Place houses around the center - scaled based on town size
639
+ house_positions = [
640
+ # North district
641
+ (town_center_x - 1800 * scale_factor, town_center_y - 1900 * scale_factor, 45, "small_house", "North_House_1"),
642
+ (town_center_x - 2100 * scale_factor, town_center_y - 2200 * scale_factor, 30, "baker_house", "North_BakerHouse"),
643
+ (town_center_x - 1300 * scale_factor, town_center_y - 2000 * scale_factor, 15, "small_house", "North_House_2"),
644
+
645
+ # East district
646
+ (town_center_x + 2200 * scale_factor, town_center_y - 1300 * scale_factor, 270, "large_house", "East_LargeHouse"),
647
+ (town_center_x + 2400 * scale_factor, town_center_y + 1200 * scale_factor, 300, "small_house", "East_House_1"),
648
+ (town_center_x + 1900 * scale_factor, town_center_y - 1700 * scale_factor, 315, "small_house", "East_House_2"),
649
+
650
+ # South district
651
+ (town_center_x + 1300 * scale_factor, town_center_y + 2100 * scale_factor, 180, "large_house", "South_LargeHouse"),
652
+ (town_center_x - 1200 * scale_factor, town_center_y + 1950 * scale_factor, 135, "small_house", "South_House_1"),
653
+
654
+ # West district
655
+ (town_center_x - 2000 * scale_factor, town_center_y + 1100 * scale_factor, 90, "large_house", "West_LargeHouse"),
656
+ (town_center_x - 2300 * scale_factor, town_center_y + 1500 * scale_factor, 45, "small_house", "West_House_1")
657
+ ]
658
+
659
+ # Place the houses
660
+ for x, y, rot, house_type, name in house_positions:
661
+ place_actor(buildings[house_type], x, y, 0, rot, name=name)
662
+
663
+ # Place special buildings
664
+ # Tower
665
+ place_actor(buildings["tower"],
666
+ town_center_x - 3000 * scale_factor,
667
+ town_center_y - 2900 * scale_factor,
668
+ 0, 45, name="North_Tower")
669
+
670
+ # Witch's house in a more secluded area
671
+ place_actor(buildings["witch_house"],
672
+ town_center_x + 3000 * scale_factor,
673
+ town_center_y + 2900 * scale_factor,
674
+ 0, 215, name="WitchHouse")
675
+
676
+ # Mill near water (imaginary river)
677
+ mill_x = town_center_x + 2900 * scale_factor
678
+ mill_y = town_center_y - 3000 * scale_factor
679
+ mill = place_actor(buildings["mill"], mill_x, mill_y, 0, 270, name="Watermill")
680
+
681
+ # Mill wings - need to be attached to the mill
682
+ mill_wings = load_asset("Mill_wings")
683
+ if mill and mill_wings:
684
+ place_actor(mill_wings, mill_x, mill_y, 0, 270, name="Watermill_Wings")
685
+
686
+ # Woodmill in a wooded area
687
+ woodmill_x = town_center_x - 2900 * scale_factor
688
+ woodmill_y = town_center_y - 2400 * scale_factor
689
+ place_actor(buildings["woodmill"], woodmill_x, woodmill_y, 0, 135, name="Woodmill")
690
+
691
+ woodmill_saw = load_asset("Woodmill_Saw")
692
+ if woodmill_saw:
693
+ place_actor(woodmill_saw, woodmill_x, woodmill_y, 0, 135, name="Woodmill_Saw")
694
+
695
+ # Forge
696
+ place_actor(buildings["forge"],
697
+ town_center_x + 1600 * scale_factor,
698
+ town_center_y - 1500 * scale_factor,
699
+ 0, 330, name="Forge")
700
+
701
+ # Mine at the edge of town
702
+ place_actor(buildings["mine"],
703
+ town_center_x - 2900 * scale_factor,
704
+ town_center_y + 2900 * scale_factor,
705
+ 0, 135, name="Mine")
706
+
707
+ #print("Buildings placed successfully")
708
+
709
+ # STEP 2: ADD NATURE ELEMENTS
710
+ #print("STEP 2: Adding trees, plants, and rocks...")
711
+
712
+ # Load nature assets
713
+ nature = {
714
+ "tree_1": load_asset("Tree_1"),
715
+ "tree_2": load_asset("Tree_2"),
716
+ "tree_4": load_asset("Tree_4"),
717
+ "pine_tree": load_asset("Pine_tree"),
718
+ "pine_tree_2": load_asset("Pine_tree_2"),
719
+ "bush_1": load_asset("Bush_1"),
720
+ "bush_2": load_asset("Bush_2"),
721
+ "fern": load_asset("Fern"),
722
+ "flowers_1": load_asset("Flowers_1"),
723
+ "flowers_2": load_asset("Flowers_2"),
724
+ "plant": load_asset("Plant"),
725
+ "rock_1": load_asset("Rock_1"),
726
+ "rock_2": load_asset("Rock_2"),
727
+ "rock_3": load_asset("Rock_3"),
728
+ "rock_4": load_asset("Rock_4"),
729
+ "stump": load_asset("Stump"),
730
+ "log": load_asset("Log"),
731
+ "mushroom_1": load_asset("Mushroom_1"),
732
+ "mushroom_2": load_asset("Mushroom_2")
733
+ }
734
+
735
+ # Create a forest area near the witch's house
736
+ forest_center_x = town_center_x + 2900 * scale_factor
737
+ forest_center_y = town_center_y + 2200 * scale_factor
738
+ forest_radius = 1500 * scale_factor
739
+
740
+ # Determine number of trees based on forest area
741
+ num_trees = int(500 * scale_factor)
742
+
743
+ # Add trees to the forest
744
+ for i in range(num_trees):
745
+ # Calculate random position within the forest area
746
+ angle = random.uniform(0, 2 * math.pi)
747
+ distance = random.uniform(0, forest_radius)
748
+ x = forest_center_x + distance * math.cos(angle)
749
+ y = forest_center_y + distance * math.sin(angle)
750
+
751
+ # Choose a random tree type
752
+ tree_type = random.choice(["tree_1", "tree_2", "tree_4", "pine_tree", "pine_tree_2"])
753
+ rot = random.uniform(0, 360)
754
+ scale = random.uniform(0.8, 1.2)
755
+
756
+ tree = place_actor(nature[tree_type], x, y, 0, rot, (scale, scale, scale), f"Forest_Tree_{i}")
757
+
758
+ # Add some undergrowth near trees if the tree was placed successfully
759
+ if tree and random.random() < 0.6:
760
+ undergrowth_type = random.choice(["bush_1", "bush_2", "fern", "mushroom_1", "mushroom_2"])
761
+ offset_x = random.uniform(-100, 100)
762
+ offset_y = random.uniform(-100, 100)
763
+ undergrowth_scale = random.uniform(0.7, 1.0)
764
+ place_actor(nature[undergrowth_type],
765
+ x + offset_x,
766
+ y + offset_y,
767
+ 0,
768
+ random.uniform(0, 360),
769
+ (undergrowth_scale, undergrowth_scale, undergrowth_scale),
770
+ f"Forest_Undergrowth_{i}")
771
+
772
+ # Add scattered trees around town - scale the number by town size
773
+ scattered_trees = int(100 * scale_factor)
774
+ for i in range(scattered_trees):
775
+ angle = random.uniform(0, 2 * math.pi)
776
+ distance = random.uniform(3000, 3000) * scale_factor
777
+ x = town_center_x + distance * math.cos(angle)
778
+ y = town_center_y + distance * math.sin(angle)
779
+
780
+ # Avoid placing in the forest area
781
+ forest_dist = math.sqrt((x - forest_center_x)**2 + (y - forest_center_y)**2)
782
+ if forest_dist < forest_radius:
783
+ continue
784
+
785
+ tree_type = random.choice(["tree_1", "tree_2", "tree_4"])
786
+ rot = random.uniform(0, 360)
787
+ scale = random.uniform(0.9, 1.1)
788
+
789
+ place_actor(nature[tree_type], x, y, 0, rot, (scale, scale, scale), f"Town_Tree_{i}")
790
+
791
+ # Add some rocks scattered around
792
+ num_rocks = int(15 * scale_factor)
793
+ for i in range(num_rocks):
794
+ angle = random.uniform(0, 2 * math.pi)
795
+ distance = random.uniform(3000, 3000) * scale_factor
796
+ x = town_center_x + distance * math.cos(angle)
797
+ y = town_center_y + distance * math.sin(angle)
798
+
799
+ rock_type = random.choice(["rock_1", "rock_2", "rock_3", "rock_4"])
800
+ rot = random.uniform(0, 360)
801
+ scale = random.uniform(0.8, 1.5)
802
+
803
+ place_actor(nature[rock_type], x, y, 0, rot, (scale, scale, scale), f"Rock_{i}")
804
+
805
+ # Create gardens near houses
806
+ houses = [
807
+ (town_center_x - 2000 * scale_factor, town_center_y - 1900 * scale_factor), # North_House_1
808
+ (town_center_x - 1300 * scale_factor, town_center_y - 2000 * scale_factor), # North_House_2
809
+ (town_center_x + 2400 * scale_factor, town_center_y + 1200 * scale_factor), # East_House_1
810
+ (town_center_x - 1200 * scale_factor, town_center_y + 1950 * scale_factor), # South_House_1
811
+ ]
812
+
813
+ for idx, (house_x, house_y) in enumerate(houses):
814
+ garden_x = house_x + random.uniform(200, 300)
815
+ garden_y = house_y + random.uniform(200, 300)
816
+
817
+ # Place flowers and plants in the garden
818
+ plants_per_garden = int(5 * scale_factor)
819
+ for j in range(plants_per_garden):
820
+ plant_x = garden_x + random.uniform(-100, 100)
821
+ plant_y = garden_y + random.uniform(-100, 100)
822
+ plant_type = random.choice(["flowers_1", "flowers_2", "plant", "bush_1"])
823
+ rot = random.uniform(0, 360)
824
+
825
+ place_actor(nature[plant_type], plant_x, plant_y, 0, rot, name=f"Garden_{idx}_Plant_{j}")
826
+
827
+ #print("Nature elements added successfully")
828
+
829
+ # STEP 3: CREATE FENCES
830
+ #print("STEP 3: Building fences...")
831
+
832
+ # Load fence assets
833
+ fences = {
834
+ "fence": load_asset("Fence"),
835
+ "fence_1": load_asset("Fence_1"),
836
+ "fence_2": load_asset("Fence_2"),
837
+ "stone_fence": load_asset("Stone_fence")
838
+ }
839
+
840
+ # Define garden perimeters to fence
841
+ gardens = [
842
+ {
843
+ "center": (town_center_x - 1800 * scale_factor, town_center_y - 1900 * scale_factor), # North_House_1
844
+ "size": (30 * scale_factor, 30 * scale_factor),
845
+ "fence_type": "fence",
846
+ "name": "North_House_1_Garden"
847
+ },
848
+ {
849
+ "center": (town_center_x + 2400 * scale_factor, town_center_y + 1200 * scale_factor), # East_House_1
850
+ "size": (25 * scale_factor, 30 * scale_factor),
851
+ "fence_type": "fence_1",
852
+ "name": "East_House_1_Garden"
853
+ },
854
+ {
855
+ "center": (town_center_x + 1300 * scale_factor, town_center_y + 2200 * scale_factor), # South_LargeHouse
856
+ "size": (35 * scale_factor, 35 * scale_factor),
857
+ "fence_type": "fence_2",
858
+ "name": "South_LargeHouse_Garden"
859
+ }
860
+ ]
861
+
862
+ # For each garden, create a fence perimeter
863
+ for garden in gardens:
864
+ center_x, center_y = garden["center"]
865
+ width, height = garden["size"]
866
+ fence_type = garden["fence_type"]
867
+ name_prefix = garden["name"]
868
+
869
+ # Calculate the corner points of the garden
870
+ half_width = width / 2
871
+ half_height = height / 2
872
+
873
+ corners = [
874
+ (center_x - half_width, center_y - half_height), # Bottom-left
875
+ (center_x + half_width, center_y - half_height), # Bottom-right
876
+ (center_x + half_width, center_y + half_height), # Top-right
877
+ (center_x - half_width, center_y + half_height) # Top-left
878
+ ]
879
+
880
+ # Build the perimeter with continuous segments
881
+ for i in range(4):
882
+ start_point = corners[i]
883
+ end_point = corners[(i + 1) % 4]
884
+
885
+ # For each side, determine if we need a gate
886
+ skip_indices = []
887
+ if i == 0: # Usually front of the garden has a gate
888
+ skip_indices = [1] # Skip middle segment for a gate
889
+
890
+ # Place continuous fence segments
891
+ place_continuous_segments(
892
+ fences[fence_type],
893
+ start_point,
894
+ end_point,
895
+ 0, # z coordinate
896
+ 90, # rotation offset (fences usually need to be rotated perpendicular to the line)
897
+ (1.0, 1.0, 1.0), # scale
898
+ f"{name_prefix}_Fence_{i}",
899
+ skip_indices
900
+ )
901
+
902
+ # Create a fence around the town center square
903
+ town_square = {
904
+ "center": (town_center_x, town_center_y),
905
+ "size": (90 * scale_factor, 90 * scale_factor),
906
+ "fence_type": "stone_fence",
907
+ "name": "Town_Square"
908
+ }
909
+
910
+ center_x, center_y = town_square["center"]
911
+ width, height = town_square["size"]
912
+ fence_type = town_square["fence_type"]
913
+ name_prefix = town_square["name"]
914
+
915
+ # Calculate the corner points
916
+ half_width = width / 2
917
+ half_height = height / 2
918
+
919
+ corners = [
920
+ (center_x - half_width, center_y - half_height), # Bottom-left
921
+ (center_x + half_width, center_y - half_height), # Bottom-right
922
+ (center_x + half_width, center_y + half_height), # Top-right
923
+ (center_x - half_width, center_y + half_height) # Top-left
924
+ ]
925
+
926
+ # Build the perimeter with continuous segments and gates
927
+ for i in range(4):
928
+ start_point = corners[i]
929
+ end_point = corners[(i + 1) % 4]
930
+
931
+ # Create entrance gates on all sides
932
+ skip_indices = [1] # Skip middle segment for a gate
933
+
934
+ # Place continuous stone fence segments
935
+ place_continuous_segments(
936
+ fences[fence_type],
937
+ start_point,
938
+ end_point,
939
+ 0, # z coordinate
940
+ 90, # rotation offset
941
+ (1.0, 1.0, 1.0), # scale
942
+ f"{name_prefix}_Fence_{i}",
943
+ skip_indices
944
+ )
945
+
946
+ #print("Fences created successfully")
947
+
948
+ # STEP 4: BUILD WALLS
949
+ #print("STEP 4: Building walls...")
950
+
951
+ # Load wall assets
952
+ walls = {
953
+ "wall_1": load_asset("Wall_1"),
954
+ "wall_2": load_asset("Wall_2")
955
+ }
956
+
957
+ # Get wall dimensions for alternating walls
958
+ wall1_width, _, _ = get_mesh_dimensions(walls["wall_1"])
959
+ wall2_width, _, _ = get_mesh_dimensions(walls["wall_2"])
960
+
961
+ # Define the town perimeter for walls - use the provided town width and height
962
+ town_perimeter = {
963
+ "center": (town_center_x, town_center_y),
964
+ "size": (town_width, town_height),
965
+ "name": "Town_Wall"
966
+ }
967
+
968
+ center_x, center_y = town_perimeter["center"]
969
+ width, height = town_perimeter["size"]
970
+ name_prefix = town_perimeter["name"]
971
+
972
+ # Calculate the corner points
973
+ half_width = (width / 2) - wall1_width
974
+ half_height = (height / 2) - wall1_width
975
+
976
+ corners = [
977
+ (center_x - half_width, center_y - half_height), # Bottom-left
978
+ (center_x + half_width, center_y - half_height), # Bottom-right
979
+ (center_x + half_width, center_y + half_height), # Top-right
980
+ (center_x - half_width, center_y + half_height) # Top-left
981
+ ]
982
+
983
+ # Build the perimeter with continuous wall segments
984
+ # For each side, first measure the total length and determine how many of each wall type to use
985
+ for i in range(4):
986
+ start_point = corners[i]
987
+ end_point = corners[(i + 1) % 4]
988
+
989
+ # Calculate side length
990
+ start_x, start_y = start_point
991
+ end_x, end_y = end_point
992
+ side_length = math.sqrt((end_x - start_x)**2 + (end_y - start_y)**2)
993
+
994
+ # Determine where to place gates - we'll place 1-2 gates on each side based on length
995
+ # Calculate how many walls would fit on this side
996
+ effective_wall1_length = wall1_width * 0.95 # Account for slight overlap
997
+ num_wall1_segments = math.ceil(side_length / effective_wall1_length)
998
+
999
+ # Identify gate positions - which segments to skip
1000
+ gate_skip_indices = []
1001
+ if num_wall1_segments > 8:
1002
+ # Place two gates if the wall is long enough
1003
+ gate_skip_indices = [num_wall1_segments // 3, 2 * num_wall1_segments // 3]
1004
+ else:
1005
+ # Otherwise just one gate in the middle
1006
+ gate_skip_indices = [num_wall1_segments // 2]
1007
+
1008
+ # Place the wall segments using primarily wall_1 with gates
1009
+ wall1_actors = place_continuous_segments(
1010
+ walls["wall_1"],
1011
+ start_point,
1012
+ end_point,
1013
+ 0, # z coordinate
1014
+ 0, # no rotation offset for walls
1015
+ (1.0, 1.0, 1.0), # scale
1016
+ f"{name_prefix}_Wall1_{i}",
1017
+ gate_skip_indices
1018
+ )
1019
+
1020
+ # For each gate position, place a tower
1021
+ for gate_idx in gate_skip_indices:
1022
+ t = gate_idx / num_wall1_segments
1023
+ pos_x = start_x + t * (end_x - start_x)
1024
+ pos_y = start_y + t * (end_y - start_y)
1025
+
1026
+ # Calculate angle
1027
+ angle = math.degrees(math.atan2(end_y - start_y, end_x - start_x))
1028
+
1029
+ # Place tower at gate position
1030
+ tower = load_asset("Tower")
1031
+ if tower:
1032
+ place_actor(tower, pos_x, pos_y, 0, angle + 90, name=f"Gate_Tower_{i}_{gate_idx}")
1033
+
1034
+ #print("Walls built successfully")
1035
+
1036
+ # STEP 5: ADD FINAL DETAILS
1037
+ #print("STEP 5: Adding final details and props...")
1038
+
1039
+ # Load remaining prop assets
1040
+ props = {
1041
+ "anvil": load_asset("Anvil"),
1042
+ "barrel": load_asset("Barrel"),
1043
+ "chest": load_asset("Chest"),
1044
+ "cauldron": load_asset("Cauldron"),
1045
+ "altar": load_asset("Altar"),
1046
+ "well": load_asset("Well"),
1047
+ "trolley": load_asset("Trolley"),
1048
+ "lamppost": load_asset("Lamppost"),
1049
+ "street_light": load_asset("street_light")
1050
+ }
1051
+
1052
+ # Place a well in the town center
1053
+ place_actor(props["well"], town_center_x + 150 * scale_factor, town_center_y - 150 * scale_factor, 0, 0, name="Town_Center_Well")
1054
+
1055
+ # Place streetlights around the town square
1056
+ square_size = 90 * scale_factor
1057
+ light_spacing = 30 * scale_factor
1058
+ for i in range(4):
1059
+ for j in range(3):
1060
+ if j == 1: # Skip middle to accommodate entrances
1061
+ continue
1062
+
1063
+ # Calculate position along each side of the square
1064
+ if i == 0: # North side
1065
+ x = town_center_x - square_size/2 + j * light_spacing
1066
+ y = town_center_y - square_size/2
1067
+ rot = 0
1068
+ elif i == 1: # East side
1069
+ x = town_center_x + square_size/2
1070
+ y = town_center_y - square_size/2 + j * light_spacing
1071
+ rot = 90
1072
+ elif i == 2: # South side
1073
+ x = town_center_x + square_size/2 - j * light_spacing
1074
+ y = town_center_y + square_size/2
1075
+ rot = 180
1076
+ else: # West side
1077
+ x = town_center_x - square_size/2
1078
+ y = town_center_y + square_size/2 - j * light_spacing
1079
+ rot = 270
1080
+
1081
+ light_type = "lamppost" if j % 2 == 0 else "street_light"
1082
+ place_actor(props[light_type], x, y, 0, rot, name=f"Square_Light_{i}_{j}")
1083
+
1084
+ # Add props near the forge
1085
+ forge_x = town_center_x + 600 * scale_factor
1086
+ forge_y = town_center_y - 500 * scale_factor
1087
+
1088
+ place_actor(props["anvil"], forge_x + 50 * scale_factor, forge_y - 80 * scale_factor, 0, 45, name="Forge_Anvil")
1089
+ place_actor(props["barrel"], forge_x - 70 * scale_factor, forge_y - 60 * scale_factor, 0, 0, name="Forge_Barrel")
1090
+
1091
+ # Add barrels and chests around the tavern
1092
+ tavern_x = town_center_x + 700 * scale_factor
1093
+ tavern_y = town_center_y + 600 * scale_factor
1094
+
1095
+ for i in range(3):
1096
+ x_offset = random.uniform(-150, 150) * scale_factor
1097
+ y_offset = random.uniform(-150, 150) * scale_factor
1098
+ rot = random.uniform(0, 360)
1099
+ place_actor(props["barrel"], tavern_x + x_offset, tavern_y + y_offset, 0, rot, name=f"Tavern_Barrel_{i}")
1100
+
1101
+ place_actor(props["chest"], tavern_x - 100 * scale_factor, tavern_y + 120 * scale_factor, 0, 45, name="Tavern_Chest")
1102
+
1103
+ # Add altar near the witch's house
1104
+ witch_house_x = town_center_x + 1800 * scale_factor
1105
+ witch_house_y = town_center_y + 1500 * scale_factor
1106
+
1107
+ place_actor(props["altar"], witch_house_x + 150 * scale_factor, witch_house_y + 100 * scale_factor, 0, 215, name="Witch_Altar")
1108
+ place_actor(props["cauldron"], witch_house_x - 80 * scale_factor, witch_house_y + 120 * scale_factor, 0, 0, name="Witch_Cauldron")
1109
+
1110
+ # Add a trolley near the mine
1111
+ mine_x = town_center_x - 2200 * scale_factor
1112
+ mine_y = town_center_y + 1800 * scale_factor
1113
+
1114
+ place_actor(props["trolley"], mine_x + 150 * scale_factor, mine_y - 100 * scale_factor, 0, 45, name="Mine_Trolley")
1115
+
1116
+ # Create a path connecting major locations with Tile assets
1117
+ tiles = {
1118
+ "tile_1": load_asset("Tile_1"),
1119
+ "tile_2": load_asset("Tile_2"),
1120
+ "tile_3": load_asset("Tile_3"),
1121
+ "tile_4": load_asset("Tile_4"),
1122
+ "tile_5": load_asset("Tile_5"),
1123
+ "tile_6": load_asset("Tile_6"),
1124
+ "tile_7": load_asset("Tile_7")
1125
+ }
1126
+
1127
+ # Define major path points - scaled based on town size
1128
+ path_points = [
1129
+ (town_center_x, town_center_y), # Town center
1130
+ (town_center_x + 700 * scale_factor, town_center_y + 600 * scale_factor), # Tavern
1131
+ (town_center_x + 600 * scale_factor, town_center_y - 500 * scale_factor), # Forge
1132
+ (town_center_x - 1000 * scale_factor, town_center_y - 1700 * scale_factor), # Tower
1133
+ (town_center_x + 1700 * scale_factor, town_center_y - 1200 * scale_factor), # Watermill
1134
+ (town_center_x - 1800 * scale_factor, town_center_y - 400 * scale_factor), # Woodmill
1135
+ (town_center_x - 2200 * scale_factor, town_center_y + 1800 * scale_factor), # Mine
1136
+ (town_center_x + 1800 * scale_factor, town_center_y + 1500 * scale_factor) # Witch house
1137
+ ]
1138
+
1139
+ # Create paths between points
1140
+ for i in range(len(path_points) - 1):
1141
+ start_x, start_y = path_points[i]
1142
+ end_x, end_y = path_points[i + 1]
1143
+
1144
+ # Calculate distance and direction
1145
+ dx = end_x - start_x
1146
+ dy = end_y - start_y
1147
+ distance = math.sqrt(dx*dx + dy*dy)
1148
+
1149
+ # Tile spacing - scale by the town size
1150
+ tile_spacing = 100 * scale_factor
1151
+ num_tiles = int(distance / tile_spacing)
1152
+
1153
+ # Place tiles along the path
1154
+ for j in range(num_tiles):
1155
+ t = j / num_tiles
1156
+ x = start_x + t * dx
1157
+ y = start_y + t * dy
1158
+
1159
+ # Add some randomness to make the path look more natural
1160
+ offset_x = random.uniform(-20, 20) * scale_factor
1161
+ offset_y = random.uniform(-20, 20) * scale_factor
1162
+
1163
+ # Random tile type
1164
+ tile_type = random.choice(["tile_1", "tile_2", "tile_3", "tile_4", "tile_5", "tile_6", "tile_7"])
1165
+ rot = random.uniform(0, 360)
1166
+
1167
+ place_actor(tiles[tile_type], x + offset_x, y + offset_y, 1, rot, name=f"Path_Tile_{i}_{j}")
1168
+
1169
+ #print("Final details added successfully")
1170
+ #print(f"Fantasy town creation complete at ({town_center_x}, {town_center_y}) with size {town_width}x{town_height}!")
1171
+
1172
+ return json.dumps({
1173
+ "status": "success",
1174
+ "result": f"Successfully created fantasy town at ({town_center_x}, {town_center_y}) with size {town_width}x{town_height}."
1175
+ })
1176
+
1177
+ except Exception as e:
1178
+ return json.dumps({ "status": "error", "message": f"Error building town: {str(e)}" })
1179
+
1180
+ @staticmethod
1181
+ def execute_blueprint_function(blueprint_name, function_name, arguments = ""):
1182
+ """Execute a function in a Blueprint"""
1183
+ try:
1184
+ # Find the Blueprint asset
1185
+ blueprint_asset = unreal.find_asset(blueprint_name)
1186
+ if not blueprint_asset:
1187
+ return json.dumps({
1188
+ "status": "error",
1189
+ "message" : f"Blueprint '{blueprint_name}' not found"
1190
+ })
1191
+
1192
+ # Get the blueprint class
1193
+ blueprint_class = unreal.load_class(blueprint_asset)
1194
+ if not blueprint_class:
1195
+ return json.dumps({
1196
+ "status": "error",
1197
+ "message": f"Could not load class from Blueprint '{blueprint_name}'"
1198
+ })
1199
+
1200
+ # Get the CDO(Class Default Object)
1201
+ cdo = unreal.get_default_object(blueprint_class)
1202
+
1203
+ # Parse arguments
1204
+ parsed_args = []
1205
+ if arguments:
1206
+ for arg in arguments.split(','):
1207
+ arg = arg.strip()
1208
+ # Try to determine argument type
1209
+ if arg.lower() in ['true', 'false']:
1210
+ parsed_args.append(arg.lower() == 'true')
1211
+ elif arg.isdigit():
1212
+ parsed_args.append(int(arg))
1213
+ elif arg.replace('.', '', 1).isdigit():
1214
+ parsed_args.append(float(arg))
1215
+ else:
1216
+ parsed_args.append(arg)
1217
+
1218
+ # Call the function
1219
+ result = getattr(cdo, function_name)(*parsed_args)
1220
+ return json.dumps({
1221
+ "status": "success",
1222
+ "result" : f"Function '{function_name}' executed. Result: {result}"
1223
+ })
1224
+ except Exception as e:
1225
+ return json.dumps({ "status": "error", "message": str(e) })
1226
+
1227
+ @staticmethod
1228
+ def execute_python(code):
1229
+ """Execute arbitrary Python code in Unreal Engine"""
1230
+
1231
+ try:
1232
+
1233
+ # decode and extract dict object
1234
+ #decoded = chunk.decode('utf-8', 'replace')
1235
+ #stripped = decoded.strip('\'"\n\r')
1236
+ replaced = code.replace('\\\\','\\').replace('\\"','"').replace("\\'","'").replace('f"\n', 'f"').replace('f\'\n', 'f\'').replace('\n"', '"').replace('\n\'', '\'')
1237
+ #response = json.loads(replaced)
1238
+
1239
+ # save to file first
1240
+ #code_file = open('code.py', 'w')
1241
+ #code_file.write(code)
1242
+ #code_file.close()
1243
+
1244
+ # read back
1245
+ #code_file = open('code.py', 'r')
1246
+ #code_text = code_file.read()
1247
+ #code_file.close()
1248
+
1249
+ # check for syntax errors
1250
+ try:
1251
+ code_obj = compile(replaced, '<string>', 'exec')
1252
+
1253
+ except SyntaxError as se:
1254
+ return json.dumps({
1255
+ "status": "error",
1256
+ "message" : f"Syntax Error executing Python code: {str(se)}",
1257
+ "traceback" : traceback.format_exc()
1258
+ })
1259
+ except Exception as ex:
1260
+ return json.dumps({
1261
+ "status": "error",
1262
+ "message" : f"Error executing Python code: {str(ex)}",
1263
+ "traceback" : traceback.format_exc()
1264
+ })
1265
+
1266
+ try:
1267
+
1268
+ # Create output capture file
1269
+ output_file = open('output.txt', 'w')
1270
+ error_file = open('error.txt', 'w')
1271
+
1272
+ # Store original stdout and stderr
1273
+ original_stdout = sys.stdout
1274
+ original_stderr = sys.stderr
1275
+
1276
+ # Redirect stdout and stderr
1277
+ sys.stdout = output_file
1278
+ sys.stderr = error_file
1279
+
1280
+ # Create a local dictionary for execution
1281
+ locals_dict = { 'unreal': unreal, 'result': None }
1282
+
1283
+ # Execute the compiled code
1284
+ exec(code_obj, globals(), locals_dict)
1285
+
1286
+ except AttributeError as ae:
1287
+ return json.dumps({
1288
+ "status": "error",
1289
+ "message" : f"Attribute Error in code: {str(ae)}",
1290
+ "traceback" : traceback.format_exc()
1291
+ })
1292
+
1293
+ except Exception as ee:
1294
+ return json.dumps({
1295
+ "status": "error",
1296
+ "message" : f"Python exec() error: {str(ee)}",
1297
+ "traceback" : traceback.format_exc()
1298
+ })
1299
+
1300
+ finally:
1301
+
1302
+ # close output
1303
+ output_file.close()
1304
+ error_file.close()
1305
+
1306
+ # Restore original stdout and stderr
1307
+ sys.stdout = original_stdout
1308
+ sys.stderr = original_stderr
1309
+
1310
+ # read output
1311
+ read_output = open('output.txt', 'r')
1312
+ result = read_output.read()
1313
+ read_output.close()
1314
+
1315
+ # read errors
1316
+ read_error = open('error.txt', 'r')
1317
+ error_text = read_error.read()
1318
+ read_error.close()
1319
+
1320
+ # Return the result if it was set
1321
+ if 'result' in locals_dict and locals_dict['result'] is not None:
1322
+ return json.dumps({
1323
+ "status": "success",
1324
+ "result" : str(locals_dict['result'])
1325
+ })
1326
+ elif error_text and len(error_text) > 0:
1327
+ return json.dumps({
1328
+ "status": "error",
1329
+ "result": error_text
1330
+ })
1331
+ elif not result:
1332
+ return json.dumps({
1333
+ "status": "error",
1334
+ "result": "Python code did not execute Successfully. No result set."
1335
+ })
1336
+ else:
1337
+ return json.dumps({
1338
+ "status": "success",
1339
+ "result": str(result)
1340
+ })
1341
+
1342
+ except Exception as exc:
1343
+
1344
+ # read error
1345
+ error_msg = str(exc)
1346
+ read_error = open('error.txt', 'r')
1347
+ if read_error:
1348
+ error_msg = read_error.read()
1349
+ read_error.close()
1350
+ return json.dumps({
1351
+ "status": "error",
1352
+ "message" : f"Python exec() error: {error_msg}",
1353
+ "traceback" : traceback.format_exc()
1354
+ })
1355
+
1356
+ # Register the bridge as a global variable
1357
+ mcp_bridge = MCPUnrealBridge()
UMCP.it-Unreal-Organizer-Assistant/Docs/BHDemoGif.gif ADDED

Git LFS Details

  • SHA256: e4ded340c6f77730088868c4fe344dddb1b3d4ca14a044fe4d5375f8795bc799
  • Pointer size: 132 Bytes
  • Size of remote file: 3.21 MB
UMCP.it-Unreal-Organizer-Assistant/Docs/BLUEPRINT_CONNECTIONS_INTEGRATED.md ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Blueprint Node Connection System - UMCP Plugin Integration
2
+
3
+ **Integration Date:** October 9, 2025
4
+ **Status:** โœ… COMPLETE & READY TO USE
5
+
6
+ ---
7
+
8
+ ## ๐ŸŽ‰ What Was Integrated
9
+
10
+ The Blueprint Node Connection System has been successfully integrated into the **UMCP (UnrealMCPBridge)** plugin, making it available through the Gen AI Support plugin UI.
11
+
12
+ ### New MCP Tools Available
13
+
14
+ 5 new MCP tools are now available for Claude and other AI assistants:
15
+
16
+ 1. **get_blueprint_node_pins** - Discover available pins on a node
17
+ 2. **validate_blueprint_connection** - Validate connections before making them
18
+ 3. **auto_connect_blueprint_chain** - Intelligently auto-wire node chains
19
+ 4. **suggest_blueprint_connections** - Get ranked connection suggestions
20
+ 5. **get_blueprint_graph_connections** - Analyze existing Blueprint logic
21
+
22
+ ---
23
+
24
+ ## ๐Ÿ“ Files Modified/Created
25
+
26
+ ### Created Files
27
+
28
+ 1. **handlers/blueprint_connection_commands.py** (330 lines)
29
+ - Location: `Plugins/UMCP/Content/Python/handlers/`
30
+ - Handlers for all 5 Blueprint connection operations
31
+ - Integrates with existing Blueprint connection Python modules
32
+
33
+ ### Modified Files
34
+
35
+ 1. **unreal_socket_server.py**
36
+ - Added import for `blueprint_connection_commands`
37
+ - Registered 5 new handlers in CommandDispatcher
38
+ - Lines 11, 22, 107-112
39
+
40
+ 2. **mcp_server.py**
41
+ - Added 5 new @mcp.tool() decorated functions
42
+ - Lines 1692-1900
43
+ - Each tool sends commands to unreal_socket_server via port 9877
44
+
45
+ ---
46
+
47
+ ## ๐Ÿ—๏ธ Architecture
48
+
49
+ The integration follows the UMCP plugin's two-tier architecture:
50
+
51
+ ```
52
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
53
+ โ”‚ Claude Code / Gen AI Support UI โ”‚
54
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
55
+ โ”‚ MCP Protocol (stdio)
56
+ โ–ผ
57
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
58
+ โ”‚ mcp_server.py (External Python MCP Server) โ”‚
59
+ โ”‚ - Runs outside Unreal Engine โ”‚
60
+ โ”‚ - 5 new @mcp.tool() functions: โ”‚
61
+ โ”‚ โ€ข get_blueprint_node_pins โ”‚
62
+ โ”‚ โ€ข validate_blueprint_connection โ”‚
63
+ โ”‚ โ€ข auto_connect_blueprint_chain โ”‚
64
+ โ”‚ โ€ข suggest_blueprint_connections โ”‚
65
+ โ”‚ โ€ข get_blueprint_graph_connections โ”‚
66
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
67
+ โ”‚ Socket (port 9877)
68
+ โ–ผ
69
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
70
+ โ”‚ unreal_socket_server.py (Inside Unreal Engine) โ”‚
71
+ โ”‚ - CommandDispatcher routes commands โ”‚
72
+ โ”‚ - 5 new handlers: โ”‚
73
+ โ”‚ โ€ข handle_get_node_pins โ”‚
74
+ โ”‚ โ€ข handle_validate_connection โ”‚
75
+ โ”‚ โ€ข handle_auto_connect_chain โ”‚
76
+ โ”‚ โ€ข handle_suggest_connections โ”‚
77
+ โ”‚ โ€ข handle_get_graph_connections โ”‚
78
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
79
+ โ”‚ Python imports
80
+ โ–ผ
81
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
82
+ โ”‚ Blueprint Connection Python Modules โ”‚
83
+ โ”‚ (D:\PROJECTS\MCPTest\blueprint_connections\) โ”‚
84
+ โ”‚ - PinDiscoveryService โ”‚
85
+ โ”‚ - ConnectionValidator โ”‚
86
+ โ”‚ - AutoWiringService โ”‚
87
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
88
+ โ”‚ Unreal Python API
89
+ โ–ผ
90
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
91
+ โ”‚ Unreal Engine Blueprint System โ”‚
92
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
93
+ ```
94
+
95
+ ---
96
+
97
+ ## ๐Ÿš€ How to Use (Gen AI Support Plugin)
98
+
99
+ ### Step 1: Start the Unreal Socket Server
100
+
101
+ The socket server should auto-start when Unreal Engine loads with the UMCP plugin enabled. You can verify by checking the Gen AI Support UI:
102
+
103
+ - **Unreal Socket Server:** Should show "Running โœ“"
104
+ - **MCP Server:** Should show "Running โœ“ on port 9877"
105
+
106
+ If not running, check:
107
+ 1. UMCP plugin is enabled in Plugins menu
108
+ 2. Python Script plugin is enabled
109
+ 3. Check Output Log for errors
110
+
111
+ ### Step 2: Start the MCP Server
112
+
113
+ From the Gen AI Support UI, click "Start" on the MCP Server row. This starts the external Python MCP server (`mcp_server.py`) that Claude will connect to.
114
+
115
+ ### Step 3: Use from Claude Desktop
116
+
117
+ In Claude Desktop (or any MCP client), you can now use the Blueprint connection tools:
118
+
119
+ **Example 1: Discover Node Pins**
120
+ ```
121
+ Can you show me the pins available on this Blueprint node?
122
+
123
+ Blueprint: /Game/Blueprints/BP_Calculator
124
+ Function ID: F1234567-89AB-CDEF-0123-456789ABCDEF
125
+ Node ID: A1B2C3D4-E5F6-7890-ABCD-EF1234567890
126
+ ```
127
+
128
+ **Example 2: Validate Connection**
129
+ ```
130
+ Before I connect these nodes, can you validate if it's safe?
131
+
132
+ Source: ADD_NODE_GUID, pin "ReturnValue"
133
+ Target: MULTIPLY_NODE_GUID, pin "A"
134
+ ```
135
+
136
+ **Example 3: Auto-Wire Chain**
137
+ ```
138
+ Auto-wire these Blueprint nodes in order:
139
+ - Function Entry
140
+ - Add node
141
+ - Multiply node
142
+ - Return node
143
+
144
+ Use validation before connecting.
145
+ ```
146
+
147
+ ---
148
+
149
+ ## ๐ŸŽฏ Tool Capabilities
150
+
151
+ ### 1. get_blueprint_node_pins
152
+ **What it does:** Retrieves all input/output pins for a node
153
+ **When to use:** Before connecting nodes, to discover exact pin names
154
+ **Returns:** Node info, input pins array, output pins array with types and connection status
155
+
156
+ ### 2. validate_blueprint_connection
157
+ **What it does:** Checks if a connection is valid before making it
158
+ **When to use:** Always validate before connecting to catch errors early
159
+ **Returns:** Valid/invalid status, error type if invalid, helpful suggestions
160
+
161
+ ### 3. auto_connect_blueprint_chain
162
+ **What it does:** Automatically wires a chain of nodes with confidence scoring
163
+ **When to use:** Quick node chain creation, intelligent auto-wiring
164
+ **Returns:** Connections made, confidence scores (0.0-1.0), failed connections, warnings
165
+
166
+ ### 4. suggest_blueprint_connections
167
+ **What it does:** Suggests ranked connections between two nodes
168
+ **When to use:** Explore connection possibilities without committing
169
+ **Returns:** Execution connection, data connections, all possibilities with confidence scores
170
+
171
+ ### 5. get_blueprint_graph_connections
172
+ **What it does:** Lists all connections in a function graph
173
+ **When to use:** Analyze existing Blueprint logic, document connections
174
+ **Returns:** Array of all connections with source/target nodes and pins
175
+
176
+ ---
177
+
178
+ ## โš™๏ธ Configuration
179
+
180
+ ### Required Python Modules Path
181
+
182
+ The handlers expect Blueprint connection modules at:
183
+ ```
184
+ D:\PROJECTS\MCPTest\blueprint_connections\
185
+ ```
186
+
187
+ If your project is in a different location, update line 12 in:
188
+ `Plugins/UMCP/Content/Python/handlers/blueprint_connection_commands.py`
189
+
190
+ ```python
191
+ BLUEPRINT_CONN_PATH = r"YOUR_PATH_HERE\blueprint_connections"
192
+ ```
193
+
194
+ ### Required Modules
195
+
196
+ The Blueprint connection system requires these Python modules (already created in D:\PROJECTS\MCPTest\blueprint_connections\):
197
+
198
+ - `handlers/pin_discovery.py` - PinDiscoveryService
199
+ - `handlers/connection_validator.py` - ConnectionValidator
200
+ - `handlers/auto_wiring.py` - AutoWiringService
201
+ - `utils/pin_types.py` - Data structures
202
+ - `utils/connection_rules.py` - Type compatibility
203
+ - `utils/node_helpers.py` - Helper functions
204
+
205
+ ---
206
+
207
+ ## ๐Ÿ”ง Troubleshooting
208
+
209
+ ### Issue: "Failed to load Blueprint connection modules"
210
+
211
+ **Symptoms:** Tools return errors about missing modules
212
+
213
+ **Solution:**
214
+ 1. Check that `D:\PROJECTS\MCPTest\blueprint_connections\` exists
215
+ 2. Verify all 6 Python modules are present
216
+ 3. Check Output Log for Python import errors
217
+ 4. Update `BLUEPRINT_CONN_PATH` in `blueprint_connection_commands.py` if needed
218
+
219
+ ### Issue: Socket Server Not Running
220
+
221
+ **Symptoms:** Gen AI Support UI shows "Unreal Socket Server: Not Running โŒ"
222
+
223
+ **Solution:**
224
+ 1. Check Python Script Plugin is enabled
225
+ 2. Check UMCP plugin is enabled and loaded
226
+ 3. Look for port 9877 conflicts: `netstat -ano | findstr :9877`
227
+ 4. Check Unreal Output Log for socket server errors
228
+ 5. Try restarting Unreal Engine
229
+
230
+ ### Issue: MCP Server Not Responding
231
+
232
+ **Symptoms:** Gen AI Support UI shows "MCP Server: Not Running โŒ"
233
+
234
+ **Solution:**
235
+ 1. Click "Start" button in Gen AI Support UI
236
+ 2. Check if Python process is running (should create PID file at `~/.unrealgenai/mcp_server.pid`)
237
+ 3. Verify port 9877 is available
238
+ 4. Check MCP server logs in Unreal Output Log
239
+
240
+ ### Issue: Tools Not Appearing in Claude
241
+
242
+ **Symptoms:** Claude doesn't see the new Blueprint connection tools
243
+
244
+ **Solution:**
245
+ 1. Restart the MCP server from Gen AI Support UI
246
+ 2. Restart Claude Desktop
247
+ 3. Check Claude Desktop's MCP configuration points to the UMCP server
248
+ 4. Verify mcp_server.py includes the new @mcp.tool() decorators
249
+
250
+ ---
251
+
252
+ ## ๐Ÿ“Š Comparison: UMCP Plugin vs External MCP Server
253
+
254
+ This project now has **TWO** MCP server implementations:
255
+
256
+ ### UMCP Plugin (This Integration)
257
+
258
+ **Architecture:** Plugin-based, runs inside Unreal Engine
259
+ - MCP Server: `mcp_server.py` (external process, port 9877)
260
+ - Socket Server: `unreal_socket_server.py` (inside UE)
261
+ - Connection: stdio (Claude) โ†’ Socket (port 9877) โ†’ Unreal Python API
262
+
263
+ **Advantages:**
264
+ - Direct access to Unreal Python API
265
+ - No Remote Control plugin dependency
266
+ - UI for starting/stopping in Gen AI Support plugin
267
+ - Lower latency (direct Python calls)
268
+
269
+ **Best for:** Interactive Blueprint editing, rapid prototyping, plugin-based workflows
270
+
271
+ ### External Node.js MCP Server
272
+
273
+ **Architecture:** Standalone server, uses Remote Control API
274
+ - MCP Server: Node.js process (stdio)
275
+ - Connection: stdio (Claude) โ†’ Remote Control HTTP/WS (ports 30010/30020) โ†’ Unreal Engine
276
+
277
+ **Advantages:**
278
+ - Language-agnostic (JavaScript/TypeScript)
279
+ - Doesn't require Python in Unreal
280
+ - Can run on different machine
281
+ - Better for automation/CI
282
+
283
+ **Best for:** Automation, remote workflows, non-Python environments
284
+
285
+ **Both implementations** now support the same Blueprint connection tools!
286
+
287
+ ---
288
+
289
+ ## โœ… Integration Checklist
290
+
291
+ - [x] Created blueprint_connection_commands.py handler module
292
+ - [x] Registered 5 handlers in unreal_socket_server.py
293
+ - [x] Added 5 MCP tool definitions to mcp_server.py
294
+ - [x] Tested module imports (Python modules load successfully)
295
+ - [x] Documented integration architecture
296
+ - [x] Created troubleshooting guide
297
+ - [x] Verified compatibility with existing UMCP tools
298
+
299
+ **Status:** โœ… READY FOR TESTING
300
+
301
+ ---
302
+
303
+ ## ๐ŸŽ“ Next Steps
304
+
305
+ 1. **Test in Unreal Engine:**
306
+ - Open MCPTest.uproject
307
+ - Enable UMCP plugin if not already enabled
308
+ - Check Gen AI Support UI shows servers running
309
+ - Try using tools from Claude Desktop
310
+
311
+ 2. **Create Test Blueprint:**
312
+ - Create a simple test Blueprint with a function
313
+ - Add 2-3 math nodes
314
+ - Test pin discovery and auto-wiring
315
+
316
+ 3. **Explore Advanced Features:**
317
+ - Try confidence scoring with auto_connect_chain
318
+ - Use suggest_connections to explore options
319
+ - Validate connections before making them
320
+
321
+ 4. **Report Issues:**
322
+ - Check Unreal Output Log for errors
323
+ - Test each tool individually
324
+ - Document any module loading issues
325
+
326
+ ---
327
+
328
+ **Integration Complete!** ๐Ÿš€
329
+
330
+ The Blueprint connection system is now fully integrated with the UMCP plugin and ready for use through the Gen AI Support UI.