Files
spacegame/Plugins/UnrealMCPython/Content/Python/UnrealMCPython/editor_actions.py
Joshua Deville 496320cc8c Add empty test level and Unreal MCP tooling
- L_TestFlight: empty, zero-gravity level for Phase 0 flight testing,
  set as editor/game default map.
- Enable UnrealMCPython + Python Editor Script Plugin, third-party
  MCP plugin (GenOrca/unreal-mcp v2.2.0) giving direct editor control
  (actors, blueprints, levels, materials, etc.) alongside Epic's
  built-in Unreal MCP plugin.
- Vendor the mcp-server Python source used to bridge to the plugin.
- .mcp.json intentionally gitignored (machine-specific absolute paths).
2026-07-08 14:56:57 -04:00

705 lines
37 KiB
Python

# Copyright (c) 2025 GenOrca. All Rights Reserved.
import unreal
import json
import traceback
from typing import List, Dict, Optional, Any # Modified import
def ue_get_selected_assets() -> str:
"""Gets the set of currently selected assets."""
try:
selected_assets = unreal.EditorUtilityLibrary.get_selected_assets()
serialized_assets = []
for asset in selected_assets:
serialized_assets.append({
"asset_name": asset.get_name(),
"asset_path": asset.get_path_name(),
"asset_class": asset.get_class().get_name(),
})
return json.dumps({"success": True, "selected_assets": serialized_assets})
except Exception as e:
return json.dumps({"success": False, "message": str(e)})
# Helper function to load MaterialInterface assets (can be Material or MaterialInstance)
def _load_material_interface(material_path: str):
# Helper implementation (ensure this is robust or use existing if available)
material = unreal.EditorAssetLibrary.load_asset(material_path)
if not material:
raise FileNotFoundError(f"Material asset not found at path: {material_path}")
if not isinstance(material, unreal.MaterialInterface): # Allows Material or MaterialInstance
raise TypeError(f"Asset at {material_path} is not a MaterialInterface, but {type(material).__name__}")
return material
# Helper function to load StaticMesh assets
def _load_static_mesh(mesh_path: str):
# Helper implementation (ensure this is robust or use existing if available)
mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
if not mesh:
raise FileNotFoundError(f"StaticMesh asset not found at path: {mesh_path}")
if not isinstance(mesh, unreal.StaticMesh):
raise TypeError(f"Asset at {mesh_path} is not a StaticMesh, but {type(mesh).__name__}")
return mesh
# Helper function to get material paths from a mesh component
def _get_component_material_paths(component: unreal.MeshComponent) -> List[str]:
material_paths = []
if component:
for i in range(component.get_num_materials()):
material = component.get_material(i)
if material:
material_paths.append(material.get_path_name())
else:
material_paths.append("") # Represent empty slot
return material_paths
# Helper function to get actors by their paths
def _get_actors_by_paths(actor_paths: List[str]) -> List[unreal.Actor]:
actors = []
all_level_actors = unreal.EditorLevelLibrary.get_all_level_actors()
for path in actor_paths:
actor = next((a for a in all_level_actors if a.get_path_name() == path), None)
if actor:
actors.append(actor)
else:
unreal.log_warning(f"MCP: Actor not found at path: {path}")
return actors
# Helper to create a map of actor paths to their unique material asset paths
def _get_materials_map_for_actors(actors_list: List[unreal.Actor]) -> Dict[str, List[str]]:
materials_map = {}
if not actors_list:
return materials_map
for actor in actors_list:
if not actor: continue
actor_path_name = actor.get_path_name()
actor_material_paths = set()
mesh_components = actor.get_components_by_class(unreal.MeshComponent.static_class())
for comp in mesh_components:
if comp:
for i in range(comp.get_num_materials()):
material = comp.get_material(i)
if material:
actor_material_paths.add(material.get_path_name())
materials_map[actor_path_name] = sorted(list(actor_material_paths))
return materials_map
# Helper to get static mesh path from a static mesh component
def _get_component_mesh_path(component: unreal.StaticMeshComponent) -> str:
if component and hasattr(component, 'static_mesh') and component.static_mesh:
return component.static_mesh.get_path_name()
return ""
# Helper to create a map of actor paths to their unique static mesh asset paths
def _get_meshes_map_for_actors(actors_list: List[unreal.Actor]) -> Dict[str, List[str]]:
meshes_map = {}
if not actors_list:
return meshes_map
for actor in actors_list:
if not actor: continue
actor_path_name = actor.get_path_name()
actor_mesh_paths = set()
sm_components = actor.get_components_by_class(unreal.StaticMeshComponent.static_class())
for comp in sm_components:
mesh_path = _get_component_mesh_path(comp)
if mesh_path:
actor_mesh_paths.add(mesh_path)
meshes_map[actor_path_name] = sorted(list(actor_mesh_paths))
return meshes_map
# Helper to create a map of actor paths to their unique skeletal mesh asset paths
def _get_skeletal_meshes_map_for_actors(actors_list: List[unreal.Actor]) -> Dict[str, List[str]]:
skeletal_meshes_map = {}
if not actors_list:
return skeletal_meshes_map
for actor in actors_list:
if not actor:
continue
actor_path_name = actor.get_path_name()
actor_skel_mesh_paths = set()
skel_components = actor.get_components_by_class(unreal.SkeletalMeshComponent.static_class())
for comp in skel_components:
if hasattr(comp, 'skeletal_mesh') and comp.skeletal_mesh:
actor_skel_mesh_paths.add(comp.skeletal_mesh.get_path_name())
skeletal_meshes_map[actor_path_name] = sorted(list(actor_skel_mesh_paths))
return skeletal_meshes_map
# Base helper for replacing meshes
def _replace_meshes_on_actors_components_base(
actors: List[unreal.Actor],
mesh_to_be_replaced_path: str, # Can be empty/None to replace any mesh
new_mesh_path: str
) -> Dict[str, Any]:
"""Base logic for replacing static meshes on components of given actors."""
mesh_to_replace = None
# Only load if a specific mesh is targeted for replacement
if mesh_to_be_replaced_path and mesh_to_be_replaced_path.lower() not in ["", "none", "any"]:
mesh_to_replace = _load_static_mesh(mesh_to_be_replaced_path) # Uses existing helper
# If _load_static_mesh raises, it will be caught by the calling ue_ function's try-except
new_mesh = _load_static_mesh(new_mesh_path) # Uses existing helper
# If _load_static_mesh raises, it will be caught by the calling ue_ function's try-except
if mesh_to_replace and new_mesh and mesh_to_replace.get_path_name() == new_mesh.get_path_name():
return {"success": True, "message": "Mesh to be replaced is the same as the new mesh. No changes made.", "changed_actors_count": 0, "changed_components_count": 0}
changed_actors_count = 0
changed_components_count = 0
details = {"actors_affected": []}
with unreal.ScopedEditorTransaction("Replace Static Meshes on Components") as trans:
for actor in actors:
if not actor: continue
actor_path = actor.get_path_name()
actor_changed_this_iteration = False
actor_details = {"actor_path": actor_path, "components_changed": []}
static_mesh_components = actor.get_components_by_class(unreal.StaticMeshComponent.static_class())
for component in static_mesh_components:
if not component or component.get_owner() != actor:
continue
component_path = component.get_path_name()
current_mesh = component.static_mesh
should_replace = False
# Case 1: Replace any mesh (mesh_to_be_replaced_path is empty/None/Any)
if not mesh_to_be_replaced_path or mesh_to_be_replaced_path.lower() in ["", "none", "any"]:
if current_mesh != new_mesh : # Avoid replacing with itself if no specific mesh to replace
should_replace = True
# Case 2: Replace a specific mesh
elif mesh_to_replace and current_mesh and current_mesh.get_path_name() == mesh_to_replace.get_path_name():
if current_mesh.get_path_name() != new_mesh.get_path_name(): # Don't replace if it's already the new mesh
should_replace = True
# Case 3: Component has no mesh, and we want to set one (mesh_to_be_replaced_path is empty/None/Any)
elif (not mesh_to_be_replaced_path or mesh_to_be_replaced_path.lower() in ["", "none", "any"]) and not current_mesh:
should_replace = True
if should_replace:
if component.set_static_mesh(new_mesh):
changed_components_count += 1
actor_changed_this_iteration = True
actor_details["components_changed"].append({"component_path": component_path, "previous_mesh": current_mesh.get_path_name() if current_mesh else None})
else:
unreal.log_warning(f"MCP: Failed to set static mesh on component {component_path} for actor {actor_path}")
if actor_changed_this_iteration:
changed_actors_count += 1
details["actors_affected"].append(actor_details)
if changed_actors_count > 0:
unreal.EditorLevelLibrary.refresh_all_level_editors()
return {
"success": True,
"message": f"Mesh replacement processed. Actors processed: {len(actors)}.",
"changed_actors_count": changed_actors_count,
"changed_components_count": changed_components_count,
"details": details
}
def ue_replace_mtl_on_selected(material_to_be_replaced_path: str, new_material_path: str) -> str:
try:
material_to_replace = _load_material_interface(material_to_be_replaced_path)
new_material = _load_material_interface(new_material_path)
selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
if not selected_actors:
return json.dumps({"success": False, "message": "No actors selected."})
mesh_components = []
for actor in selected_actors:
components = actor.get_components_by_class(unreal.MeshComponent.static_class())
mesh_components.extend(c for c in components if c)
if not mesh_components:
return json.dumps({"success": False, "message": "No mesh components found on selected actors."})
initial_materials_map = {}
for comp in mesh_components:
initial_materials_map[comp.get_path_name()] = _get_component_material_paths(comp)
unreal.EditorLevelLibrary.replace_mesh_components_materials(
mesh_components,
material_to_replace,
new_material
)
num_components_affected = 0
affected_component_paths = []
for comp in mesh_components:
current_materials = _get_component_material_paths(comp)
original_materials = initial_materials_map.get(comp.get_path_name(), [])
component_changed_here = False
for slot_idx, original_mat_path in enumerate(original_materials):
if original_mat_path == material_to_replace.get_path_name():
if slot_idx < len(current_materials) and current_materials[slot_idx] == new_material.get_path_name():
component_changed_here = True
break
if component_changed_here:
num_components_affected += 1
affected_component_paths.append(comp.get_path_name())
if num_components_affected > 0:
return json.dumps({
"success": True,
"message": f"Successfully replaced material '{material_to_be_replaced_path}' with '{new_material_path}' on {num_components_affected} mesh component(s) across {len(selected_actors)} selected actor(s).",
"affected_actors_count": len(selected_actors),
"affected_components_count": num_components_affected,
"affected_component_paths": affected_component_paths
})
else:
return json.dumps({
"success": False,
"message": f"Failed to replace material. Target material '{material_to_be_replaced_path}' not found or not replaced on any mesh components of selected actors."
})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_replace_mtl_on_specified(actor_paths: List[str], material_to_be_replaced_path: str, new_material_path: str) -> str:
try:
material_to_replace = _load_material_interface(material_to_be_replaced_path)
new_material = _load_material_interface(new_material_path)
actors_to_process = _get_actors_by_paths(actor_paths)
if not actors_to_process:
return json.dumps({"success": False, "message": "No valid actors found from the provided paths."})
all_mesh_components_in_actors = []
for actor in actors_to_process:
components = actor.get_components_by_class(unreal.MeshComponent.static_class())
all_mesh_components_in_actors.extend(c for c in components if c)
if not all_mesh_components_in_actors:
return json.dumps({"success": False, "message": "No mesh components found on the specified actors."})
initial_materials_map = {}
for comp in all_mesh_components_in_actors:
initial_materials_map[comp.get_path_name()] = _get_component_material_paths(comp)
unreal.EditorLevelLibrary.replace_mesh_components_materials_on_actors(
actors_to_process,
material_to_replace,
new_material
)
num_components_affected = 0
affected_component_paths = []
for comp in all_mesh_components_in_actors:
current_materials = _get_component_material_paths(comp)
original_materials = initial_materials_map.get(comp.get_path_name(), [])
component_changed_here = False
for slot_idx, original_mat_path in enumerate(original_materials):
if original_mat_path == material_to_replace.get_path_name():
if slot_idx < len(current_materials) and current_materials[slot_idx] == new_material.get_path_name():
component_changed_here = True
break
if component_changed_here:
num_components_affected += 1
affected_component_paths.append(comp.get_path_name())
if num_components_affected > 0:
return json.dumps({
"success": True,
"message": f"Successfully replaced material '{material_to_be_replaced_path}' with '{new_material_path}' on {num_components_affected} mesh component(s) across {len(actors_to_process)} specified actor(s).",
"affected_actors_count": len(actors_to_process),
"affected_components_count": num_components_affected,
"affected_component_paths": affected_component_paths
})
else:
return json.dumps({
"success": False,
"message": f"Failed to replace material. Target material '{material_to_be_replaced_path}' not found or not replaced on any mesh components of specified actors."
})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_replace_mesh_on_selected(mesh_to_be_replaced_path: str, new_mesh_path: str) -> str:
"""Replaces static meshes on components of selected actors using Unreal's batch API if available."""
try:
# Check if mesh_to_be_replaced_path exists (if specified)
if mesh_to_be_replaced_path and mesh_to_be_replaced_path.lower() not in ["", "none", "any"]:
try:
_ = _load_static_mesh(mesh_to_be_replaced_path)
except FileNotFoundError:
return json.dumps({
"success": False,
"message": f"The mesh_to_be_replaced_path '{mesh_to_be_replaced_path}' does not exist as a StaticMesh asset.",
"error_type": "MeshToReplaceNotFound"
})
selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
if not selected_actors:
return json.dumps({"success": True, "message": "No actors selected.", "changed_actors_count": 0, "changed_components_count": 0})
mesh_to_replace = None
if mesh_to_be_replaced_path and mesh_to_be_replaced_path.lower() not in ["", "none", "any"]:
mesh_to_replace = _load_static_mesh(mesh_to_be_replaced_path)
new_mesh = _load_static_mesh(new_mesh_path)
# Gather all static mesh components from selected actors
all_mesh_components = []
for actor in selected_actors:
comps = actor.get_components_by_class(unreal.StaticMeshComponent.static_class())
all_mesh_components.extend(c for c in comps if c)
if not all_mesh_components:
return json.dumps({"success": False, "message": "No static mesh components found on selected actors."})
# Save initial mesh paths for change detection
initial_meshes_map = {comp.get_path_name(): comp.static_mesh.get_path_name() if comp.static_mesh else "" for comp in all_mesh_components}
# Use Unreal's batch API if available
if hasattr(unreal.EditorLevelLibrary, "replace_mesh_components_meshes_on_actors"):
unreal.EditorLevelLibrary.replace_mesh_components_meshes_on_actors(
selected_actors,
mesh_to_replace,
new_mesh
)
else:
# Fallback to manual replacement if batch API is not available
for comp in all_mesh_components:
current_mesh = comp.static_mesh
should_replace = False
if not mesh_to_replace:
if current_mesh != new_mesh:
should_replace = True
elif current_mesh and current_mesh.get_path_name() == mesh_to_replace.get_path_name():
if current_mesh.get_path_name() != new_mesh.get_path_name():
should_replace = True
elif not current_mesh and not mesh_to_replace:
should_replace = True
if should_replace:
comp.set_static_mesh(new_mesh)
# Detect changes
changed_components_count = 0
affected_component_paths = []
for comp in all_mesh_components:
before = initial_meshes_map.get(comp.get_path_name(), "")
after = comp.static_mesh.get_path_name() if comp.static_mesh else ""
if mesh_to_replace:
if before == mesh_to_replace.get_path_name() and after == new_mesh.get_path_name():
changed_components_count += 1
affected_component_paths.append(comp.get_path_name())
else:
if before != after and after == new_mesh.get_path_name():
changed_components_count += 1
affected_component_paths.append(comp.get_path_name())
if changed_components_count > 0:
return json.dumps({
"success": True,
"message": f"Successfully replaced mesh on {changed_components_count} static mesh component(s) across {len(selected_actors)} selected actor(s).",
"affected_actors_count": len(selected_actors),
"affected_components_count": changed_components_count,
"affected_component_paths": affected_component_paths
})
else:
return json.dumps({
"success": False,
"message": f"Failed to replace mesh. Target mesh '{mesh_to_be_replaced_path}' not found or not replaced on any static mesh components of selected actors."
})
except FileNotFoundError as e:
unreal.log_error(f"MCP: Asset loading error in ue_replace_mesh_on_selected: {e}")
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
except TypeError as e:
unreal.log_error(f"MCP: Asset type error in ue_replace_mesh_on_selected: {e}")
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
except Exception as e:
unreal.log_error(f"MCP: Error in ue_replace_mesh_on_selected: {e}\n{traceback.format_exc()}")
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_replace_mesh_on_specified(actor_paths: List[str], mesh_to_be_replaced_path: str, new_mesh_path: str) -> str:
"""Replaces static meshes on components of specified actors using Unreal's batch API if available."""
try:
# Check if mesh_to_be_replaced_path exists (if specified)
if mesh_to_be_replaced_path and mesh_to_be_replaced_path.lower() not in ["", "none", "any"]:
try:
_ = _load_static_mesh(mesh_to_be_replaced_path)
except FileNotFoundError:
return json.dumps({
"success": False,
"message": f"The mesh_to_be_replaced_path '{mesh_to_be_replaced_path}' does not exist as a StaticMesh asset.",
"error_type": "MeshToReplaceNotFound"
})
actors_to_process = _get_actors_by_paths(actor_paths)
if not actors_to_process:
return json.dumps({"success": False, "message": "No valid actors found from the provided paths."})
mesh_to_replace = None
if mesh_to_be_replaced_path and mesh_to_be_replaced_path.lower() not in ["", "none", "any"]:
mesh_to_replace = _load_static_mesh(mesh_to_be_replaced_path)
new_mesh = _load_static_mesh(new_mesh_path)
# Gather all static mesh components from specified actors
all_mesh_components = []
for actor in actors_to_process:
comps = actor.get_components_by_class(unreal.StaticMeshComponent.static_class())
all_mesh_components.extend(c for c in comps if c)
if not all_mesh_components:
# Get the actual actor types and names for better diagnosis
actor_info = [{"name": actor.get_name(), "class": actor.get_class().get_name()} for actor in actors_to_process]
actors_materials_info = _get_materials_map_for_actors(actors_to_process)
actors_meshes_info = _get_meshes_map_for_actors(actors_to_process)
actors_skel_meshes_info = _get_skeletal_meshes_map_for_actors(actors_to_process)
return json.dumps({
"success": False,
"message": "No static mesh components found on specified actors.",
"specified_actors_info": actor_info,
"current_materials": actors_materials_info,
"current_meshes": actors_meshes_info,
"current_skeletal_meshes": actors_skel_meshes_info
})
# Save initial mesh paths for change detection
initial_meshes_map = {comp.get_path_name(): comp.static_mesh.get_path_name() if comp.static_mesh else "" for comp in all_mesh_components}
# Use Unreal's batch API if available
if hasattr(unreal.EditorLevelLibrary, "replace_mesh_components_meshes_on_actors"):
unreal.EditorLevelLibrary.replace_mesh_components_meshes_on_actors(
actors_to_process,
mesh_to_replace,
new_mesh
)
else:
# Fallback to manual replacement if batch API is not available
for comp in all_mesh_components:
current_mesh = comp.static_mesh
should_replace = False
if not mesh_to_replace:
if current_mesh != new_mesh:
should_replace = True
elif current_mesh and current_mesh.get_path_name() == mesh_to_replace.get_path_name():
if current_mesh.get_path_name() != new_mesh.get_path_name():
should_replace = True
elif not current_mesh and not mesh_to_replace:
should_replace = True
if should_replace:
comp.set_static_mesh(new_mesh)
# Detect changes
changed_components_count = 0
affected_component_paths = []
unchanged_components_info = []
for comp in all_mesh_components:
before = initial_meshes_map.get(comp.get_path_name(), "")
after = comp.static_mesh.get_path_name() if comp.static_mesh else ""
owner_actor = comp.get_owner()
owner_name = owner_actor.get_name() if owner_actor else "Unknown"
if mesh_to_replace:
if before == mesh_to_replace.get_path_name() and after == new_mesh.get_path_name():
changed_components_count += 1
affected_component_paths.append(comp.get_path_name())
else:
is_candidate = before == mesh_to_replace.get_path_name()
unchanged_components_info.append({
"component_path": comp.get_path_name(),
"component_name": comp.get_name(),
"actor_name": owner_name,
"current_mesh": before,
"is_candidate": is_candidate,
"reason": (
"Component is a candidate for replacement (matches mesh_to_be_replaced_path) but was not changed"
if is_candidate else
("Current mesh doesn't match the mesh to be replaced" if before != mesh_to_replace.get_path_name() else "Failed to set new mesh")
)
})
else:
if before != after and after == new_mesh.get_path_name():
changed_components_count += 1
affected_component_paths.append(comp.get_path_name())
else:
unchanged_components_info.append({
"component_path": comp.get_path_name(),
"component_name": comp.get_name(),
"actor_name": owner_name,
"current_mesh": before,
"is_candidate": False,
"reason": "Component already has the target mesh" if before == new_mesh.get_path_name() else "Failed to set new mesh"
})
unchanged_components_info = unchanged_components_info if 'unchanged_components_info' in locals() else []
if changed_components_count > 0:
return json.dumps({
"success": True,
"message": f"Successfully replaced mesh on {changed_components_count} static mesh component(s) across {len(actors_to_process)} specified actor(s).",
"affected_actors_count": len(actors_to_process),
"affected_components_count": changed_components_count,
"affected_component_paths": affected_component_paths,
"unchanged_components": unchanged_components_info
})
else:
actors_materials_info = _get_materials_map_for_actors(actors_to_process)
actors_meshes_info = _get_meshes_map_for_actors(actors_to_process)
actors_skel_meshes_info = _get_skeletal_meshes_map_for_actors(actors_to_process)
unchanged_components_info = unchanged_components_info if 'unchanged_components_info' in locals() else []
return json.dumps({
"success": False,
"message": f"Failed to replace mesh. Target mesh '{mesh_to_be_replaced_path}' not found or not replaced on any static mesh components of specified actors.",
"current_materials": actors_materials_info,
"current_meshes": actors_meshes_info,
"current_skeletal_meshes": actors_skel_meshes_info,
"unchanged_components": unchanged_components_info
})
except FileNotFoundError as e:
unreal.log_error(f"MCP: Asset loading error in ue_replace_mesh_on_specified: {e}")
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
except TypeError as e:
unreal.log_error(f"MCP: Asset type error in ue_replace_mesh_on_specified: {e}")
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
except Exception as e:
unreal.log_error(f"MCP: Error in ue_replace_mesh_on_specified: {e}\n{traceback.format_exc()}")
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_replace_selected_with_bp(blueprint_asset_path: str) -> str:
"""Replaces the currently selected actors with new actors spawned from a specified Blueprint asset path using Unreal's official API."""
import unreal
import json
import traceback
try:
selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors()
if not selected_actors:
return json.dumps({"success": False, "message": "No actors selected."})
# Check if the blueprint asset exists
blueprint = unreal.EditorAssetLibrary.load_asset(blueprint_asset_path)
if not blueprint:
return json.dumps({"success": False, "message": f"Blueprint asset not found at path: {blueprint_asset_path}"})
# Use the official API
unreal.EditorLevelLibrary.replace_selected_actors(blueprint_asset_path)
return json.dumps({
"success": True,
"message": f"Replaced {len(selected_actors)} actors with Blueprint '{blueprint_asset_path}' using official API.",
"replaced_actors_count": len(selected_actors)
})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
# --- Actor merging / proxy geometry --------------------------------------------
def _actors_by_labels(labels):
sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
by_label = {a.get_actor_label(): a for a in sub.get_all_level_actors()}
found, missing = [], []
for l in labels:
(found.append(by_label[l]) if l in by_label else missing.append(l))
return found, missing
def _merged_mesh_path(actor):
try:
mesh = actor.static_mesh_component.static_mesh
return mesh.get_path_name().split(".")[0] if mesh else None
except Exception:
return None
def ue_merge_actors(actor_labels: list = None, base_package_name: str = None,
destroy_source_actors: bool = False) -> str:
"""Merges static mesh actors into ONE new static mesh asset + actor (geometry is baked together)."""
if not actor_labels or base_package_name is None:
return json.dumps({"success": False, "message": "Required: actor_labels (non-empty), base_package_name."})
try:
actors, missing = _actors_by_labels(actor_labels)
if missing:
return json.dumps({"success": False, "message": f"Actors not found: {missing}"})
sms = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem)
opts = unreal.EditorScriptingMergeStaticMeshActorsOptions()
opts.base_package_name = base_package_name
opts.destroy_source_actors = bool(destroy_source_actors)
merged = sms.merge_static_mesh_actors(actors, opts)
if not merged:
return json.dumps({"success": False, "message": "merge_static_mesh_actors returned None (are these StaticMesh actors?)."})
return json.dumps({"success": True, "merged_actor": merged.get_actor_label(),
"mesh_asset": _merged_mesh_path(merged),
"source_destroyed": bool(destroy_source_actors)})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_join_actors(actor_labels: list = None, new_actor_label: str = "") -> str:
"""Joins static mesh actors into one actor with multiple components (no new mesh asset is baked)."""
if not actor_labels:
return json.dumps({"success": False, "message": "Required parameter 'actor_labels' is missing."})
try:
actors, missing = _actors_by_labels(actor_labels)
if missing:
return json.dumps({"success": False, "message": f"Actors not found: {missing}"})
sms = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem)
opts = unreal.EditorScriptingJoinStaticMeshActorsOptions()
if new_actor_label:
opts.new_actor_label = new_actor_label
joined = sms.join_static_mesh_actors(actors, opts)
if not joined:
return json.dumps({"success": False, "message": "join_static_mesh_actors returned None."})
return json.dumps({"success": True, "joined_actor": joined.get_actor_label()})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_create_proxy_actor(actor_labels: list = None, base_package_name: str = None,
screen_size: int = 300, destroy_source_actors: bool = False) -> str:
"""Bakes static mesh actors into ONE simplified proxy mesh (Proxy Geometry tool) and spawns it."""
if not actor_labels or base_package_name is None:
return json.dumps({"success": False, "message": "Required: actor_labels (non-empty), base_package_name."})
try:
actors, missing = _actors_by_labels(actor_labels)
if missing:
return json.dumps({"success": False, "message": f"Actors not found: {missing}"})
sms = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem)
opts = unreal.EditorScriptingCreateProxyMeshActorOptions()
opts.base_package_name = base_package_name
opts.destroy_source_actors = bool(destroy_source_actors)
settings = opts.mesh_proxy_settings
settings.set_editor_property("screen_size", int(screen_size))
opts.mesh_proxy_settings = settings
proxy = sms.create_proxy_mesh_actor(actors, opts)
if not proxy:
return json.dumps({"success": False, "message": "create_proxy_mesh_actor returned None (ProxyLOD plugin enabled? StaticMesh actors?)."})
return json.dumps({"success": True, "proxy_actor": proxy.get_actor_label(),
"mesh_asset": _merged_mesh_path(proxy),
"source_destroyed": bool(destroy_source_actors)})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
# ─── Asset editor control ─────────────────────────────────────────────────────
def ue_open_editor_for_asset(asset_path: str = None) -> str:
"""Opens the asset-specific editor (Blueprint, Material, etc.) for an asset."""
if asset_path is None:
return json.dumps({"success": False, "message": "Required parameter 'asset_path' is missing."})
try:
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
if not asset:
return json.dumps({"success": False, "message": f"Asset not found: {asset_path}"})
unreal.get_editor_subsystem(unreal.AssetEditorSubsystem).open_editor_for_assets([asset])
return json.dumps({"success": True, "asset_path": asset_path})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_get_open_assets() -> str:
"""Lists assets that currently have an editor open."""
try:
assets = unreal.MCPythonHelper.get_all_edited_assets()
open_assets = [{"asset_name": a.get_name(), "asset_path": a.get_path_name(),
"asset_class": a.get_class().get_name()} for a in assets if a]
return json.dumps({"success": True, "count": len(open_assets), "open_assets": open_assets})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_close_asset_editor(asset_path: str = None) -> str:
"""Closes any open editor for the given asset."""
if asset_path is None:
return json.dumps({"success": False, "message": "Required parameter 'asset_path' is missing."})
try:
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
if not asset:
return json.dumps({"success": False, "message": f"Asset not found: {asset_path}"})
unreal.get_editor_subsystem(unreal.AssetEditorSubsystem).close_all_editors_for_asset(asset)
return json.dumps({"success": True, "asset_path": asset_path})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})