fixed prefab creation/update errors
parent
4f5a6a0014
commit
ba4c2a85bf
|
|
@ -17,6 +17,14 @@ namespace UnityMCP.Editor.Tools
|
|||
{
|
||||
// --- Main Handler ---
|
||||
|
||||
// Define the list of valid actions
|
||||
private static readonly List<string> ValidActions = new List<string>
|
||||
{
|
||||
"import", "create", "modify", "delete", "duplicate",
|
||||
"move", "rename", "search", "get_info", "create_folder",
|
||||
"get_components"
|
||||
};
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
string action = @params["action"]?.ToString().ToLower();
|
||||
|
|
@ -25,6 +33,13 @@ namespace UnityMCP.Editor.Tools
|
|||
return Response.Error("Action parameter is required.");
|
||||
}
|
||||
|
||||
// Check if the action is valid before switching
|
||||
if (!ValidActions.Contains(action))
|
||||
{
|
||||
string validActionsList = string.Join(", ", ValidActions);
|
||||
return Response.Error($"Unknown action: '{action}'. Valid actions are: {validActionsList}");
|
||||
}
|
||||
|
||||
// Common parameters
|
||||
string path = @params["path"]?.ToString();
|
||||
|
||||
|
|
@ -52,9 +67,13 @@ namespace UnityMCP.Editor.Tools
|
|||
return GetAssetInfo(path, @params["generatePreview"]?.ToObject<bool>() ?? false);
|
||||
case "create_folder": // Added specific action for clarity
|
||||
return CreateFolder(path);
|
||||
case "get_components":
|
||||
return GetComponentsFromAsset(path);
|
||||
|
||||
default:
|
||||
return Response.Error($"Unknown action: '{action}'.");
|
||||
// This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications.
|
||||
string validActionsListDefault = string.Join(", ", ValidActions);
|
||||
return Response.Error($"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -239,27 +258,72 @@ namespace UnityMCP.Editor.Tools
|
|||
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
|
||||
if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}");
|
||||
|
||||
bool modified = false;
|
||||
// Example: Modifying a Material
|
||||
if (asset is Material material)
|
||||
bool modified = false; // Flag to track if any changes were made
|
||||
|
||||
// --- NEW: Handle GameObject / Prefab Component Modification ---
|
||||
if (asset is GameObject gameObject)
|
||||
{
|
||||
modified = ApplyMaterialProperties(material, properties);
|
||||
// Iterate through the properties JSON: keys are component names, values are properties objects for that component
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string componentName = prop.Name; // e.g., "Collectible"
|
||||
// Check if the value associated with the component name is actually an object containing properties
|
||||
if (prop.Value is JObject componentProperties && componentProperties.HasValues) // e.g., {"bobSpeed": 2.0}
|
||||
{
|
||||
// Find the component on the GameObject using the name from the JSON key
|
||||
// Using GetComponent(string) is convenient but might require exact type name or be ambiguous.
|
||||
// Consider using FindType helper if needed for more complex scenarios.
|
||||
Component targetComponent = gameObject.GetComponent(componentName);
|
||||
|
||||
if (targetComponent != null)
|
||||
{
|
||||
// Apply the nested properties (e.g., bobSpeed) to the found component instance
|
||||
// Use |= to ensure 'modified' becomes true if any component is successfully modified
|
||||
modified |= ApplyObjectProperties(targetComponent, componentProperties);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Log a warning if a specified component couldn't be found
|
||||
Debug.LogWarning($"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Log a warning if the structure isn't {"ComponentName": {"prop": value}}
|
||||
// We could potentially try to apply this property directly to the GameObject here if needed,
|
||||
// but the primary goal is component modification.
|
||||
Debug.LogWarning($"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping.");
|
||||
}
|
||||
}
|
||||
// Note: 'modified' is now true if ANY component property was successfully changed.
|
||||
}
|
||||
// Example: Modifying a ScriptableObject (more complex, needs reflection or specific interface)
|
||||
// --- End NEW ---
|
||||
|
||||
// --- Existing logic for other asset types (now as else-if) ---
|
||||
// Example: Modifying a Material
|
||||
else if (asset is Material material)
|
||||
{
|
||||
// Apply properties directly to the material. If this modifies, it sets modified=true.
|
||||
// Use |= in case the asset was already marked modified by previous logic (though unlikely here)
|
||||
modified |= ApplyMaterialProperties(material, properties);
|
||||
}
|
||||
// Example: Modifying a ScriptableObject
|
||||
else if (asset is ScriptableObject so)
|
||||
{
|
||||
modified = ApplyObjectProperties(so, properties); // General helper
|
||||
// Apply properties directly to the ScriptableObject.
|
||||
modified |= ApplyObjectProperties(so, properties); // General helper
|
||||
}
|
||||
// Example: Modifying TextureImporter settings
|
||||
else if (asset is Texture) {
|
||||
AssetImporter importer = AssetImporter.GetAtPath(fullPath);
|
||||
if (importer is TextureImporter textureImporter)
|
||||
{
|
||||
modified = ApplyObjectProperties(textureImporter, properties);
|
||||
if (modified) {
|
||||
// Importer settings need saving
|
||||
bool importerModified = ApplyObjectProperties(textureImporter, properties);
|
||||
if (importerModified) {
|
||||
// Importer settings need saving and reimporting
|
||||
AssetDatabase.WriteImportSettingsIfDirty(fullPath);
|
||||
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes
|
||||
modified = true; // Mark overall operation as modified
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -267,25 +331,37 @@ namespace UnityMCP.Editor.Tools
|
|||
}
|
||||
}
|
||||
// TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.)
|
||||
else
|
||||
else // Fallback for other asset types OR direct properties on non-GameObject assets
|
||||
{
|
||||
Debug.LogWarning($"Modification for asset type '{asset.GetType().Name}' at '{fullPath}' is not fully implemented. Attempting generic property setting.");
|
||||
modified = ApplyObjectProperties(asset, properties);
|
||||
// This block handles non-GameObject/Material/ScriptableObject/Texture assets.
|
||||
// Attempts to apply properties directly to the asset itself.
|
||||
Debug.LogWarning($"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself.");
|
||||
modified |= ApplyObjectProperties(asset, properties);
|
||||
}
|
||||
// --- End Existing Logic ---
|
||||
|
||||
// Check if any modification happened (either component or direct asset modification)
|
||||
if (modified)
|
||||
{
|
||||
EditorUtility.SetDirty(asset); // Mark the asset itself as dirty
|
||||
AssetDatabase.SaveAssets(); // Save changes to disk
|
||||
// AssetDatabase.Refresh(); // SaveAssets usually handles refresh
|
||||
// Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it.
|
||||
EditorUtility.SetDirty(asset);
|
||||
// Save all modified assets to disk.
|
||||
AssetDatabase.SaveAssets();
|
||||
// Refresh might be needed in some edge cases, but SaveAssets usually covers it.
|
||||
// AssetDatabase.Refresh();
|
||||
return Response.Success($"Asset '{fullPath}' modified successfully.", GetAssetData(fullPath));
|
||||
} else {
|
||||
return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath));
|
||||
// If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed.
|
||||
return Response.Success($"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.", GetAssetData(fullPath));
|
||||
// Previous message: return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}");
|
||||
// Log the detailed error internally
|
||||
Debug.LogError($"[ManageAsset] Action 'modify' failed for path '{path}': {e}");
|
||||
// Return a user-friendly error message
|
||||
return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -487,6 +563,61 @@ namespace UnityMCP.Editor.Tools
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves components attached to a GameObject asset (like a Prefab).
|
||||
/// </summary>
|
||||
/// <param name="path">The asset path of the GameObject or Prefab.</param>
|
||||
/// <returns>A response object containing a list of component type names or an error.</returns>
|
||||
private static object GetComponentsFromAsset(string path)
|
||||
{
|
||||
// 1. Validate input path
|
||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for get_components.");
|
||||
|
||||
// 2. Sanitize and check existence
|
||||
string fullPath = SanitizeAssetPath(path);
|
||||
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
|
||||
|
||||
try
|
||||
{
|
||||
// 3. Load the asset
|
||||
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
|
||||
if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}");
|
||||
|
||||
// 4. Check if it's a GameObject (Prefabs load as GameObjects)
|
||||
GameObject gameObject = asset as GameObject;
|
||||
if (gameObject == null)
|
||||
{
|
||||
// Also check if it's *directly* a Component type (less common for primary assets)
|
||||
Component componentAsset = asset as Component;
|
||||
if (componentAsset != null) {
|
||||
// If the asset itself *is* a component, maybe return just its info?
|
||||
// This is an edge case. Let's stick to GameObjects for now.
|
||||
return Response.Error($"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject.");
|
||||
}
|
||||
return Response.Error($"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type.");
|
||||
}
|
||||
|
||||
// 5. Get components
|
||||
Component[] components = gameObject.GetComponents<Component>();
|
||||
|
||||
// 6. Format component data
|
||||
List<object> componentList = components.Select(comp => new {
|
||||
typeName = comp.GetType().FullName,
|
||||
instanceID = comp.GetInstanceID(),
|
||||
// TODO: Add more component-specific details here if needed in the future?
|
||||
// Requires reflection or specific handling per component type.
|
||||
}).ToList<object>(); // Explicit cast for clarity if needed
|
||||
|
||||
// 7. Return success response
|
||||
return Response.Success($"Found {componentList.Count} component(s) on asset '{fullPath}'.", componentList);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}");
|
||||
return Response.Error($"Error getting components for asset '{fullPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Internal Helpers ---
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,54 @@ namespace UnityMCP.Editor.Tools
|
|||
string searchMethod = @params["searchMethod"]?.ToString().ToLower();
|
||||
string name = @params["name"]?.ToString();
|
||||
|
||||
// --- Prefab Redirection Check ---
|
||||
string targetPath = targetToken?.Type == JTokenType.String ? targetToken.ToString() : null;
|
||||
if (!string.IsNullOrEmpty(targetPath) && targetPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Allow 'create' (instantiate), 'find' (?), 'get_components' (?)
|
||||
if (action == "modify" || action == "set_component_property")
|
||||
{
|
||||
Debug.Log($"[ManageGameObject->ManageAsset] Redirecting action '{action}' for prefab '{targetPath}' to ManageAsset.");
|
||||
// Prepare params for ManageAsset.ModifyAsset
|
||||
JObject assetParams = new JObject();
|
||||
assetParams["action"] = "modify"; // ManageAsset uses "modify"
|
||||
assetParams["path"] = targetPath;
|
||||
|
||||
// Extract properties.
|
||||
// For 'set_component_property', combine componentName and componentProperties.
|
||||
// For 'modify', directly use componentProperties.
|
||||
JObject properties = null;
|
||||
if (action == "set_component_property")
|
||||
{
|
||||
string compName = @params["componentName"]?.ToString();
|
||||
JObject compProps = @params["componentProperties"]?[compName] as JObject; // Handle potential nesting
|
||||
if (string.IsNullOrEmpty(compName)) return Response.Error("Missing 'componentName' for 'set_component_property' on prefab.");
|
||||
if (compProps == null) return Response.Error($"Missing or invalid 'componentProperties' for component '{compName}' for 'set_component_property' on prefab.");
|
||||
|
||||
properties = new JObject();
|
||||
properties[compName] = compProps;
|
||||
}
|
||||
else // action == "modify"
|
||||
{
|
||||
properties = @params["componentProperties"] as JObject;
|
||||
if (properties == null) return Response.Error("Missing 'componentProperties' for 'modify' action on prefab.");
|
||||
}
|
||||
|
||||
assetParams["properties"] = properties;
|
||||
|
||||
// Call ManageAsset handler
|
||||
return ManageAsset.HandleCommand(assetParams);
|
||||
}
|
||||
else if (action == "delete" || action == "add_component" || action == "remove_component" || action == "get_components") // Added get_components here too
|
||||
{
|
||||
// Explicitly block other modifications on the prefab asset itself via manage_gameobject
|
||||
return Response.Error($"Action '{action}' on a prefab asset ('{targetPath}') should be performed using the 'manage_asset' command.");
|
||||
}
|
||||
// Allow 'create' (instantiation) and 'find' to proceed, although finding a prefab asset by path might be less common via manage_gameobject.
|
||||
// No specific handling needed here, the code below will run.
|
||||
}
|
||||
// --- End Prefab Redirection Check ---
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
|
|
@ -80,48 +128,141 @@ namespace UnityMCP.Editor.Tools
|
|||
bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject<bool>() ?? false;
|
||||
string prefabPath = @params["prefabPath"]?.ToString();
|
||||
string tag = @params["tag"]?.ToString(); // Get tag for creation
|
||||
string primitiveType = @params["primitiveType"]?.ToString(); // Keep primitiveType check
|
||||
GameObject newGo = null; // Initialize as null
|
||||
|
||||
if (saveAsPrefab && string.IsNullOrEmpty(prefabPath))
|
||||
// --- Try Instantiating Prefab First ---
|
||||
string originalPrefabPath = prefabPath; // Keep original for messages
|
||||
if (!string.IsNullOrEmpty(prefabPath))
|
||||
{
|
||||
return Response.Error("'prefabPath' is required when 'saveAsPrefab' is true.");
|
||||
}
|
||||
if (saveAsPrefab && !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Response.Error($"'prefabPath' must end with '.prefab'. Provided: '{prefabPath}'");
|
||||
}
|
||||
|
||||
string primitiveType = @params["primitiveType"]?.ToString();
|
||||
GameObject newGo;
|
||||
|
||||
// Create primitive or empty GameObject
|
||||
if (!string.IsNullOrEmpty(primitiveType))
|
||||
{
|
||||
try
|
||||
// If no extension, search for the prefab by name
|
||||
if (!prefabPath.Contains("/") && !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true);
|
||||
newGo = GameObject.CreatePrimitive(type);
|
||||
newGo.name = name; // Set name after creation
|
||||
string prefabNameOnly = prefabPath;
|
||||
Debug.Log($"[ManageGameObject.Create] Searching for prefab named: '{prefabNameOnly}'");
|
||||
string[] guids = AssetDatabase.FindAssets($"t:Prefab {prefabNameOnly}");
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
return Response.Error($"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 Response.Error($"Multiple prefabs found matching name '{prefabNameOnly}': {foundPaths}. Please provide a more specific path.");
|
||||
}
|
||||
else // Exactly one found
|
||||
{
|
||||
prefabPath = AssetDatabase.GUIDToAssetPath(guids[0]); // Update prefabPath with the full path
|
||||
Debug.Log($"[ManageGameObject.Create] Found unique prefab at path: '{prefabPath}'");
|
||||
}
|
||||
}
|
||||
catch (ArgumentException)
|
||||
else if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Response.Error($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}");
|
||||
// If it looks like a path but doesn't end with .prefab, assume user forgot it and append it.
|
||||
// We could also error here, but appending might be more user-friendly.
|
||||
Debug.LogWarning($"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' does not end with .prefab. Assuming it's missing and appending.");
|
||||
prefabPath += ".prefab";
|
||||
// Note: This path might still not exist, AssetDatabase.LoadAssetAtPath will handle that.
|
||||
}
|
||||
|
||||
// Removed the early return error for missing .prefab ending.
|
||||
// The logic above now handles finding or assuming the .prefab extension.
|
||||
|
||||
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
if (prefabAsset != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Instantiate the prefab, initially place it at the root
|
||||
// Parent will be set later if specified
|
||||
newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject;
|
||||
|
||||
if (newGo == null)
|
||||
{
|
||||
// This might happen if the asset exists but isn't a valid GameObject prefab somehow
|
||||
Debug.LogError($"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject.");
|
||||
return Response.Error($"Failed to instantiate prefab at '{prefabPath}'.");
|
||||
}
|
||||
|
||||
// Name the instance based on the 'name' parameter, not the prefab's default name
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
newGo.name = name;
|
||||
}
|
||||
|
||||
// Register Undo for prefab instantiation
|
||||
Undo.RegisterCreatedObjectUndo(newGo, $"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'");
|
||||
Debug.Log($"[ManageGameObject.Create] Instantiated prefab '{prefabAsset.name}' from path '{prefabPath}' as '{newGo.name}'.");
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Error instantiating prefab '{prefabPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only return error if prefabPath was specified but not found.
|
||||
// If prefabPath was empty/null, we proceed to create primitive/empty.
|
||||
Debug.LogWarning($"[ManageGameObject.Create] Prefab asset not found at path: '{prefabPath}'. Will proceed to create new object if specified.");
|
||||
// Do not return error here, allow fallback to primitive/empty creation
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Failed to create primitive '{primitiveType}': {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
// --- Fallback: Create Primitive or Empty GameObject ---
|
||||
bool createdNewObject = false; // Flag to track if we created (not instantiated)
|
||||
if (newGo == null) // Only proceed if prefab instantiation didn't happen
|
||||
{
|
||||
newGo = new GameObject(name);
|
||||
if (!string.IsNullOrEmpty(primitiveType))
|
||||
{
|
||||
try
|
||||
{
|
||||
PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true);
|
||||
newGo = GameObject.CreatePrimitive(type);
|
||||
// Set name *after* creation for primitives
|
||||
if (!string.IsNullOrEmpty(name)) newGo.name = name;
|
||||
else return Response.Error("'name' parameter is required when creating a primitive."); // Name is essential
|
||||
createdNewObject = true;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return Response.Error($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Failed to create primitive '{primitiveType}': {e.Message}");
|
||||
}
|
||||
}
|
||||
else // Create empty GameObject
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return Response.Error("'name' parameter is required for 'create' action when not instantiating a prefab or creating a primitive.");
|
||||
}
|
||||
newGo = new GameObject(name);
|
||||
createdNewObject = true;
|
||||
}
|
||||
|
||||
// Record creation for Undo *only* if we created a new object
|
||||
if (createdNewObject)
|
||||
{
|
||||
Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Record creation for Undo (initial object)
|
||||
// Note: Prefab saving might have its own Undo implications or require different handling.
|
||||
// PrefabUtility operations often handle their own Undo steps.
|
||||
Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{name}'");
|
||||
// --- Common Setup (Parent, Transform, Tag, Components) - Applied AFTER object exists ---
|
||||
if (newGo == null)
|
||||
{
|
||||
// Should theoretically not happen if logic above is correct, but safety check.
|
||||
return Response.Error("Failed to create or instantiate the GameObject.");
|
||||
}
|
||||
|
||||
// Set Parent (before potentially making it a prefab root)
|
||||
// Record potential changes to the existing prefab instance or the new GO
|
||||
// Record transform separately in case parent changes affect it
|
||||
Undo.RecordObject(newGo.transform, "Set GameObject Transform");
|
||||
Undo.RecordObject(newGo, "Set GameObject Properties");
|
||||
|
||||
// Set Parent
|
||||
JToken parentToken = @params["parent"];
|
||||
if (parentToken != null)
|
||||
{
|
||||
|
|
@ -202,52 +343,85 @@ namespace UnityMCP.Editor.Tools
|
|||
}
|
||||
}
|
||||
|
||||
// Save as Prefab if requested
|
||||
GameObject prefabInstance = newGo; // Keep track of the instance potentially linked to the prefab
|
||||
if (saveAsPrefab)
|
||||
// Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true
|
||||
GameObject finalInstance = newGo; // Use this for selection and return data
|
||||
if (createdNewObject && saveAsPrefab)
|
||||
{
|
||||
string finalPrefabPath = prefabPath; // Use a separate variable for saving path
|
||||
// This check should now happen *before* attempting to save
|
||||
if (string.IsNullOrEmpty(finalPrefabPath))
|
||||
{
|
||||
// Clean up the created object before returning error
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return Response.Error("'prefabPath' is required when 'saveAsPrefab' is true and creating a new object.");
|
||||
}
|
||||
// Ensure the *saving* path ends with .prefab
|
||||
if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Debug.Log($"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'");
|
||||
finalPrefabPath += ".prefab";
|
||||
}
|
||||
|
||||
// Removed the error check here as we now ensure the extension exists
|
||||
// if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// UnityEngine.Object.DestroyImmediate(newGo);
|
||||
// return Response.Error($"'prefabPath' must end with '.prefab'. Provided: '{prefabPath}'");
|
||||
// }
|
||||
|
||||
try
|
||||
{
|
||||
// Ensure directory exists
|
||||
string directoryPath = System.IO.Path.GetDirectoryName(prefabPath);
|
||||
if (!System.IO.Directory.Exists(directoryPath))
|
||||
// Ensure directory exists using the final saving path
|
||||
string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath);
|
||||
if (!string.IsNullOrEmpty(directoryPath) && !System.IO.Directory.Exists(directoryPath))
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(directoryPath);
|
||||
AssetDatabase.Refresh(); // Refresh asset database to recognize the new folder
|
||||
Debug.Log($"[ManageGameObject.Create] Created directory for prefab: {directoryPath}");
|
||||
}
|
||||
|
||||
// Save the GameObject as a prefab asset and connect the instance
|
||||
// Use SaveAsPrefabAssetAndConnect to keep the instance in the scene linked
|
||||
prefabInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, prefabPath, InteractionMode.UserAction);
|
||||
// Use SaveAsPrefabAssetAndConnect with the final saving path
|
||||
finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, finalPrefabPath, InteractionMode.UserAction);
|
||||
|
||||
if (prefabInstance == null)
|
||||
if (finalInstance == null)
|
||||
{
|
||||
// Destroy the original if saving failed somehow (shouldn't usually happen if path is valid)
|
||||
UnityEngine.Object.DestroyImmediate(newGo);
|
||||
return Response.Error($"Failed to save GameObject '{name}' as prefab at '{prefabPath}'. Check path and permissions.");
|
||||
return Response.Error($"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions.");
|
||||
}
|
||||
Debug.Log($"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{prefabPath}' and instance connected.");
|
||||
Debug.Log($"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected.");
|
||||
// Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it.
|
||||
// EditorUtility.SetDirty(prefabInstance); // Instance is handled by SaveAsPrefabAssetAndConnect
|
||||
// EditorUtility.SetDirty(finalInstance); // Instance is handled by SaveAsPrefabAssetAndConnect
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Clean up the instance if prefab saving fails
|
||||
UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt
|
||||
return Response.Error($"Error saving prefab '{prefabPath}': {e.Message}");
|
||||
return Response.Error($"Error saving prefab '{finalPrefabPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Select the instance in the scene (which might now be a prefab instance)
|
||||
Selection.activeGameObject = prefabInstance;
|
||||
// Select the instance in the scene (either prefab instance or newly created/saved one)
|
||||
Selection.activeGameObject = finalInstance;
|
||||
|
||||
string successMessage = saveAsPrefab
|
||||
? $"GameObject '{name}' created and saved as prefab to '{prefabPath}'."
|
||||
: $"GameObject '{name}' created successfully in scene.";
|
||||
// Determine appropriate success message using the potentially updated or original path
|
||||
string messagePrefabPath = finalInstance == null ? originalPrefabPath : AssetDatabase.GetAssetPath(PrefabUtility.GetCorrespondingObjectFromSource(finalInstance) ?? (UnityEngine.Object)finalInstance);
|
||||
string successMessage;
|
||||
if (!createdNewObject && !string.IsNullOrEmpty(messagePrefabPath)) // Instantiated existing prefab
|
||||
{
|
||||
successMessage = $"Prefab '{messagePrefabPath}' instantiated successfully as '{finalInstance.name}'.";
|
||||
}
|
||||
else if (createdNewObject && saveAsPrefab && !string.IsNullOrEmpty(messagePrefabPath)) // Created new and saved as prefab
|
||||
{
|
||||
successMessage = $"GameObject '{finalInstance.name}' created and saved as prefab to '{messagePrefabPath}'.";
|
||||
}
|
||||
else // Created new primitive or empty GO, didn't save as prefab
|
||||
{
|
||||
successMessage = $"GameObject '{finalInstance.name}' created successfully in scene.";
|
||||
}
|
||||
|
||||
// Return data for the instance in the scene
|
||||
return Response.Success(successMessage, GetGameObjectData(prefabInstance));
|
||||
return Response.Success(successMessage, GetGameObjectData(finalInstance));
|
||||
}
|
||||
|
||||
private static object ModifyGameObject(JObject @params, JToken targetToken, string searchMethod)
|
||||
|
|
@ -967,10 +1141,13 @@ namespace UnityMCP.Editor.Tools
|
|||
{
|
||||
try
|
||||
{
|
||||
// Basic types first
|
||||
if (targetType == typeof(string)) return token.ToObject<string>();
|
||||
if (targetType == typeof(int)) return token.ToObject<int>();
|
||||
if (targetType == typeof(float)) return token.ToObject<float>();
|
||||
if (targetType == typeof(bool)) return token.ToObject<bool>();
|
||||
|
||||
// Vector/Quaternion/Color types
|
||||
if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2)
|
||||
return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>());
|
||||
if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3)
|
||||
|
|
@ -981,47 +1158,121 @@ namespace UnityMCP.Editor.Tools
|
|||
return new Quaternion(arrQ[0].ToObject<float>(), arrQ[1].ToObject<float>(), arrQ[2].ToObject<float>(), arrQ[3].ToObject<float>());
|
||||
if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA
|
||||
return new Color(arrC[0].ToObject<float>(), arrC[1].ToObject<float>(), arrC[2].ToObject<float>(), arrC.Count > 3 ? arrC[3].ToObject<float>() : 1.0f);
|
||||
if (targetType.IsEnum)
|
||||
|
||||
// Enum types
|
||||
if (targetType.IsEnum)
|
||||
return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing
|
||||
|
||||
// Handle assigning Unity Objects (like Prefabs, Materials, Textures) using their asset path
|
||||
// Handle assigning Unity Objects (Assets, Scene Objects, Components)
|
||||
if (typeof(UnityEngine.Object).IsAssignableFrom(targetType))
|
||||
{
|
||||
// Check if the input token is a string, which we'll assume is the asset path
|
||||
if (token.Type == JTokenType.String)
|
||||
{
|
||||
string assetPath = token.ToString();
|
||||
if (!string.IsNullOrEmpty(assetPath))
|
||||
{
|
||||
// Attempt to load the asset from the provided path using the target type
|
||||
UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(assetPath, targetType);
|
||||
if (loadedAsset != null)
|
||||
// CASE 1: Reference is a JSON Object specifying a scene object/component find criteria
|
||||
if (token is JObject refObject)
|
||||
{
|
||||
JToken findToken = refObject["find"];
|
||||
string findMethod = refObject["method"]?.ToString() ?? "by_id_or_name_or_path"; // Default search
|
||||
string componentTypeName = refObject["component"]?.ToString();
|
||||
|
||||
if (findToken == null)
|
||||
{
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Reference object missing 'find' property: {token}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the target GameObject
|
||||
// Pass 'searchInactive: true' for internal lookups to be more robust
|
||||
JObject findParams = new JObject();
|
||||
findParams["searchInactive"] = true;
|
||||
GameObject foundGo = FindObjectInternal(findToken, findMethod, findParams);
|
||||
|
||||
if (foundGo == null)
|
||||
{
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Could not find GameObject specified by reference object: {token}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// If a component type is specified, try to get it
|
||||
if (!string.IsNullOrEmpty(componentTypeName))
|
||||
{
|
||||
Type compType = FindType(componentTypeName);
|
||||
if (compType == null)
|
||||
{
|
||||
return loadedAsset; // Return the loaded asset if successful
|
||||
}
|
||||
else
|
||||
{
|
||||
// Log a warning if the asset could not be found at the path
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Could not load asset of type '{targetType.Name}' from path: '{assetPath}'. Make sure the path is correct and the asset exists.");
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Could not find component type '{componentTypeName}' specified in reference object: {token}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle cases where an empty string might be intended to clear the reference
|
||||
return null; // Assign null if the path is empty
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
// Ensure the targetType is assignable from the found component type
|
||||
if (!targetType.IsAssignableFrom(compType))
|
||||
{
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Found component '{componentTypeName}' but it is not assignable to the target property type '{targetType.Name}'. Reference: {token}");
|
||||
return null;
|
||||
}
|
||||
|
||||
Component foundComp = foundGo.GetComponent(compType);
|
||||
if (foundComp == null)
|
||||
{
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Found GameObject '{foundGo.name}' but could not find component '{componentTypeName}' on it. Reference: {token}");
|
||||
return null;
|
||||
}
|
||||
return foundComp; // Return the found component
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, return the GameObject itself, ensuring it's assignable
|
||||
if (!targetType.IsAssignableFrom(typeof(GameObject)))
|
||||
{
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Found GameObject '{foundGo.name}' but it is not assignable to the target property type '{targetType.Name}' (component name was not specified). Reference: {token}");
|
||||
return null;
|
||||
}
|
||||
return foundGo; // Return the found GameObject
|
||||
}
|
||||
}
|
||||
// CASE 2: Reference is a string, assume it's an asset path
|
||||
else if (token.Type == JTokenType.String)
|
||||
{
|
||||
string assetPath = token.ToString();
|
||||
if (!string.IsNullOrEmpty(assetPath))
|
||||
{
|
||||
// Attempt to load the asset from the provided path using the target type
|
||||
UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(assetPath, targetType);
|
||||
if (loadedAsset != null)
|
||||
{
|
||||
return loadedAsset; // Return the loaded asset if successful
|
||||
}
|
||||
else
|
||||
{
|
||||
// Log a warning if the asset could not be found at the path
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Could not load asset of type '{targetType.Name}' from path: '{assetPath}'. Make sure the path is correct and the asset exists.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle cases where an empty string might be intended to clear the reference
|
||||
return null; // Assign null if the path is empty
|
||||
}
|
||||
}
|
||||
// CASE 3: Reference is null or empty JToken, assign null
|
||||
else if (token.Type == JTokenType.Null || string.IsNullOrEmpty(token.ToString()))
|
||||
{
|
||||
// Log a warning if the input token is not a string (path) for a Unity Object assignment
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Expected a string asset path to assign Unity Object of type '{targetType.Name}', but received token type '{token.Type}'. Value: {token}");
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
// CASE 4: Invalid format for Unity Object reference
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Expected a string asset path or a reference object to assign Unity Object of type '{targetType.Name}', but received token type '{token.Type}'. Value: {token}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try direct conversion (might work for simple value types)
|
||||
return token.ToObject(targetType);
|
||||
// Fallback: Try direct conversion (might work for other simple value types)
|
||||
// Be cautious here, this might throw errors for complex types not handled above
|
||||
try {
|
||||
return token.ToObject(targetType);
|
||||
} catch (Exception directConversionEx) {
|
||||
Debug.LogWarning($"[ConvertJTokenToType] Direct conversion failed for JToken '{token}' to type '{targetType.Name}': {directConversionEx.Message}. Specific handling might be needed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -317,7 +317,6 @@ namespace UnityMCP.Editor.Tools
|
|||
childrenData.Add(GetGameObjectDataRecursive(child.gameObject));
|
||||
}
|
||||
|
||||
// Basic info
|
||||
var gameObjectData = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", go.name },
|
||||
|
|
@ -328,14 +327,12 @@ namespace UnityMCP.Editor.Tools
|
|||
{ "isStatic", go.isStatic },
|
||||
{ "instanceID", go.GetInstanceID() }, // Useful unique identifier
|
||||
{ "transform", new {
|
||||
position = go.transform.localPosition,
|
||||
rotation = go.transform.localRotation.eulerAngles, // Euler for simplicity
|
||||
scale = go.transform.localScale
|
||||
position = new { x = go.transform.localPosition.x, y = go.transform.localPosition.y, z = go.transform.localPosition.z },
|
||||
rotation = new { x = go.transform.localRotation.eulerAngles.x, y = go.transform.localRotation.eulerAngles.y, z = go.transform.localRotation.eulerAngles.z }, // Euler for simplicity
|
||||
scale = new { x = go.transform.localScale.x, y = go.transform.localScale.y, z = go.transform.localScale.z }
|
||||
}
|
||||
},
|
||||
{ "children", childrenData }
|
||||
// Add components if needed - potentially large data
|
||||
// { "components", go.GetComponents<Component>().Select(c => c.GetType().FullName).ToList() }
|
||||
};
|
||||
|
||||
return gameObjectData;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ namespace UnityMCP.Editor.Tools
|
|||
}
|
||||
|
||||
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
||||
string relativeDir = path ?? string.Empty;
|
||||
// Set default directory to "Scripts" if path is not provided
|
||||
string relativeDir = path ?? "Scripts"; // Default to "Scripts" if path is null
|
||||
if (!string.IsNullOrEmpty(relativeDir))
|
||||
{
|
||||
relativeDir = relativeDir.Replace('\\', '/').Trim('/');
|
||||
|
|
@ -52,6 +53,10 @@ namespace UnityMCP.Editor.Tools
|
|||
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
|
||||
}
|
||||
}
|
||||
// Handle empty string case explicitly after processing
|
||||
if (string.IsNullOrEmpty(relativeDir)) {
|
||||
relativeDir = "Scripts"; // Ensure default if path was provided as "" or only "/" or "Assets/"
|
||||
}
|
||||
|
||||
// Construct paths
|
||||
string scriptFileName = $"{name}.cs";
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ namespace UnityMCP.Editor.Tools
|
|||
public static class ReadConsole
|
||||
{
|
||||
// Reflection members for accessing internal LogEntry data
|
||||
private static MethodInfo _getEntriesMethod;
|
||||
// private static MethodInfo _getEntriesMethod; // Removed as it's unused and fails reflection
|
||||
private static MethodInfo _startGettingEntriesMethod;
|
||||
private static MethodInfo _stopGettingEntriesMethod;
|
||||
private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End...
|
||||
private static MethodInfo _clearMethod;
|
||||
private static MethodInfo _getCountMethod;
|
||||
private static MethodInfo _getEntryMethod;
|
||||
|
|
@ -38,33 +38,49 @@ namespace UnityMCP.Editor.Tools
|
|||
Type logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
|
||||
if (logEntriesType == null) throw new Exception("Could not find internal type UnityEditor.LogEntries");
|
||||
|
||||
_getEntriesMethod = logEntriesType.GetMethod("GetEntries", BindingFlags.Static | BindingFlags.Public);
|
||||
_startGettingEntriesMethod = logEntriesType.GetMethod("StartGettingEntries", BindingFlags.Static | BindingFlags.Public);
|
||||
_stopGettingEntriesMethod = logEntriesType.GetMethod("StopGettingEntries", BindingFlags.Static | BindingFlags.Public);
|
||||
_clearMethod = logEntriesType.GetMethod("Clear", BindingFlags.Static | BindingFlags.Public);
|
||||
_getCountMethod = logEntriesType.GetMethod("GetCount", BindingFlags.Static | BindingFlags.Public);
|
||||
_getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public);
|
||||
// Include NonPublic binding flags as internal APIs might change accessibility
|
||||
BindingFlags staticFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
BindingFlags instanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
_startGettingEntriesMethod = logEntriesType.GetMethod("StartGettingEntries", staticFlags);
|
||||
if (_startGettingEntriesMethod == null) throw new Exception("Failed to reflect LogEntries.StartGettingEntries");
|
||||
|
||||
// Try reflecting EndGettingEntries based on warning message
|
||||
_endGettingEntriesMethod = logEntriesType.GetMethod("EndGettingEntries", staticFlags);
|
||||
if (_endGettingEntriesMethod == null) throw new Exception("Failed to reflect LogEntries.EndGettingEntries");
|
||||
|
||||
_clearMethod = logEntriesType.GetMethod("Clear", staticFlags);
|
||||
if (_clearMethod == null) throw new Exception("Failed to reflect LogEntries.Clear");
|
||||
|
||||
_getCountMethod = logEntriesType.GetMethod("GetCount", staticFlags);
|
||||
if (_getCountMethod == null) throw new Exception("Failed to reflect LogEntries.GetCount");
|
||||
|
||||
_getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", staticFlags);
|
||||
if (_getEntryMethod == null) throw new Exception("Failed to reflect LogEntries.GetEntryInternal");
|
||||
|
||||
Type logEntryType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntry");
|
||||
if (logEntryType == null) throw new Exception("Could not find internal type UnityEditor.LogEntry");
|
||||
|
||||
_modeField = logEntryType.GetField("mode", BindingFlags.Instance | BindingFlags.Public);
|
||||
_messageField = logEntryType.GetField("message", BindingFlags.Instance | BindingFlags.Public);
|
||||
_fileField = logEntryType.GetField("file", BindingFlags.Instance | BindingFlags.Public);
|
||||
_lineField = logEntryType.GetField("line", BindingFlags.Instance | BindingFlags.Public);
|
||||
_instanceIdField = logEntryType.GetField("instanceID", BindingFlags.Instance | BindingFlags.Public);
|
||||
_modeField = logEntryType.GetField("mode", instanceFlags);
|
||||
if (_modeField == null) throw new Exception("Failed to reflect LogEntry.mode");
|
||||
|
||||
// Basic check if reflection worked
|
||||
if (_getEntriesMethod == null || _clearMethod == null || _modeField == null || _messageField == null)
|
||||
{
|
||||
throw new Exception("Failed to get required reflection members for LogEntries/LogEntry.");
|
||||
}
|
||||
_messageField = logEntryType.GetField("message", instanceFlags);
|
||||
if (_messageField == null) throw new Exception("Failed to reflect LogEntry.message");
|
||||
|
||||
_fileField = logEntryType.GetField("file", instanceFlags);
|
||||
if (_fileField == null) throw new Exception("Failed to reflect LogEntry.file");
|
||||
|
||||
_lineField = logEntryType.GetField("line", instanceFlags);
|
||||
if (_lineField == null) throw new Exception("Failed to reflect LogEntry.line");
|
||||
|
||||
_instanceIdField = logEntryType.GetField("instanceID", instanceFlags);
|
||||
if (_instanceIdField == null) throw new Exception("Failed to reflect LogEntry.instanceID");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries. Console reading/clearing will likely fail. Error: {e}");
|
||||
Debug.LogError($"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries/LogEntry. Console reading/clearing will likely fail. Specific Error: {e.Message}");
|
||||
// Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this.
|
||||
_getEntriesMethod = _startGettingEntriesMethod = _stopGettingEntriesMethod = _clearMethod = _getCountMethod = _getEntryMethod = null;
|
||||
_startGettingEntriesMethod = _endGettingEntriesMethod = _clearMethod = _getCountMethod = _getEntryMethod = null;
|
||||
_modeField = _messageField = _fileField = _lineField = _instanceIdField = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -73,9 +89,13 @@ namespace UnityMCP.Editor.Tools
|
|||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
// Check if reflection setup failed in static constructor
|
||||
if (_clearMethod == null || _getEntriesMethod == null || _startGettingEntriesMethod == null || _stopGettingEntriesMethod == null || _getCountMethod == null || _getEntryMethod == null || _modeField == null || _messageField == null)
|
||||
// Check if ALL required reflection members were successfully initialized.
|
||||
if (_startGettingEntriesMethod == null || _endGettingEntriesMethod == null ||
|
||||
_clearMethod == null || _getCountMethod == null || _getEntryMethod == null ||
|
||||
_modeField == null || _messageField == null || _fileField == null || _lineField == null || _instanceIdField == null)
|
||||
{
|
||||
// Log the error here as well for easier debugging in Unity Console
|
||||
Debug.LogError("[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue.");
|
||||
return Response.Error("ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs.");
|
||||
}
|
||||
|
||||
|
|
@ -162,6 +182,7 @@ namespace UnityMCP.Editor.Tools
|
|||
int mode = (int)_modeField.GetValue(logEntryInstance);
|
||||
string message = (string)_messageField.GetValue(logEntryInstance);
|
||||
string file = (string)_fileField.GetValue(logEntryInstance);
|
||||
|
||||
int line = (int)_lineField.GetValue(logEntryInstance);
|
||||
// int instanceId = (int)_instanceIdField.GetValue(logEntryInstance);
|
||||
|
||||
|
|
@ -220,15 +241,15 @@ namespace UnityMCP.Editor.Tools
|
|||
}
|
||||
catch (Exception e) {
|
||||
Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}");
|
||||
// Ensure StopGettingEntries is called even if there's an error during iteration
|
||||
try { _stopGettingEntriesMethod.Invoke(null, null); } catch { /* Ignore nested exception */ }
|
||||
// Ensure EndGettingEntries is called even if there's an error during iteration
|
||||
try { _endGettingEntriesMethod.Invoke(null, null); } catch { /* Ignore nested exception */ }
|
||||
return Response.Error($"Error retrieving log entries: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Ensure we always call StopGettingEntries
|
||||
try { _stopGettingEntriesMethod.Invoke(null, null); } catch (Exception e) {
|
||||
Debug.LogError($"[ReadConsole] Failed to call StopGettingEntries: {e}");
|
||||
// Ensure we always call EndGettingEntries
|
||||
try { _endGettingEntriesMethod.Invoke(null, null); } catch (Exception e) {
|
||||
Debug.LogError($"[ReadConsole] Failed to call EndGettingEntries: {e}");
|
||||
// Don't return error here as we might have valid data, but log it.
|
||||
}
|
||||
}
|
||||
|
|
@ -257,20 +278,30 @@ namespace UnityMCP.Editor.Tools
|
|||
|
||||
private static LogType GetLogTypeFromMode(int mode)
|
||||
{
|
||||
// Check for specific error/exception/assert types first
|
||||
// Combine general and scripting-specific bits for broader matching.
|
||||
// First, determine the type based on the original logic (most severe first)
|
||||
LogType initialType;
|
||||
if ((mode & (ModeBitError | ModeBitScriptingError | ModeBitException | ModeBitScriptingException)) != 0) {
|
||||
return LogType.Error;
|
||||
initialType = LogType.Error;
|
||||
}
|
||||
if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) {
|
||||
return LogType.Assert;
|
||||
else if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) {
|
||||
initialType = LogType.Assert;
|
||||
}
|
||||
if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) {
|
||||
return LogType.Warning;
|
||||
else if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) {
|
||||
initialType = LogType.Warning;
|
||||
}
|
||||
else {
|
||||
initialType = LogType.Log;
|
||||
}
|
||||
|
||||
// Apply the observed "one level lower" correction
|
||||
switch (initialType)
|
||||
{
|
||||
case LogType.Error: return LogType.Warning; // Error becomes Warning
|
||||
case LogType.Warning: return LogType.Log; // Warning becomes Log
|
||||
case LogType.Assert: return LogType.Assert; // Assert remains Assert (no lower level defined)
|
||||
case LogType.Log: return LogType.Log; // Log remains Log
|
||||
default: return LogType.Log; // Default fallback
|
||||
}
|
||||
// If none of the above, assume it's a standard log message.
|
||||
// This covers ModeBitLog and ModeBitScriptingLog.
|
||||
return LogType.Log;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -55,14 +55,16 @@ def asset_creation_strategy() -> str:
|
|||
"""Guide for discovering and using Unity MCP tools effectively."""
|
||||
return (
|
||||
"Available Unity MCP Server Tools:\\n\\n"
|
||||
"For detailed usage, please refer to the specific tool's documentation.\\n\\n"
|
||||
"- `manage_editor`: Controls editor state (play/pause/stop) and queries info (state, selection).\\n"
|
||||
"- `execute_menu_item`: Executes Unity Editor menu items by path (e.g., 'File/Save Project').\\n"
|
||||
"- `manage_editor`: Controls editor state and queries info.\\n"
|
||||
"- `execute_menu_item`: Executes Unity Editor menu items by path.\\n"
|
||||
"- `read_console`: Reads or clears Unity console messages, with filtering options.\\n"
|
||||
"- `manage_scene`: Manages scenes (load, save, create, get hierarchy).\\n"
|
||||
"- `manage_gameobject`: Manages GameObjects in the scene (CRUD, find, components, assign properties).\\n"
|
||||
"- `manage_script`: Manages C# script files (CRUD).\\n"
|
||||
"- `manage_asset`: Manages project assets (import, create, modify, delete, search).\\n\\n"
|
||||
"- `manage_scene`: Manages scenes.\\n"
|
||||
"- `manage_gameobject`: Manages GameObjects in the scene.\\n"
|
||||
"- `manage_script`: Manages C# script files.\\n"
|
||||
"- `manage_asset`: Manages prefabs and assets.\\n\\n"
|
||||
"Tips:\\n"
|
||||
"- Create prefabs for reusable GameObjects.\\n"
|
||||
"- Always include a camera and main light in your scenes.\\n"
|
||||
)
|
||||
|
||||
# Run the server
|
||||
|
|
|
|||
|
|
@ -11,10 +11,8 @@ def register_execute_menu_item_tools(mcp: FastMCP):
|
|||
async def execute_menu_item(
|
||||
ctx: Context,
|
||||
menu_path: str,
|
||||
action: Optional[str] = 'execute', # Allows extending later (e.g., 'validate', 'get_available')
|
||||
parameters: Optional[Dict[str, Any]] = None, # For menu items that might accept parameters (less common)
|
||||
# alias: Optional[str] = None, # Potential future addition for common commands
|
||||
# context: Optional[Dict[str, Any]] = None # Potential future addition for context-specific menus
|
||||
action: Optional[str] = 'execute',
|
||||
parameters: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Executes a Unity Editor menu item via its path (e.g., "File/Save Project").
|
||||
|
||||
|
|
@ -35,8 +33,6 @@ def register_execute_menu_item_tools(mcp: FastMCP):
|
|||
"action": action,
|
||||
"menuPath": menu_path,
|
||||
"parameters": parameters if parameters else {},
|
||||
# "alias": alias,
|
||||
# "context": context
|
||||
}
|
||||
|
||||
# Remove None values
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
"""
|
||||
Defines the manage_asset tool for interacting with Unity assets.
|
||||
"""
|
||||
import asyncio # Added: Import asyncio for running sync code in async
|
||||
from typing import Optional, Dict, Any, List
|
||||
from mcp.server.fastmcp import FastMCP, Context
|
||||
# from ..unity_connection import get_unity_connection # Original line that caused error
|
||||
from unity_connection import get_unity_connection # Use absolute import relative to Python dir
|
||||
|
||||
def register_manage_asset_tools(mcp: FastMCP):
|
||||
"""Registers the manage_asset tool with the MCP server."""
|
||||
|
|
@ -14,12 +17,11 @@ def register_manage_asset_tools(mcp: FastMCP):
|
|||
path: str,
|
||||
asset_type: Optional[str] = None,
|
||||
properties: Optional[Dict[str, Any]] = None,
|
||||
destination: Optional[str] = None, # Used for move/duplicate
|
||||
destination: Optional[str] = None,
|
||||
generate_preview: Optional[bool] = False,
|
||||
# Search specific parameters
|
||||
search_pattern: Optional[str] = None, # Replaces path for search action? Or use path as pattern?
|
||||
filter_type: Optional[str] = None, # Redundant with asset_type?
|
||||
filter_date_after: Optional[str] = None, # ISO 8601 format
|
||||
search_pattern: Optional[str] = None,
|
||||
filter_type: Optional[str] = None,
|
||||
filter_date_after: Optional[str] = None,
|
||||
page_size: Optional[int] = None,
|
||||
page_number: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
|
|
@ -27,7 +29,7 @@ def register_manage_asset_tools(mcp: FastMCP):
|
|||
|
||||
Args:
|
||||
ctx: The MCP context.
|
||||
action: Operation to perform (e.g., 'import', 'create', 'search').
|
||||
action: Operation to perform (e.g., 'import', 'create', 'modify', 'delete', 'duplicate', 'move', 'rename', 'search', 'get_info', 'create_folder', 'get_components').
|
||||
path: Asset path (e.g., "Materials/MyMaterial.mat") or search scope.
|
||||
asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'.
|
||||
properties: Dictionary of properties for 'create'/'modify'.
|
||||
|
|
@ -61,6 +63,18 @@ def register_manage_asset_tools(mcp: FastMCP):
|
|||
# Remove None values to avoid sending unnecessary nulls
|
||||
params_dict = {k: v for k, v in params_dict.items() if v is not None}
|
||||
|
||||
# Forward the command to the Unity editor handler using the send_command method
|
||||
# The C# side expects a command type and parameters.
|
||||
return await ctx.send_command("manage_asset", params_dict)
|
||||
# Get the current asyncio event loop
|
||||
loop = asyncio.get_running_loop()
|
||||
# Get the Unity connection instance
|
||||
connection = get_unity_connection()
|
||||
|
||||
# Run the synchronous send_command in the default executor (thread pool)
|
||||
# This prevents blocking the main async event loop.
|
||||
result = await loop.run_in_executor(
|
||||
None, # Use default executor
|
||||
connection.send_command, # The function to call
|
||||
"manage_asset", # First argument for send_command
|
||||
params_dict # Second argument for send_command
|
||||
)
|
||||
# Return the result obtained from Unity
|
||||
return result
|
||||
|
|
@ -11,15 +11,9 @@ def register_manage_editor_tools(mcp: FastMCP):
|
|||
action: str,
|
||||
wait_for_completion: Optional[bool] = None,
|
||||
# --- Parameters for specific actions ---
|
||||
# For 'set_active_tool'
|
||||
tool_name: Optional[str] = None,
|
||||
# For 'add_tag', 'remove_tag'
|
||||
tag_name: Optional[str] = None,
|
||||
# For 'add_layer', 'remove_layer'
|
||||
layer_name: Optional[str] = None,
|
||||
# Example: width: Optional[int] = None, height: Optional[int] = None
|
||||
# Example: window_name: Optional[str] = None
|
||||
# context: Optional[Dict[str, Any]] = None # Additional context
|
||||
) -> Dict[str, Any]:
|
||||
"""Controls and queries the Unity editor's state and settings.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,50 +9,60 @@ def register_manage_gameobject_tools(mcp: FastMCP):
|
|||
def manage_gameobject(
|
||||
ctx: Context,
|
||||
action: str,
|
||||
target: Optional[Union[str, int]] = None, # Name, path, or instance ID
|
||||
search_method: Optional[str] = None, # by_name, by_tag, by_layer, by_component, by_id
|
||||
target: Optional[Union[str, int]] = None,
|
||||
search_method: Optional[str] = None,
|
||||
# --- Parameters for 'create' ---
|
||||
name: Optional[str] = None, # Required for 'create'
|
||||
tag: Optional[str] = None, # Tag to assign during creation
|
||||
parent: Optional[Union[str, int]] = None, # Name or ID of parent
|
||||
position: Optional[List[float]] = None, # [x, y, z]
|
||||
rotation: Optional[List[float]] = None, # [x, y, z] Euler angles
|
||||
scale: Optional[List[float]] = None, # [x, y, z]
|
||||
components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None, # List of component names or dicts with properties
|
||||
primitive_type: Optional[str] = None, # Optional: create primitive (Cube, Sphere, etc.) instead of empty
|
||||
save_as_prefab: Optional[bool] = False, # If True, save the created object as a prefab
|
||||
prefab_path: Optional[str] = None, # Full path to save prefab (e.g., "Assets/Prefabs/MyObject.prefab"). Overrides prefab_folder.
|
||||
prefab_folder: Optional[str] = "Assets/Prefabs", # Default folder if prefab_path not set (e.g., "Assets/Prefabs")
|
||||
name: Optional[str] = None,
|
||||
tag: Optional[str] = None,
|
||||
parent: Optional[Union[str, int]] = None,
|
||||
position: Optional[List[float]] = None,
|
||||
rotation: Optional[List[float]] = None,
|
||||
scale: Optional[List[float]] = None,
|
||||
components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None,
|
||||
primitive_type: Optional[str] = None,
|
||||
save_as_prefab: Optional[bool] = False,
|
||||
prefab_path: Optional[str] = None,
|
||||
prefab_folder: Optional[str] = "Assets/Prefabs",
|
||||
# --- Parameters for 'modify' ---
|
||||
new_name: Optional[str] = None,
|
||||
new_parent: Optional[Union[str, int]] = None,
|
||||
set_active: Optional[bool] = None,
|
||||
new_tag: Optional[str] = None,
|
||||
new_layer: Optional[Union[str, int]] = None, # Layer name or number
|
||||
new_layer: Optional[Union[str, int]] = None,
|
||||
components_to_remove: Optional[List[str]] = None,
|
||||
component_properties: Optional[Dict[str, Dict[str, Any]]] = None, # { "ComponentName": { "propName": value } }
|
||||
component_properties: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
# --- Parameters for 'find' ---
|
||||
search_term: Optional[str] = None, # Used with search_method (e.g., name, tag value, component type)
|
||||
find_all: Optional[bool] = False, # Find all matches or just the first?
|
||||
search_in_children: Optional[bool] = False, # Limit search scope
|
||||
search_inactive: Optional[bool] = False, # Include inactive GameObjects?
|
||||
search_term: Optional[str] = None,
|
||||
find_all: Optional[bool] = False,
|
||||
search_in_children: Optional[bool] = False,
|
||||
search_inactive: Optional[bool] = False,
|
||||
# -- Component Management Arguments --
|
||||
component_name: Optional[str] = None, # Target component for component actions
|
||||
component_name: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Manages GameObjects: create, modify, delete, find, and component operations.
|
||||
|
||||
Args:
|
||||
action: Operation (e.g., 'create', 'modify', 'find', 'add_component').
|
||||
action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property').
|
||||
target: GameObject identifier (name, path, ID) for modify/delete/component actions.
|
||||
search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find'.
|
||||
search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups.
|
||||
component_properties: Dict mapping Component names to their properties to set.
|
||||
Example: {"Rigidbody": {"mass": 10.0, "useGravity": True}},
|
||||
To set references:
|
||||
- Use asset path string for Prefabs/Materials, e.g., {"MeshRenderer": {"material": "Assets/Materials/MyMat.mat"}}
|
||||
- Use a dict for scene objects/components, e.g.:
|
||||
{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}} (assigns GameObject)
|
||||
{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}} (assigns Component)
|
||||
Action-specific arguments (e.g., name, parent, position for 'create';
|
||||
component_name, component_properties for component actions;
|
||||
component_name for component actions;
|
||||
search_term, find_all for 'find').
|
||||
|
||||
Returns:
|
||||
Dictionary with operation results ('success', 'message', 'data').
|
||||
"""
|
||||
try:
|
||||
# --- Early check for attempting to modify a prefab asset ---
|
||||
# ----------------------------------------------------------
|
||||
|
||||
# Prepare parameters, removing None values
|
||||
params = {
|
||||
"action": action,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ def register_manage_scene_tools(mcp: FastMCP):
|
|||
name: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
build_index: Optional[int] = None,
|
||||
# Add other potential parameters like load_additive, etc. if needed
|
||||
# context: Optional[Dict[str, Any]] = None # Future: Contextual info (e.g., current project settings)
|
||||
) -> Dict[str, Any]:
|
||||
"""Manages Unity scenes (load, save, create, get hierarchy, etc.).
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ def register_manage_script_tools(mcp: FastMCP):
|
|||
namespace: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Manages C# scripts in Unity (create, read, update, delete).
|
||||
Make reference variables public for easier access in the Unity Editor.
|
||||
|
||||
Args:
|
||||
action: Operation ('create', 'read', 'update', 'delete').
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@ Defines the read_console tool for accessing Unity Editor console messages.
|
|||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from mcp.server.fastmcp import FastMCP, Context
|
||||
from unity_connection import get_unity_connection
|
||||
|
||||
def register_read_console_tools(mcp: FastMCP):
|
||||
"""Registers the read_console tool with the MCP server."""
|
||||
|
||||
@mcp.tool()
|
||||
async def read_console(
|
||||
def read_console(
|
||||
ctx: Context,
|
||||
action: Optional[str] = 'get', # Default action is to get messages
|
||||
types: Optional[List[str]] = ['error', 'warning', 'log'], # Default types to retrieve
|
||||
count: Optional[int] = None, # Max number of messages to return (null for all matching)
|
||||
filter_text: Optional[str] = None, # Text to filter messages by
|
||||
since_timestamp: Optional[str] = None, # ISO 8601 timestamp to get messages since
|
||||
format: Optional[str] = 'detailed', # 'plain', 'detailed', 'json'
|
||||
include_stacktrace: Optional[bool] = True, # Whether to include stack traces in detailed/json formats
|
||||
# context: Optional[Dict[str, Any]] = None # Future context
|
||||
action: Optional[str] = 'get',
|
||||
types: Optional[List[str]] = ['error', 'warning', 'log'],
|
||||
count: Optional[int] = None,
|
||||
filter_text: Optional[str] = None,
|
||||
since_timestamp: Optional[str] = None,
|
||||
format: Optional[str] = 'detailed',
|
||||
include_stacktrace: Optional[bool] = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""Gets messages from or clears the Unity Editor console.
|
||||
|
||||
|
|
@ -34,6 +34,9 @@ def register_read_console_tools(mcp: FastMCP):
|
|||
Dictionary with results. For 'get', includes 'data' (messages).
|
||||
"""
|
||||
|
||||
# Get the connection instance
|
||||
bridge = get_unity_connection()
|
||||
|
||||
# Normalize action
|
||||
action = action.lower() if action else 'get'
|
||||
|
||||
|
|
@ -55,6 +58,7 @@ def register_read_console_tools(mcp: FastMCP):
|
|||
if 'count' not in params_dict:
|
||||
params_dict['count'] = None
|
||||
|
||||
# Forward the command to the Unity editor handler
|
||||
# The C# handler name might need adjustment (e.g., CommandRegistry)
|
||||
return await ctx.bridge.unity_editor.HandleReadConsole(params_dict)
|
||||
# Forward the command using the bridge's send_command method
|
||||
# The command type is the name of the tool itself in this case
|
||||
# No await needed as send_command is synchronous
|
||||
return bridge.send_command("read_console", params_dict)
|
||||
Loading…
Reference in New Issue