Files
spacegame/Plugins/UnrealMCPython/Content/Python/UnrealMCPython/level_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

145 lines
6.3 KiB
Python

# Copyright (c) 2025 GenOrca. All Rights Reserved.
import unreal
import json
import traceback
def ue_create_level(level_path: str = None) -> str:
"""Creates a new empty level and saves it at the given content-browser path."""
if not level_path:
return json.dumps({"success": False, "message": "Required parameter 'level_path' is missing."})
try:
success = unreal.EditorLevelLibrary.new_level(level_path)
if not success:
return json.dumps({"success": False, "message": f"EditorLevelLibrary.new_level() returned False for '{level_path}'."})
return json.dumps({"success": True, "level_path": level_path, "message": f"Level created at '{level_path}'."})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_load_level(level_path: str = None) -> str:
"""Opens (loads) an existing level in the editor."""
if not level_path:
return json.dumps({"success": False, "message": "Required parameter 'level_path' is missing."})
try:
success = unreal.EditorLevelLibrary.load_level(level_path)
if not success:
return json.dumps({"success": False, "message": f"EditorLevelLibrary.load_level() returned False for '{level_path}'."})
return json.dumps({"success": True, "level_path": level_path, "message": f"Level '{level_path}' loaded."})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_list_level_actors(class_filter: str = None) -> str:
"""
Lists all actors in the current level.
Optional class_filter is a partial class name (e.g. 'StaticMeshActor', 'BP_Bird').
"""
try:
actors = unreal.EditorLevelLibrary.get_all_level_actors()
result = []
for actor in actors:
class_name = actor.get_class().get_name()
if class_filter and class_filter.lower() not in class_name.lower():
continue
result.append({
"name": actor.get_name(),
"label": actor.get_actor_label(),
"class": class_name,
"location": {
"x": actor.get_actor_location().x,
"y": actor.get_actor_location().y,
"z": actor.get_actor_location().z,
},
"path": actor.get_path_name(),
})
return json.dumps({"success": True, "actors": result, "count": len(result)})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_set_world_settings(gravity: float = None, time_dilation: float = None) -> str:
"""
Modifies WorldSettings of the current level.
gravity sets GlobalGravityZ (negative = downward, e.g. -980).
time_dilation sets the global time dilation multiplier.
"""
try:
world = unreal.EditorLevelLibrary.get_editor_world()
if world is None:
return json.dumps({"success": False, "message": "No editor world available."})
ws = world.get_world_settings()
if ws is None:
return json.dumps({"success": False, "message": "Could not get WorldSettings."})
applied = {}
if gravity is not None:
for prop in ['global_gravity_z', 'GlobalGravityZ']:
try:
# Must also enable custom gravity
for flag in ['global_gravity', 'bGlobalGravity', 'override_world_gravity', 'bOverrideWorldGravity']:
try:
ws.set_editor_property(flag, True)
except Exception:
pass
ws.set_editor_property(prop, gravity)
applied['gravity'] = gravity
break
except Exception:
pass
if time_dilation is not None:
for prop in ['world_to_meters', 'TimeDilation', 'time_dilation']:
try:
ws.set_editor_property(prop, time_dilation)
applied['time_dilation'] = time_dilation
break
except Exception:
pass
if not applied:
return json.dumps({"success": False, "message": "No settings were applied. Check property names for this UE version."})
unreal.EditorLevelLibrary.save_current_level()
return json.dumps({"success": True, "applied": applied, "message": "World settings updated."})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
# --- Current level info / saving ----------------------------------------------
def ue_get_current_level_path() -> str:
"""Returns the path of the currently open editor world/level."""
try:
world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
if not world:
return json.dumps({"success": False, "message": "No editor world is open."})
return json.dumps({"success": True, "level_path": world.get_path_name(),
"level_name": world.get_name()})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_save_current_level() -> str:
"""Saves the currently open level. Returns success=False for an unsaved/untitled level."""
try:
ok = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).save_current_level()
return json.dumps({"success": bool(ok),
"message": "Saved current level." if ok else "save_current_level returned False (untitled or unsaved level)."})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})
def ue_save_all_levels() -> str:
"""Saves all dirty levels."""
try:
ok = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).save_all_dirty_levels()
return json.dumps({"success": bool(ok),
"message": "Saved all dirty levels." if ok else "save_all_dirty_levels returned False (nothing to save)."})
except Exception as e:
return json.dumps({"success": False, "message": str(e), "traceback": traceback.format_exc()})