Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. ย See raw diff
- .gitattributes +9 -0
- README.md +3 -3
- UMCP.it-Unreal-Organizer-Assistant/.gitignore +43 -0
- UMCP.it-Unreal-Organizer-Assistant/Config/DefaultGenerativeAISupport.ini +3 -0
- UMCP.it-Unreal-Organizer-Assistant/Config/FilterPlugin.ini +8 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/ExampleBlueprints/ChatAPIExamples.uasset +3 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/__init__.py +1 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/__init__.py +1 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/auto_wiring.py +438 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/connection_validator.py +351 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/handlers/pin_discovery.py +362 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/utils/connection_rules.py +225 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/utils/node_helpers.py +273 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/blueprint_connections/utils/pin_types.py +152 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/__init__.py +1 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/actor_commands.py +332 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/base_handler.py +23 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/basic_commands.py +487 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/blueprint_commands.py +874 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/blueprint_connection_commands.py +338 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/organization_commands.py +646 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/physics_commands.py +262 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/physics_commands_backup.py +303 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/physics_commands_simple.py +262 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/python_commands.py +278 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/handlers/ui_commands.py +181 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/init_unreal.py +143 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/knowledge_base/how_to_use.md +289 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mcp_server.py +1911 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/__init__.py +27 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/__main__.py +83 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/base.py +261 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/darwin.py +217 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/exception.py +15 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/factory.py +40 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/linux.py +481 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/models.py +23 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/py.typed +0 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/screenshot.py +125 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/tools.py +65 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/mss/windows.py +250 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/socket_server_config.json +3 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/unreal_socket_server.py +304 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/__init__.py +1 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/asset_validation.py +232 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/logging.py +70 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/Python/utils/unreal_conversions.py +54 -0
- UMCP.it-Unreal-Organizer-Assistant/Content/unreal_server_init.py +1357 -0
- UMCP.it-Unreal-Organizer-Assistant/Docs/BHDemoGif.gif +3 -0
- 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** | [
|
| 105 |
-
| **macOS** | [
|
| 106 |
-
| **Linux** | [
|
| 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
|
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.
|