503d938b4a
Fixes #1297. At action:"create", component_properties was accepted and coerced by the C# dispatcher (ManageGameObject.cs) but only ever consumed by the "modify" handler, so it silently did nothing. Meanwhile the shape "create" already reads directly out of each componentsToAdd entry ({typeName, properties}) was rejected before it reached Unity, because the Python schema typed components_to_add as list[str]. - GameObjectComponentHelpers.cs: factor the componentProperties loop + error aggregation out of GameObjectModify.cs into a shared ApplyComponentProperties helper, so both actions apply it identically. - GameObjectCreate.cs: call the new helper after components are added, destroying the partially-created object and returning the error if any property fails to set (matching how component-add failures are handled). - GameObjectModify.cs: switch to the shared helper (behavior-preserving refactor, no functional change on the modify path). - manage_gameobject.py: widen components_to_add to accept {"typeName": ..., "properties": {...}} objects alongside plain strings, matching what GameObjectCreate.cs already reads. - Regenerated website/docs/reference/tools/core/manage_gameobject.md via tools/generate_docs_reference.py for the updated parameter docs. Tested: Server/tests/test_manage_gameobject.py exercises the Python contract end-to-end, including a real fastmcp/pydantic schema validation run of the issue's exact repro payloads (confirmed the pre-fix ValidationError reproduces on the unmodified file, and is gone after). Added TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ ManageGameObjectCreateTests.cs coverage for the C# side, but this was not run against a live Editor.
264 lines
11 KiB
C#
264 lines
11 KiB
C#
#nullable disable
|
|
using System;
|
|
using System.Linq;
|
|
using MCPForUnity.Editor.Helpers;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEditorInternal;
|
|
using UnityEngine;
|
|
|
|
namespace MCPForUnity.Editor.Tools.GameObjects
|
|
{
|
|
internal static class GameObjectModify
|
|
{
|
|
internal static object Handle(JObject @params, JToken targetToken, string searchMethod)
|
|
{
|
|
// When setActive=true is specified, we need to search for inactive objects
|
|
// otherwise we can't find an inactive object to activate it
|
|
JObject findParams = null;
|
|
if (@params["setActive"]?.ToObject<bool?>() == true)
|
|
{
|
|
findParams = new JObject { ["searchInactive"] = true };
|
|
}
|
|
|
|
GameObject targetGo = ManageGameObjectCommon.FindObjectInternal(targetToken, searchMethod, findParams);
|
|
if (targetGo == null)
|
|
{
|
|
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
|
}
|
|
|
|
Undo.RecordObject(targetGo.transform, "Modify GameObject Transform");
|
|
Undo.RecordObject(targetGo, "Modify GameObject Properties");
|
|
|
|
bool modified = false;
|
|
|
|
string name = @params["name"]?.ToString() ?? @params["new_name"]?.ToString() ?? @params["newName"]?.ToString();
|
|
if (!string.IsNullOrEmpty(name) && targetGo.name != name)
|
|
{
|
|
// Check if we're renaming the root object of an open prefab stage
|
|
var prefabStageForRename = PrefabStageUtility.GetCurrentPrefabStage();
|
|
bool isRenamingPrefabRoot = prefabStageForRename != null &&
|
|
prefabStageForRename.prefabContentsRoot == targetGo;
|
|
|
|
if (isRenamingPrefabRoot)
|
|
{
|
|
// Rename the prefab asset file to match the new name (avoids Unity dialog)
|
|
string assetPath = prefabStageForRename.assetPath;
|
|
string directory = System.IO.Path.GetDirectoryName(assetPath);
|
|
string newAssetPath = AssetPathUtility.NormalizeSeparators(System.IO.Path.Combine(directory, name + ".prefab"));
|
|
|
|
// Only rename if the path actually changes
|
|
if (newAssetPath != assetPath)
|
|
{
|
|
// Check for collision using GUID comparison
|
|
string currentGuid = AssetDatabase.AssetPathToGUID(assetPath);
|
|
string existingGuid = AssetDatabase.AssetPathToGUID(newAssetPath);
|
|
|
|
// Collision only if there's a different asset at the new path
|
|
if (!string.IsNullOrEmpty(existingGuid) && existingGuid != currentGuid)
|
|
{
|
|
return new ErrorResponse($"Cannot rename prefab root to '{name}': a prefab already exists at '{newAssetPath}'.");
|
|
}
|
|
|
|
// Rename the asset file
|
|
string renameError = AssetDatabase.RenameAsset(assetPath, name);
|
|
if (!string.IsNullOrEmpty(renameError))
|
|
{
|
|
return new ErrorResponse($"Failed to rename prefab asset: {renameError}");
|
|
}
|
|
|
|
McpLog.Info($"[GameObjectModify] Renamed prefab asset from '{assetPath}' to '{newAssetPath}'");
|
|
}
|
|
}
|
|
|
|
targetGo.name = name;
|
|
modified = true;
|
|
}
|
|
|
|
JToken parentToken = @params["parent"];
|
|
if (parentToken != null)
|
|
{
|
|
GameObject newParentGo = ManageGameObjectCommon.FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
|
if (
|
|
newParentGo == null
|
|
&& !(parentToken.Type == JTokenType.Null
|
|
|| (parentToken.Type == JTokenType.String && string.IsNullOrEmpty(parentToken.ToString())))
|
|
)
|
|
{
|
|
return new ErrorResponse($"New parent ('{parentToken}') not found.");
|
|
}
|
|
if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform))
|
|
{
|
|
return new ErrorResponse($"Cannot parent '{targetGo.name}' to '{newParentGo.name}', as it would create a hierarchy loop.");
|
|
}
|
|
if (targetGo.transform.parent != (newParentGo?.transform))
|
|
{
|
|
targetGo.transform.SetParent(newParentGo?.transform, true);
|
|
modified = true;
|
|
}
|
|
}
|
|
|
|
bool? setActive = @params["setActive"]?.ToObject<bool?>();
|
|
if (setActive.HasValue && targetGo.activeSelf != setActive.Value)
|
|
{
|
|
targetGo.SetActive(setActive.Value);
|
|
modified = true;
|
|
}
|
|
|
|
string tag = @params["tag"]?.ToString();
|
|
if (tag != null && targetGo.tag != tag)
|
|
{
|
|
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
|
|
|
if (tagToSet != "Untagged" && !System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagToSet))
|
|
{
|
|
McpLog.Info($"[ManageGameObject] Tag '{tagToSet}' not found. Creating it.");
|
|
try
|
|
{
|
|
InternalEditorUtility.AddTag(tagToSet);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ErrorResponse($"Failed to create tag '{tagToSet}': {ex.Message}.");
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
targetGo.tag = tagToSet;
|
|
modified = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ErrorResponse($"Failed to set tag to '{tagToSet}': {ex.Message}.");
|
|
}
|
|
}
|
|
|
|
string layerName = @params["layer"]?.ToString();
|
|
if (!string.IsNullOrEmpty(layerName))
|
|
{
|
|
int layerId = LayerMask.NameToLayer(layerName);
|
|
if (layerId == -1)
|
|
{
|
|
return new ErrorResponse($"Invalid layer specified: '{layerName}'. Use a valid layer name.");
|
|
}
|
|
if (layerId != -1 && targetGo.layer != layerId)
|
|
{
|
|
targetGo.layer = layerId;
|
|
modified = true;
|
|
}
|
|
}
|
|
|
|
bool? isStatic = @params["isStatic"]?.ToObject<bool?>();
|
|
if (isStatic.HasValue)
|
|
{
|
|
var desiredFlags = isStatic.Value ? (StaticEditorFlags)~0 : 0;
|
|
var currentFlags = GameObjectUtility.GetStaticEditorFlags(targetGo);
|
|
|
|
if (currentFlags != desiredFlags)
|
|
{
|
|
GameObjectUtility.SetStaticEditorFlags(targetGo, desiredFlags);
|
|
modified = true;
|
|
}
|
|
}
|
|
|
|
Vector3? position = VectorParsing.ParseVector3(@params["position"]);
|
|
Vector3? rotation = VectorParsing.ParseVector3(@params["rotation"]);
|
|
Vector3? scale = VectorParsing.ParseVector3(@params["scale"]);
|
|
|
|
if (position.HasValue && targetGo.transform.localPosition != position.Value)
|
|
{
|
|
targetGo.transform.localPosition = position.Value;
|
|
modified = true;
|
|
}
|
|
if (rotation.HasValue && targetGo.transform.localEulerAngles != rotation.Value)
|
|
{
|
|
targetGo.transform.localEulerAngles = rotation.Value;
|
|
modified = true;
|
|
}
|
|
if (scale.HasValue && targetGo.transform.localScale != scale.Value)
|
|
{
|
|
targetGo.transform.localScale = scale.Value;
|
|
modified = true;
|
|
}
|
|
|
|
if (@params["componentsToRemove"] is JArray componentsToRemoveArray)
|
|
{
|
|
foreach (var compToken in componentsToRemoveArray)
|
|
{
|
|
string typeName = compToken.ToString();
|
|
if (!string.IsNullOrEmpty(typeName))
|
|
{
|
|
var removeResult = GameObjectComponentHelpers.RemoveComponentInternal(targetGo, typeName);
|
|
if (removeResult != null)
|
|
return removeResult;
|
|
modified = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (@params["componentsToAdd"] is JArray componentsToAddArrayModify)
|
|
{
|
|
foreach (var compToken in componentsToAddArrayModify)
|
|
{
|
|
string typeName = null;
|
|
JObject properties = null;
|
|
if (compToken.Type == JTokenType.String)
|
|
typeName = compToken.ToString();
|
|
else if (compToken is JObject compObj)
|
|
{
|
|
typeName = compObj["typeName"]?.ToString();
|
|
properties = compObj["properties"] as JObject;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(typeName))
|
|
{
|
|
var addResult = GameObjectComponentHelpers.AddComponentInternal(targetGo, typeName, properties);
|
|
if (addResult != null)
|
|
return addResult;
|
|
modified = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
var componentPropertiesError = GameObjectComponentHelpers.ApplyComponentProperties(
|
|
targetGo, @params["componentProperties"] as JObject, out bool componentPropertiesModified);
|
|
if (componentPropertiesError != null)
|
|
{
|
|
return componentPropertiesError;
|
|
}
|
|
if (componentPropertiesModified)
|
|
{
|
|
modified = true;
|
|
}
|
|
|
|
if (!modified)
|
|
{
|
|
return new SuccessResponse(
|
|
$"No modifications applied to GameObject '{targetGo.name}'.",
|
|
Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
|
);
|
|
}
|
|
|
|
EditorUtility.SetDirty(targetGo);
|
|
|
|
// Mark the appropriate scene as dirty (handles both regular scenes and prefab stages)
|
|
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
if (prefabStage != null)
|
|
{
|
|
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
|
}
|
|
else
|
|
{
|
|
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
|
}
|
|
|
|
return new SuccessResponse(
|
|
$"GameObject '{targetGo.name}' modified successfully.",
|
|
Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
|
);
|
|
}
|
|
}
|
|
}
|