升级XR插件版本
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Component resolver that delegates to UnityTypeResolver.
|
||||
/// Kept for backwards compatibility.
|
||||
/// </summary>
|
||||
internal static class ComponentResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolve a Component/MonoBehaviour type by short or fully-qualified name.
|
||||
/// Delegates to UnityTypeResolver.TryResolve with Component constraint.
|
||||
/// </summary>
|
||||
public static bool TryResolve(string nameOrFullName, out Type type, out string error)
|
||||
{
|
||||
return UnityTypeResolver.TryResolve(nameOrFullName, out type, out error, typeof(Component));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all accessible property and field names from a component type.
|
||||
/// </summary>
|
||||
public static List<string> GetAllComponentProperties(Type componentType)
|
||||
{
|
||||
if (componentType == null) return new List<string>();
|
||||
|
||||
var properties = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead && p.CanWrite)
|
||||
.Select(p => p.Name);
|
||||
|
||||
var fields = componentType.GetFields(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(f => !f.IsInitOnly && !f.IsLiteral)
|
||||
.Select(f => f.Name);
|
||||
|
||||
// Also include SerializeField private fields (common in Unity)
|
||||
var serializeFields = componentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.Where(f => f.GetCustomAttribute<SerializeField>() != null)
|
||||
.Select(f => f.Name);
|
||||
|
||||
return properties.Concat(fields).Concat(serializeFields).Distinct().OrderBy(x => x).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suggests the most likely property matches for a user's input using fuzzy matching.
|
||||
/// Uses Levenshtein distance, substring matching, and common naming pattern heuristics.
|
||||
/// </summary>
|
||||
public static List<string> GetFuzzyPropertySuggestions(string userInput, List<string> availableProperties)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput) || !availableProperties.Any())
|
||||
return new List<string>();
|
||||
|
||||
var cacheKey = $"{userInput.ToLowerInvariant()}:{string.Join(",", availableProperties)}";
|
||||
if (PropertySuggestionCache.TryGetValue(cacheKey, out var cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
var suggestions = GetRuleBasedSuggestions(userInput, availableProperties);
|
||||
PropertySuggestionCache[cacheKey] = suggestions;
|
||||
return suggestions;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[Property Matching] Error getting suggestions for '{userInput}': {ex.Message}");
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, List<string>> PropertySuggestionCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Rule-based suggestions that mimic AI behavior for property matching.
|
||||
/// This provides immediate value while we could add real AI integration later.
|
||||
/// </summary>
|
||||
private static List<string> GetRuleBasedSuggestions(string userInput, List<string> availableProperties)
|
||||
{
|
||||
var suggestions = new List<string>();
|
||||
var cleanedInput = userInput.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", "");
|
||||
|
||||
foreach (var property in availableProperties)
|
||||
{
|
||||
var cleanedProperty = property.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", "");
|
||||
|
||||
if (cleanedProperty == cleanedInput)
|
||||
{
|
||||
suggestions.Add(property);
|
||||
continue;
|
||||
}
|
||||
|
||||
var inputWords = userInput.ToLowerInvariant().Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (inputWords.All(word => cleanedProperty.Contains(word.ToLowerInvariant())))
|
||||
{
|
||||
suggestions.Add(property);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LevenshteinDistance(cleanedInput, cleanedProperty) <= Math.Max(2, cleanedInput.Length / 4))
|
||||
{
|
||||
suggestions.Add(property);
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions.OrderBy(s => LevenshteinDistance(cleanedInput, s.ToLowerInvariant().Replace(" ", "")))
|
||||
.Take(3)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Levenshtein distance between two strings for similarity matching.
|
||||
/// </summary>
|
||||
private static int LevenshteinDistance(string s1, string s2)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s1)) return s2?.Length ?? 0;
|
||||
if (string.IsNullOrEmpty(s2)) return s1.Length;
|
||||
|
||||
var matrix = new int[s1.Length + 1, s2.Length + 1];
|
||||
|
||||
for (int i = 0; i <= s1.Length; i++) matrix[i, 0] = i;
|
||||
for (int j = 0; j <= s2.Length; j++) matrix[0, j] = j;
|
||||
|
||||
for (int i = 1; i <= s1.Length; i++)
|
||||
{
|
||||
for (int j = 1; j <= s2.Length; j++)
|
||||
{
|
||||
int cost = (s2[j - 1] == s1[i - 1]) ? 0 : 1;
|
||||
matrix[i, j] = Math.Min(Math.Min(
|
||||
matrix[i - 1, j] + 1,
|
||||
matrix[i, j - 1] + 1),
|
||||
matrix[i - 1, j - 1] + cost);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[s1.Length, s2.Length];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5e5a46bdebc040c68897fa4b5e689c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,410 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectComponentHelpers
|
||||
{
|
||||
internal static object AddComponentInternal(GameObject targetGo, string typeName, JObject properties)
|
||||
{
|
||||
Type componentType = FindType(typeName);
|
||||
if (componentType == null)
|
||||
{
|
||||
return new ErrorResponse($"Component type '{typeName}' not found or is not a valid Component.");
|
||||
}
|
||||
if (!typeof(Component).IsAssignableFrom(componentType))
|
||||
{
|
||||
return new ErrorResponse($"Type '{typeName}' is not a Component.");
|
||||
}
|
||||
|
||||
if (componentType == typeof(Transform))
|
||||
{
|
||||
return new ErrorResponse("Cannot add another Transform component.");
|
||||
}
|
||||
|
||||
bool isAdding2DPhysics = typeof(Rigidbody2D).IsAssignableFrom(componentType) || typeof(Collider2D).IsAssignableFrom(componentType);
|
||||
bool isAdding3DPhysics = typeof(Rigidbody).IsAssignableFrom(componentType) || typeof(Collider).IsAssignableFrom(componentType);
|
||||
|
||||
if (isAdding2DPhysics)
|
||||
{
|
||||
if (targetGo.GetComponent<Rigidbody>() != null || targetGo.GetComponent<Collider>() != null)
|
||||
{
|
||||
return new ErrorResponse($"Cannot add 2D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 3D Rigidbody or Collider.");
|
||||
}
|
||||
}
|
||||
else if (isAdding3DPhysics)
|
||||
{
|
||||
if (targetGo.GetComponent<Rigidbody2D>() != null || targetGo.GetComponent<Collider2D>() != null)
|
||||
{
|
||||
return new ErrorResponse($"Cannot add 3D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 2D Rigidbody or Collider.");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Component newComponent = Undo.AddComponent(targetGo, componentType);
|
||||
if (newComponent == null)
|
||||
{
|
||||
return new ErrorResponse($"Failed to add component '{typeName}' to '{targetGo.name}'. It might be disallowed (e.g., adding script twice)."
|
||||
);
|
||||
}
|
||||
|
||||
if (newComponent is Light light)
|
||||
{
|
||||
light.type = LightType.Directional;
|
||||
}
|
||||
|
||||
if (properties != null)
|
||||
{
|
||||
var setResult = SetComponentPropertiesInternal(targetGo, typeName, properties, newComponent);
|
||||
if (setResult != null)
|
||||
{
|
||||
Undo.DestroyObjectImmediate(newComponent);
|
||||
return setResult;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error adding component '{typeName}' to '{targetGo.name}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static object RemoveComponentInternal(GameObject targetGo, string typeName)
|
||||
{
|
||||
if (targetGo == null)
|
||||
{
|
||||
return new ErrorResponse("Target GameObject is null.");
|
||||
}
|
||||
|
||||
Type componentType = FindType(typeName);
|
||||
if (componentType == null)
|
||||
{
|
||||
return new ErrorResponse($"Component type '{typeName}' not found for removal.");
|
||||
}
|
||||
|
||||
if (componentType == typeof(Transform))
|
||||
{
|
||||
return new ErrorResponse("Cannot remove the Transform component.");
|
||||
}
|
||||
|
||||
Component componentToRemove = targetGo.GetComponent(componentType);
|
||||
if (componentToRemove == null)
|
||||
{
|
||||
return new ErrorResponse($"Component '{typeName}' not found on '{targetGo.name}' to remove.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Undo.DestroyObjectImmediate(componentToRemove);
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error removing component '{typeName}' from '{targetGo.name}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static object SetComponentPropertiesInternal(GameObject targetGo, string componentTypeName, JObject properties, Component targetComponentInstance = null)
|
||||
{
|
||||
Component targetComponent = targetComponentInstance;
|
||||
if (targetComponent == null)
|
||||
{
|
||||
if (ComponentResolver.TryResolve(componentTypeName, out var compType, out var compError))
|
||||
{
|
||||
targetComponent = targetGo.GetComponent(compType);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetComponent = targetGo.GetComponent(componentTypeName);
|
||||
}
|
||||
}
|
||||
if (targetComponent == null)
|
||||
{
|
||||
return new ErrorResponse($"Component '{componentTypeName}' not found on '{targetGo.name}' to set properties.");
|
||||
}
|
||||
|
||||
Undo.RecordObject(targetComponent, "Set Component Properties");
|
||||
|
||||
var failures = new List<string>();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string propName = prop.Name;
|
||||
JToken propValue = prop.Value;
|
||||
|
||||
try
|
||||
{
|
||||
bool setResult = SetProperty(targetComponent, propName, propValue);
|
||||
if (!setResult)
|
||||
{
|
||||
var availableProperties = ComponentResolver.GetAllComponentProperties(targetComponent.GetType());
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions(propName, availableProperties);
|
||||
var msg = suggestions.Any()
|
||||
? $"Property '{propName}' not found. Did you mean: {string.Join(", ", suggestions)}? Available: [{string.Join(", ", availableProperties)}]"
|
||||
: $"Property '{propName}' not found. Available: [{string.Join(", ", availableProperties)}]";
|
||||
McpLog.Warn($"[ManageGameObject] {msg}");
|
||||
failures.Add(msg);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ManageGameObject] Error setting property '{propName}' on '{componentTypeName}': {e.Message}");
|
||||
failures.Add($"Error setting '{propName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(targetComponent);
|
||||
return failures.Count == 0
|
||||
? null
|
||||
: new ErrorResponse($"One or more properties failed on '{componentTypeName}'.", new { errors = failures });
|
||||
}
|
||||
|
||||
private static JsonSerializer InputSerializer => UnityJsonSerializer.Instance;
|
||||
|
||||
private static bool SetProperty(object target, string memberName, JToken value)
|
||||
{
|
||||
Type type = target.GetType();
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
|
||||
string normalizedName = Helpers.ParamCoercion.NormalizePropertyName(memberName);
|
||||
var inputSerializer = InputSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
if (memberName.Contains('.') || memberName.Contains('['))
|
||||
{
|
||||
return SetNestedProperty(target, memberName, value, inputSerializer);
|
||||
}
|
||||
|
||||
PropertyInfo propInfo = type.GetProperty(memberName, flags) ?? type.GetProperty(normalizedName, flags);
|
||||
if (propInfo != null && propInfo.CanWrite)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
propInfo.SetValue(target, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FieldInfo fieldInfo = type.GetField(memberName, flags) ?? type.GetField(normalizedName, flags);
|
||||
if (fieldInfo != null)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
fieldInfo.SetValue(target, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var npField = type.GetField(memberName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase)
|
||||
?? type.GetField(normalizedName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (npField != null && npField.GetCustomAttribute<SerializeField>() != null)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, npField.FieldType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
npField.SetValue(target, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"[SetProperty] Failed to set '{memberName}' on {type.Name}: {ex.Message}\nToken: {value.ToString(Formatting.None)}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
string[] pathParts = SplitPropertyPath(path);
|
||||
if (pathParts.Length == 0)
|
||||
return false;
|
||||
|
||||
object currentObject = target;
|
||||
Type currentType = currentObject.GetType();
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
|
||||
for (int i = 0; i < pathParts.Length - 1; i++)
|
||||
{
|
||||
string part = pathParts[i];
|
||||
bool isArray = false;
|
||||
int arrayIndex = -1;
|
||||
|
||||
if (part.Contains("["))
|
||||
{
|
||||
int startBracket = part.IndexOf('[');
|
||||
int endBracket = part.IndexOf(']');
|
||||
if (startBracket > 0 && endBracket > startBracket)
|
||||
{
|
||||
string indexStr = part.Substring(startBracket + 1, endBracket - startBracket - 1);
|
||||
if (int.TryParse(indexStr, out arrayIndex))
|
||||
{
|
||||
isArray = true;
|
||||
part = part.Substring(0, startBracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PropertyInfo propInfo = currentType.GetProperty(part, flags);
|
||||
FieldInfo fieldInfo = null;
|
||||
if (propInfo == null)
|
||||
{
|
||||
fieldInfo = currentType.GetField(part, flags);
|
||||
if (fieldInfo == null)
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Could not find property or field '{part}' on type '{currentType.Name}'");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
currentObject = propInfo != null ? propInfo.GetValue(currentObject) : fieldInfo.GetValue(currentObject);
|
||||
if (currentObject == null)
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Property '{part}' is null, cannot access nested properties.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isArray)
|
||||
{
|
||||
if (currentObject is Material[])
|
||||
{
|
||||
var materials = currentObject as Material[];
|
||||
if (arrayIndex < 0 || arrayIndex >= materials.Length)
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Material index {arrayIndex} out of range (0-{materials.Length - 1})");
|
||||
return false;
|
||||
}
|
||||
currentObject = materials[arrayIndex];
|
||||
}
|
||||
else if (currentObject is System.Collections.IList)
|
||||
{
|
||||
var list = currentObject as System.Collections.IList;
|
||||
if (arrayIndex < 0 || arrayIndex >= list.Count)
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Index {arrayIndex} out of range (0-{list.Count - 1})");
|
||||
return false;
|
||||
}
|
||||
currentObject = list[arrayIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[SetNestedProperty] Property '{part}' is not an array or list, cannot access by index.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
currentType = currentObject.GetType();
|
||||
}
|
||||
|
||||
string finalPart = pathParts[pathParts.Length - 1];
|
||||
|
||||
if (currentObject is Material material && finalPart.StartsWith("_"))
|
||||
{
|
||||
return MaterialOps.TrySetShaderProperty(material, finalPart, value, inputSerializer);
|
||||
}
|
||||
|
||||
PropertyInfo finalPropInfo = currentType.GetProperty(finalPart, flags);
|
||||
if (finalPropInfo != null && finalPropInfo.CanWrite)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, finalPropInfo.PropertyType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
finalPropInfo.SetValue(currentObject, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags);
|
||||
if (finalFieldInfo != null)
|
||||
{
|
||||
object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer);
|
||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||
{
|
||||
finalFieldInfo.SetValue(currentObject, convertedValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}\nToken: {value.ToString(Formatting.None)}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string[] SplitPropertyPath(string path)
|
||||
{
|
||||
List<string> parts = new List<string>();
|
||||
int startIndex = 0;
|
||||
bool inBrackets = false;
|
||||
|
||||
for (int i = 0; i < path.Length; i++)
|
||||
{
|
||||
char c = path[i];
|
||||
|
||||
if (c == '[')
|
||||
{
|
||||
inBrackets = true;
|
||||
}
|
||||
else if (c == ']')
|
||||
{
|
||||
inBrackets = false;
|
||||
}
|
||||
else if (c == '.' && !inBrackets)
|
||||
{
|
||||
parts.Add(path.Substring(startIndex, i - startIndex));
|
||||
startIndex = i + 1;
|
||||
}
|
||||
}
|
||||
if (startIndex < path.Length)
|
||||
{
|
||||
parts.Add(path.Substring(startIndex));
|
||||
}
|
||||
return parts.ToArray();
|
||||
}
|
||||
|
||||
private static object ConvertJTokenToType(JToken token, Type targetType, JsonSerializer inputSerializer)
|
||||
{
|
||||
return PropertyConversion.ConvertToType(token, targetType);
|
||||
}
|
||||
|
||||
private static Type FindType(string typeName)
|
||||
{
|
||||
if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error))
|
||||
{
|
||||
return resolvedType;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
McpLog.Warn($"[FindType] {error}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b580af06e2d3a4788960f3f779edac54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,338 @@
|
||||
#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 GameObjectCreate
|
||||
{
|
||||
internal static object Handle(JObject @params)
|
||||
{
|
||||
string name = @params["name"]?.ToString();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return new ErrorResponse("'name' parameter is required for 'create' action.");
|
||||
}
|
||||
|
||||
// Get prefab creation parameters
|
||||
bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject<bool>() ?? false;
|
||||
string prefabPath = @params["prefabPath"]?.ToString();
|
||||
string tag = @params["tag"]?.ToString();
|
||||
string primitiveType = @params["primitiveType"]?.ToString();
|
||||
GameObject newGo = null;
|
||||
|
||||
// --- Try Instantiating Prefab First ---
|
||||
string originalPrefabPath = prefabPath;
|
||||
if (!saveAsPrefab && !string.IsNullOrEmpty(prefabPath))
|
||||
{
|
||||
string extension = System.IO.Path.GetExtension(prefabPath);
|
||||
|
||||
if (!prefabPath.Contains("/") && (string.IsNullOrEmpty(extension) || extension.Equals(".prefab", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
string prefabNameOnly = prefabPath;
|
||||
McpLog.Info($"[ManageGameObject.Create] Searching for prefab named: '{prefabNameOnly}'");
|
||||
string[] guids = AssetDatabase.FindAssets($"t:Prefab {prefabNameOnly}");
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
return new ErrorResponse($"Prefab named '{prefabNameOnly}' not found anywhere in the project.");
|
||||
}
|
||||
else if (guids.Length > 1)
|
||||
{
|
||||
string foundPaths = string.Join(", ", guids.Select(g => AssetDatabase.GUIDToAssetPath(g)));
|
||||
return new ErrorResponse($"Multiple prefabs found matching name '{prefabNameOnly}': {foundPaths}. Please provide a more specific path.");
|
||||
}
|
||||
else
|
||||
{
|
||||
prefabPath = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
McpLog.Info($"[ManageGameObject.Create] Found unique prefab at path: '{prefabPath}'");
|
||||
}
|
||||
}
|
||||
else if (prefabPath.Contains("/") && string.IsNullOrEmpty(extension))
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' has no extension. Assuming it's a prefab and appending .prefab.");
|
||||
prefabPath += ".prefab";
|
||||
}
|
||||
else if (!prefabPath.Contains("/") && !string.IsNullOrEmpty(extension) && !extension.Equals(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string fileName = prefabPath;
|
||||
string fileNameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(fileName);
|
||||
McpLog.Info($"[ManageGameObject.Create] Searching for asset file named: '{fileName}'");
|
||||
|
||||
string[] guids = AssetDatabase.FindAssets(fileNameWithoutExtension);
|
||||
var matches = guids
|
||||
.Select(g => AssetDatabase.GUIDToAssetPath(g))
|
||||
.Where(p => p.EndsWith("/" + fileName, StringComparison.OrdinalIgnoreCase) || p.Equals(fileName, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
if (matches.Length == 0)
|
||||
{
|
||||
return new ErrorResponse($"Asset file '{fileName}' not found anywhere in the project.");
|
||||
}
|
||||
else if (matches.Length > 1)
|
||||
{
|
||||
string foundPaths = string.Join(", ", matches);
|
||||
return new ErrorResponse($"Multiple assets found matching file name '{fileName}': {foundPaths}. Please provide a more specific path.");
|
||||
}
|
||||
else
|
||||
{
|
||||
prefabPath = matches[0];
|
||||
McpLog.Info($"[ManageGameObject.Create] Found unique asset at path: '{prefabPath}'");
|
||||
}
|
||||
}
|
||||
|
||||
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
if (prefabAsset != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject;
|
||||
|
||||
if (newGo == null)
|
||||
{
|
||||
McpLog.Error($"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject.");
|
||||
return new ErrorResponse($"Failed to instantiate prefab at '{prefabPath}'.");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
newGo.name = name;
|
||||
}
|
||||
Undo.RegisterCreatedObjectUndo(newGo, $"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'");
|
||||
McpLog.Info($"[ManageGameObject.Create] Instantiated prefab '{prefabAsset.name}' from path '{prefabPath}' as '{newGo.name}'.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Error instantiating prefab '{prefabPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"Asset not found or not a GameObject at path: '{prefabPath}'.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fallback: Create Primitive or Empty GameObject ---
|
||||
bool createdNewObject = false;
|
||||
if (newGo == null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(primitiveType))
|
||||
{
|
||||
try
|
||||
{
|
||||
PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true);
|
||||
newGo = GameObject.CreatePrimitive(type);
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
newGo.name = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse("'name' parameter is required when creating a primitive.");
|
||||
}
|
||||
createdNewObject = true;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return new ErrorResponse($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse($"Failed to create primitive '{primitiveType}': {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return new ErrorResponse("'name' parameter is required for 'create' action when not instantiating a prefab or creating a primitive.");
|
||||
}
|
||||
newGo = new GameObject(name);
|
||||
createdNewObject = true;
|
||||
}
|
||||
|
||||
if (createdNewObject)
|
||||
{
|
||||
Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'");
|
||||
}
|
||||
}
|
||||
|
||||
if (newGo == null)
|
||||
{
|
||||
return new ErrorResponse("Failed to create or instantiate the GameObject.");
|
||||
}
|
||||
|
||||
Undo.RecordObject(newGo.transform, "Set GameObject Transform");
|
||||
Undo.RecordObject(newGo, "Set GameObject Properties");
|
||||
|
||||
// Set Parent
|
||||
JToken parentToken = @params["parent"];
|
||||
if (parentToken != null)
|
||||
{
|
||||
GameObject parentGo = ManageGameObjectCommon.FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
||||
if (parentGo == null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Parent specified ('{parentToken}') but not found.");
|
||||
}
|
||||
newGo.transform.SetParent(parentGo.transform, true);
|
||||
}
|
||||
|
||||
// Set Transform
|
||||
Vector3? position = VectorParsing.ParseVector3(@params["position"]);
|
||||
Vector3? rotation = VectorParsing.ParseVector3(@params["rotation"]);
|
||||
Vector3? scale = VectorParsing.ParseVector3(@params["scale"]);
|
||||
|
||||
if (position.HasValue) newGo.transform.localPosition = position.Value;
|
||||
if (rotation.HasValue) newGo.transform.localEulerAngles = rotation.Value;
|
||||
if (scale.HasValue) newGo.transform.localScale = scale.Value;
|
||||
|
||||
// Set Tag
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
{
|
||||
if (tag != "Untagged" && !System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tag))
|
||||
{
|
||||
McpLog.Info($"[ManageGameObject.Create] Tag '{tag}' not found. Creating it.");
|
||||
try
|
||||
{
|
||||
InternalEditorUtility.AddTag(tag);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Failed to create tag '{tag}': {ex.Message}.");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
newGo.tag = tag;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Failed to set tag to '{tag}' during creation: {ex.Message}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Set Layer
|
||||
string layerName = @params["layer"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(layerName))
|
||||
{
|
||||
int layerId = LayerMask.NameToLayer(layerName);
|
||||
if (layerId != -1)
|
||||
{
|
||||
newGo.layer = layerId;
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Create] Layer '{layerName}' not found. Using default layer.");
|
||||
}
|
||||
}
|
||||
|
||||
// Add Components
|
||||
if (@params["componentsToAdd"] is JArray componentsToAddArray)
|
||||
{
|
||||
foreach (var compToken in componentsToAddArray)
|
||||
{
|
||||
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(newGo, typeName, properties);
|
||||
if (addResult != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return addResult;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject] Invalid component format in componentsToAdd: {compToken}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true
|
||||
GameObject finalInstance = newGo;
|
||||
if (createdNewObject && saveAsPrefab)
|
||||
{
|
||||
string finalPrefabPath = prefabPath;
|
||||
if (string.IsNullOrEmpty(finalPrefabPath))
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse("'prefabPath' is required when 'saveAsPrefab' is true and creating a new object.");
|
||||
}
|
||||
if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
McpLog.Info($"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'");
|
||||
finalPrefabPath += ".prefab";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath);
|
||||
if (!string.IsNullOrEmpty(directoryPath) && !System.IO.Directory.Exists(directoryPath))
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(directoryPath);
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
McpLog.Info($"[ManageGameObject.Create] Created directory for prefab: {directoryPath}");
|
||||
}
|
||||
|
||||
finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, finalPrefabPath, InteractionMode.UserAction);
|
||||
|
||||
if (finalInstance == null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions.");
|
||||
}
|
||||
McpLog.Info($"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return new ErrorResponse($"Error saving prefab '{finalPrefabPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Selection.activeGameObject = finalInstance;
|
||||
|
||||
string messagePrefabPath =
|
||||
finalInstance == null
|
||||
? originalPrefabPath
|
||||
: AssetDatabase.GetAssetPath(PrefabUtility.GetCorrespondingObjectFromSource(finalInstance) ?? (UnityEngine.Object)finalInstance);
|
||||
|
||||
string successMessage;
|
||||
if (!createdNewObject && !string.IsNullOrEmpty(messagePrefabPath))
|
||||
{
|
||||
successMessage = $"Prefab '{messagePrefabPath}' instantiated successfully as '{finalInstance.name}'.";
|
||||
}
|
||||
else if (createdNewObject && saveAsPrefab && !string.IsNullOrEmpty(messagePrefabPath))
|
||||
{
|
||||
successMessage = $"GameObject '{finalInstance.name}' created and saved as prefab to '{messagePrefabPath}'.";
|
||||
}
|
||||
else
|
||||
{
|
||||
successMessage = $"GameObject '{finalInstance.name}' created successfully in scene.";
|
||||
}
|
||||
|
||||
return new SuccessResponse(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0931774a07e4b4626b4261dd8d0974c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
#nullable disable
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectDelete
|
||||
{
|
||||
internal static object Handle(JToken targetToken, string searchMethod)
|
||||
{
|
||||
List<GameObject> targets = ManageGameObjectCommon.FindObjectsInternal(targetToken, searchMethod, true);
|
||||
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject(s) ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
List<object> deletedObjects = new List<object>();
|
||||
foreach (var targetGo in targets)
|
||||
{
|
||||
if (targetGo != null)
|
||||
{
|
||||
string goName = targetGo.name;
|
||||
int goId = targetGo.GetInstanceID();
|
||||
// Note: Undo.DestroyObjectImmediate doesn't work reliably in test context,
|
||||
// so we use Object.DestroyImmediate. This means delete isn't undoable.
|
||||
// TODO: Investigate Undo.DestroyObjectImmediate behavior in Unity 2022+
|
||||
Object.DestroyImmediate(targetGo);
|
||||
deletedObjects.Add(new { name = goName, instanceID = goId });
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedObjects.Count > 0)
|
||||
{
|
||||
string message =
|
||||
targets.Count == 1
|
||||
? $"GameObject '{((dynamic)deletedObjects[0]).name}' deleted successfully."
|
||||
: $"{deletedObjects.Count} GameObjects deleted successfully.";
|
||||
return new SuccessResponse(message, deletedObjects);
|
||||
}
|
||||
|
||||
return new ErrorResponse("Failed to delete target GameObject(s).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 505a482aaf60b415abd794737a630b10
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,86 @@
|
||||
#nullable disable
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectDuplicate
|
||||
{
|
||||
internal static object Handle(JObject @params, JToken targetToken, string searchMethod)
|
||||
{
|
||||
GameObject sourceGo = ManageGameObjectCommon.FindObjectInternal(targetToken, searchMethod);
|
||||
if (sourceGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
string newName = @params["new_name"]?.ToString();
|
||||
Vector3? position = VectorParsing.ParseVector3(@params["position"]);
|
||||
Vector3? offset = VectorParsing.ParseVector3(@params["offset"]);
|
||||
JToken parentToken = @params["parent"];
|
||||
|
||||
GameObject duplicatedGo = UnityEngine.Object.Instantiate(sourceGo);
|
||||
Undo.RegisterCreatedObjectUndo(duplicatedGo, $"Duplicate {sourceGo.name}");
|
||||
|
||||
if (!string.IsNullOrEmpty(newName))
|
||||
{
|
||||
duplicatedGo.name = newName;
|
||||
}
|
||||
else
|
||||
{
|
||||
duplicatedGo.name = sourceGo.name.Replace("(Clone)", "").Trim() + "_Copy";
|
||||
}
|
||||
|
||||
if (position.HasValue)
|
||||
{
|
||||
duplicatedGo.transform.position = position.Value;
|
||||
}
|
||||
else if (offset.HasValue)
|
||||
{
|
||||
duplicatedGo.transform.position = sourceGo.transform.position + offset.Value;
|
||||
}
|
||||
|
||||
if (parentToken != null)
|
||||
{
|
||||
if (parentToken.Type == JTokenType.Null || (parentToken.Type == JTokenType.String && string.IsNullOrEmpty(parentToken.ToString())))
|
||||
{
|
||||
duplicatedGo.transform.SetParent(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject newParent = ManageGameObjectCommon.FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
||||
if (newParent != null)
|
||||
{
|
||||
duplicatedGo.transform.SetParent(newParent.transform, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Duplicate] Parent '{parentToken}' not found. Object will remain at root level.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
duplicatedGo.transform.SetParent(sourceGo.transform.parent, true);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(duplicatedGo);
|
||||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||||
|
||||
Selection.activeGameObject = duplicatedGo;
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Duplicated '{sourceGo.name}' as '{duplicatedGo.name}'.",
|
||||
new
|
||||
{
|
||||
originalName = sourceGo.name,
|
||||
originalId = sourceGo.GetInstanceID(),
|
||||
duplicatedObject = Helpers.GameObjectSerializer.GetGameObjectData(duplicatedGo)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 698728d56425a47af92a45377031a48b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
#nullable disable
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectHandlers
|
||||
{
|
||||
internal static object Create(JObject @params) => GameObjectCreate.Handle(@params);
|
||||
|
||||
internal static object Modify(JObject @params, JToken targetToken, string searchMethod)
|
||||
=> GameObjectModify.Handle(@params, targetToken, searchMethod);
|
||||
|
||||
internal static object Delete(JToken targetToken, string searchMethod)
|
||||
=> GameObjectDelete.Handle(targetToken, searchMethod);
|
||||
|
||||
internal static object Duplicate(JObject @params, JToken targetToken, string searchMethod)
|
||||
=> GameObjectDuplicate.Handle(@params, targetToken, searchMethod);
|
||||
|
||||
internal static object MoveRelative(JObject @params, JToken targetToken, string searchMethod)
|
||||
=> GameObjectMoveRelative.Handle(@params, targetToken, searchMethod);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3cf2313460d44a09b258d2ee04c5ef0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,297 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 componentErrors = new List<object>();
|
||||
if (@params["componentProperties"] is JObject componentPropertiesObj)
|
||||
{
|
||||
foreach (var prop in componentPropertiesObj.Properties())
|
||||
{
|
||||
string compName = prop.Name;
|
||||
JObject propertiesToSet = prop.Value as JObject;
|
||||
if (propertiesToSet != null)
|
||||
{
|
||||
var setResult = GameObjectComponentHelpers.SetComponentPropertiesInternal(targetGo, compName, propertiesToSet);
|
||||
if (setResult != null)
|
||||
{
|
||||
componentErrors.Add(setResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (componentErrors.Count > 0)
|
||||
{
|
||||
var aggregatedErrors = new List<string>();
|
||||
foreach (var errorObj in componentErrors)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dataProp = errorObj?.GetType().GetProperty("data");
|
||||
var dataVal = dataProp?.GetValue(errorObj);
|
||||
if (dataVal != null)
|
||||
{
|
||||
var errorsProp = dataVal.GetType().GetProperty("errors");
|
||||
var errorsEnum = errorsProp?.GetValue(dataVal) as System.Collections.IEnumerable;
|
||||
if (errorsEnum != null)
|
||||
{
|
||||
foreach (var item in errorsEnum)
|
||||
{
|
||||
var s = item?.ToString();
|
||||
if (!string.IsNullOrEmpty(s)) aggregatedErrors.Add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[GameObjectModify] Error aggregating component errors: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return new ErrorResponse(
|
||||
$"One or more component property operations failed on '{targetGo.name}'.",
|
||||
new { componentErrors = componentErrors, errors = aggregatedErrors }
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec5e33513bd094257a26ef6f75ea4574
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,119 @@
|
||||
#nullable disable
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class GameObjectMoveRelative
|
||||
{
|
||||
internal static object Handle(JObject @params, JToken targetToken, string searchMethod)
|
||||
{
|
||||
GameObject targetGo = ManageGameObjectCommon.FindObjectInternal(targetToken, searchMethod);
|
||||
if (targetGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'.");
|
||||
}
|
||||
|
||||
JToken referenceToken = @params["reference_object"];
|
||||
if (referenceToken == null)
|
||||
{
|
||||
return new ErrorResponse("'reference_object' parameter is required for 'move_relative' action.");
|
||||
}
|
||||
|
||||
GameObject referenceGo = ManageGameObjectCommon.FindObjectInternal(referenceToken, "by_id_or_name_or_path");
|
||||
if (referenceGo == null)
|
||||
{
|
||||
return new ErrorResponse($"Reference object '{referenceToken}' not found.");
|
||||
}
|
||||
|
||||
string direction = @params["direction"]?.ToString()?.ToLower();
|
||||
float distance = @params["distance"]?.ToObject<float>() ?? 1f;
|
||||
Vector3? customOffset = VectorParsing.ParseVector3(@params["offset"]);
|
||||
bool useWorldSpace = @params["world_space"]?.ToObject<bool>() ?? true;
|
||||
|
||||
Undo.RecordObject(targetGo.transform, $"Move {targetGo.name} relative to {referenceGo.name}");
|
||||
|
||||
Vector3 newPosition;
|
||||
|
||||
if (customOffset.HasValue)
|
||||
{
|
||||
if (useWorldSpace)
|
||||
{
|
||||
newPosition = referenceGo.transform.position + customOffset.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
newPosition = referenceGo.transform.TransformPoint(customOffset.Value);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(direction))
|
||||
{
|
||||
Vector3 directionVector = GetDirectionVector(direction, referenceGo.transform, useWorldSpace);
|
||||
newPosition = referenceGo.transform.position + directionVector * distance;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse("Either 'direction' or 'offset' parameter is required for 'move_relative' action.");
|
||||
}
|
||||
|
||||
targetGo.transform.position = newPosition;
|
||||
|
||||
EditorUtility.SetDirty(targetGo);
|
||||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Moved '{targetGo.name}' relative to '{referenceGo.name}'.",
|
||||
new
|
||||
{
|
||||
movedObject = targetGo.name,
|
||||
referenceObject = referenceGo.name,
|
||||
newPosition = new[] { targetGo.transform.position.x, targetGo.transform.position.y, targetGo.transform.position.z },
|
||||
direction = direction,
|
||||
distance = distance,
|
||||
gameObject = Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static Vector3 GetDirectionVector(string direction, Transform referenceTransform, bool useWorldSpace)
|
||||
{
|
||||
if (useWorldSpace)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case "right": return Vector3.right;
|
||||
case "left": return Vector3.left;
|
||||
case "up": return Vector3.up;
|
||||
case "down": return Vector3.down;
|
||||
case "forward":
|
||||
case "front": return Vector3.forward;
|
||||
case "back":
|
||||
case "backward":
|
||||
case "behind": return Vector3.back;
|
||||
default:
|
||||
McpLog.Warn($"[ManageGameObject.MoveRelative] Unknown direction '{direction}', defaulting to forward.");
|
||||
return Vector3.forward;
|
||||
}
|
||||
}
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case "right": return referenceTransform.right;
|
||||
case "left": return -referenceTransform.right;
|
||||
case "up": return referenceTransform.up;
|
||||
case "down": return -referenceTransform.up;
|
||||
case "forward":
|
||||
case "front": return referenceTransform.forward;
|
||||
case "back":
|
||||
case "backward":
|
||||
case "behind": return -referenceTransform.forward;
|
||||
default:
|
||||
McpLog.Warn($"[ManageGameObject.MoveRelative] Unknown direction '{direction}', defaulting to forward.");
|
||||
return referenceTransform.forward;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b19997a165de45c2af3ada79a6d3f08
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,115 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers; // For Response class
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles GameObject manipulation within the current scene (CRUD, find, components).
|
||||
/// </summary>
|
||||
[McpForUnityTool("manage_gameobject", AutoRegister = false)]
|
||||
public static class ManageGameObject
|
||||
{
|
||||
// --- Main Handler ---
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
{
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
}
|
||||
|
||||
string action = @params["action"]?.ToString().ToLower();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return new ErrorResponse("Action parameter is required.");
|
||||
}
|
||||
|
||||
// Parameters used by various actions
|
||||
JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID)
|
||||
string name = @params["name"]?.ToString();
|
||||
|
||||
// --- Usability Improvement: Alias 'name' to 'target' for modification actions ---
|
||||
// If 'target' is missing but 'name' is provided, and we aren't creating a new object,
|
||||
// assume the user meant "find object by name".
|
||||
if (targetToken == null && !string.IsNullOrEmpty(name) && action != "create")
|
||||
{
|
||||
targetToken = name;
|
||||
// We don't update @params["target"] because we use targetToken locally mostly,
|
||||
// but some downstream methods might parse @params directly. Let's update @params too for safety.
|
||||
@params["target"] = name;
|
||||
}
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
string searchMethod = @params["searchMethod"]?.ToString().ToLower();
|
||||
string tag = @params["tag"]?.ToString();
|
||||
string layer = @params["layer"]?.ToString();
|
||||
JToken parentToken = @params["parent"];
|
||||
|
||||
// Coerce string JSON to JObject for 'componentProperties' if provided as a JSON string
|
||||
var componentPropsToken = @params["componentProperties"];
|
||||
if (componentPropsToken != null && componentPropsToken.Type == JTokenType.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsed = JObject.Parse(componentPropsToken.ToString());
|
||||
@params["componentProperties"] = parsed;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject] Could not parse 'componentProperties' JSON string: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Prefab Asset Check ---
|
||||
// Prefab assets require different tools. Only 'create' (instantiation) is valid here.
|
||||
string targetPath =
|
||||
targetToken?.Type == JTokenType.String ? targetToken.ToString() : null;
|
||||
if (
|
||||
!string.IsNullOrEmpty(targetPath)
|
||||
&& targetPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)
|
||||
&& action != "create" // Allow prefab instantiation
|
||||
)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Target '{targetPath}' is a prefab asset. " +
|
||||
$"Use 'manage_asset' with action='modify' for prefab asset modifications, " +
|
||||
$"or 'manage_prefabs' with action='open_stage' to edit the prefab in isolation mode."
|
||||
);
|
||||
}
|
||||
// --- End Prefab Asset Check ---
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
// --- Primary lifecycle actions (kept in manage_gameobject) ---
|
||||
case "create":
|
||||
return GameObjectCreate.Handle(@params);
|
||||
case "modify":
|
||||
return GameObjectModify.Handle(@params, targetToken, searchMethod);
|
||||
case "delete":
|
||||
return GameObjectDelete.Handle(targetToken, searchMethod);
|
||||
case "duplicate":
|
||||
return GameObjectDuplicate.Handle(@params, targetToken, searchMethod);
|
||||
case "move_relative":
|
||||
return GameObjectMoveRelative.Handle(@params, targetToken, searchMethod);
|
||||
|
||||
default:
|
||||
return new ErrorResponse($"Unknown action: '{action}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ManageGameObject] Action '{action}' failed: {e}");
|
||||
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7641d7388f0f6634b9d83d34de87b2ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,238 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.GameObjects
|
||||
{
|
||||
internal static class ManageGameObjectCommon
|
||||
{
|
||||
internal static GameObject FindObjectInternal(JToken targetToken, string searchMethod, JObject findParams = null)
|
||||
{
|
||||
bool findAll = findParams?["findAll"]?.ToObject<bool>() ?? false;
|
||||
|
||||
if (
|
||||
targetToken?.Type == JTokenType.Integer
|
||||
|| (searchMethod == "by_id" && int.TryParse(targetToken?.ToString(), out _))
|
||||
)
|
||||
{
|
||||
findAll = false;
|
||||
}
|
||||
|
||||
List<GameObject> results = FindObjectsInternal(targetToken, searchMethod, findAll, findParams);
|
||||
return results.Count > 0 ? results[0] : null;
|
||||
}
|
||||
|
||||
internal static List<GameObject> FindObjectsInternal(
|
||||
JToken targetToken,
|
||||
string searchMethod,
|
||||
bool findAll,
|
||||
JObject findParams = null
|
||||
)
|
||||
{
|
||||
List<GameObject> results = new List<GameObject>();
|
||||
string searchTerm = findParams?["searchTerm"]?.ToString() ?? targetToken?.ToString();
|
||||
bool searchInChildren = findParams?["searchInChildren"]?.ToObject<bool>() ?? false;
|
||||
bool searchInactive = findParams?["searchInactive"]?.ToObject<bool>() ?? false;
|
||||
|
||||
if (string.IsNullOrEmpty(searchMethod))
|
||||
{
|
||||
if (targetToken?.Type == JTokenType.Integer)
|
||||
searchMethod = "by_id";
|
||||
else if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Contains('/'))
|
||||
searchMethod = "by_path";
|
||||
else
|
||||
searchMethod = "by_name";
|
||||
}
|
||||
|
||||
GameObject rootSearchObject = null;
|
||||
if (searchInChildren && targetToken != null)
|
||||
{
|
||||
rootSearchObject = FindObjectInternal(targetToken, "by_id_or_name_or_path");
|
||||
if (rootSearchObject == null)
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Find] Root object '{targetToken}' for child search not found.");
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
switch (searchMethod)
|
||||
{
|
||||
case "by_id":
|
||||
if (int.TryParse(searchTerm, out int instanceId))
|
||||
{
|
||||
var allObjects = GetAllSceneObjects(searchInactive);
|
||||
GameObject obj = allObjects.FirstOrDefault(go => go.GetInstanceID() == instanceId);
|
||||
if (obj != null)
|
||||
results.Add(obj);
|
||||
}
|
||||
break;
|
||||
|
||||
case "by_name":
|
||||
var searchPoolName = rootSearchObject
|
||||
? rootSearchObject
|
||||
.GetComponentsInChildren<Transform>(searchInactive)
|
||||
.Select(t => t.gameObject)
|
||||
: GetAllSceneObjects(searchInactive);
|
||||
results.AddRange(searchPoolName.Where(go => go.name == searchTerm));
|
||||
break;
|
||||
|
||||
case "by_path":
|
||||
if (rootSearchObject != null)
|
||||
{
|
||||
Transform foundTransform = rootSearchObject.transform.Find(searchTerm);
|
||||
if (foundTransform != null)
|
||||
results.Add(foundTransform.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
if (prefabStage != null || searchInactive)
|
||||
{
|
||||
// In Prefab Stage, GameObject.Find() doesn't work, need to search manually
|
||||
var allObjects = GetAllSceneObjects(searchInactive);
|
||||
foreach (var go in allObjects)
|
||||
{
|
||||
if (GameObjectLookup.MatchesPath(go, searchTerm))
|
||||
{
|
||||
results.Add(go);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var found = GameObject.Find(searchTerm);
|
||||
if (found != null)
|
||||
results.Add(found);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "by_tag":
|
||||
var searchPoolTag = rootSearchObject
|
||||
? rootSearchObject
|
||||
.GetComponentsInChildren<Transform>(searchInactive)
|
||||
.Select(t => t.gameObject)
|
||||
: GetAllSceneObjects(searchInactive);
|
||||
results.AddRange(searchPoolTag.Where(go => go.CompareTag(searchTerm)));
|
||||
break;
|
||||
|
||||
case "by_layer":
|
||||
var searchPoolLayer = rootSearchObject
|
||||
? rootSearchObject
|
||||
.GetComponentsInChildren<Transform>(searchInactive)
|
||||
.Select(t => t.gameObject)
|
||||
: GetAllSceneObjects(searchInactive);
|
||||
if (int.TryParse(searchTerm, out int layerIndex))
|
||||
{
|
||||
results.AddRange(searchPoolLayer.Where(go => go.layer == layerIndex));
|
||||
}
|
||||
else
|
||||
{
|
||||
int namedLayer = LayerMask.NameToLayer(searchTerm);
|
||||
if (namedLayer != -1)
|
||||
results.AddRange(searchPoolLayer.Where(go => go.layer == namedLayer));
|
||||
}
|
||||
break;
|
||||
|
||||
case "by_component":
|
||||
Type componentType = FindType(searchTerm);
|
||||
if (componentType != null)
|
||||
{
|
||||
IEnumerable<GameObject> searchPoolComp;
|
||||
if (rootSearchObject)
|
||||
{
|
||||
searchPoolComp = rootSearchObject
|
||||
.GetComponentsInChildren(componentType, searchInactive)
|
||||
.Select(c => (c as Component).gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if UNITY_2023_1_OR_NEWER
|
||||
var inactive = searchInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude;
|
||||
searchPoolComp = UnityEngine.Object.FindObjectsByType(componentType, inactive, FindObjectsSortMode.None)
|
||||
.Cast<Component>()
|
||||
.Select(c => c.gameObject);
|
||||
#else
|
||||
searchPoolComp = UnityEngine.Object.FindObjectsOfType(componentType, searchInactive)
|
||||
.Cast<Component>()
|
||||
.Select(c => c.gameObject);
|
||||
#endif
|
||||
}
|
||||
results.AddRange(searchPoolComp.Where(go => go != null));
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn($"[ManageGameObject.Find] Component type not found: {searchTerm}");
|
||||
}
|
||||
break;
|
||||
|
||||
case "by_id_or_name_or_path":
|
||||
if (int.TryParse(searchTerm, out int id))
|
||||
{
|
||||
var allObjectsId = GetAllSceneObjects(true);
|
||||
GameObject objById = allObjectsId.FirstOrDefault(go => go.GetInstanceID() == id);
|
||||
if (objById != null)
|
||||
{
|
||||
results.Add(objById);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Try path search - in Prefab Stage, GameObject.Find() doesn't work
|
||||
var allObjectsForPath = GetAllSceneObjects(true);
|
||||
GameObject objByPath = allObjectsForPath.FirstOrDefault(go =>
|
||||
{
|
||||
return GameObjectLookup.MatchesPath(go, searchTerm);
|
||||
});
|
||||
if (objByPath != null)
|
||||
{
|
||||
results.Add(objByPath);
|
||||
break;
|
||||
}
|
||||
|
||||
var allObjectsName = GetAllSceneObjects(true);
|
||||
results.AddRange(allObjectsName.Where(go => go.name == searchTerm));
|
||||
break;
|
||||
|
||||
default:
|
||||
McpLog.Warn($"[ManageGameObject.Find] Unknown search method: {searchMethod}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!findAll && results.Count > 1)
|
||||
{
|
||||
return new List<GameObject> { results[0] };
|
||||
}
|
||||
|
||||
return results.Distinct().ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<GameObject> GetAllSceneObjects(bool includeInactive)
|
||||
{
|
||||
// Delegate to GameObjectLookup to avoid code duplication and ensure consistent behavior
|
||||
return GameObjectLookup.GetAllSceneObjects(includeInactive);
|
||||
}
|
||||
|
||||
private static Type FindType(string typeName)
|
||||
{
|
||||
if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error))
|
||||
{
|
||||
return resolvedType;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
McpLog.Warn($"[FindType] {error}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bf0edf3cd2af46729294682cee3bee4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user