unity-mcp/Editor/Commands/ScriptCommandHandler.cs

497 lines
20 KiB
C#
Raw Normal View History

2025-03-18 22:03:32 +08:00
using System;
2025-03-18 19:00:50 +08:00
using System.IO;
using System.Linq;
using System.Text;
2025-03-18 19:00:50 +08:00
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
2025-03-18 19:00:50 +08:00
2025-03-20 19:24:31 +08:00
namespace UnityMCP.Editor.Commands
2025-03-18 19:00:50 +08:00
{
/// <summary>
/// Handles script-related commands for Unity
/// </summary>
public static class ScriptCommandHandler
{
/// <summary>
/// Views the contents of a Unity script file
/// </summary>
public static object ViewScript(JObject @params)
{
string scriptPath =
(string)@params["script_path"]
?? throw new Exception("Parameter 'script_path' is required.");
2025-03-18 22:41:01 +08:00
bool requireExists = (bool?)@params["require_exists"] ?? true;
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
// Handle path correctly to avoid double "Assets" folder issue
string relativePath;
if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
// If path already starts with Assets/, remove it for local path operations
relativePath = scriptPath.Substring(7);
}
else
{
relativePath = scriptPath;
}
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
string fullPath = Path.Combine(Application.dataPath, relativePath);
2025-03-18 19:00:50 +08:00
if (!File.Exists(fullPath))
2025-03-18 22:41:01 +08:00
{
if (requireExists)
{
2025-03-20 19:24:31 +08:00
throw new Exception($"Script file not found: {scriptPath}");
2025-03-18 22:41:01 +08:00
}
else
{
return new { exists = false, message = $"Script file not found: {scriptPath}" };
}
}
2025-03-18 19:00:50 +08:00
string content = File.ReadAllText(fullPath);
byte[] contentBytes = System.Text.Encoding.UTF8.GetBytes(content);
string base64Content = Convert.ToBase64String(contentBytes);
return new
{
exists = true,
content = base64Content,
encoding = "base64"
};
2025-03-18 19:00:50 +08:00
}
/// <summary>
/// Ensures the Scripts folder exists in the project
/// </summary>
private static void EnsureScriptsFolderExists()
{
2025-03-18 22:41:01 +08:00
// Never create an "Assets" folder as it's the project root
// Instead create "Scripts" within the existing Assets folder
2025-03-18 19:00:50 +08:00
string scriptsFolderPath = Path.Combine(Application.dataPath, "Scripts");
if (!Directory.Exists(scriptsFolderPath))
{
Directory.CreateDirectory(scriptsFolderPath);
AssetDatabase.Refresh();
}
}
/// <summary>
2025-03-18 22:41:01 +08:00
/// Creates a new Unity script file in the specified folder
2025-03-18 19:00:50 +08:00
/// </summary>
public static object CreateScript(JObject @params)
{
string scriptName =
(string)@params["script_name"]
?? throw new Exception("Parameter 'script_name' is required.");
2025-03-18 19:00:50 +08:00
string scriptType = (string)@params["script_type"] ?? "MonoBehaviour";
string namespaceName = (string)@params["namespace"];
string template = (string)@params["template"];
2025-03-18 22:01:51 +08:00
string scriptFolder = (string)@params["script_folder"];
string content = (string)@params["content"];
2025-03-18 22:41:01 +08:00
bool overwrite = (bool?)@params["overwrite"] ?? false;
2025-03-18 19:00:50 +08:00
// Ensure script name ends with .cs
2025-03-18 22:41:01 +08:00
if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
2025-03-18 19:00:50 +08:00
scriptName += ".cs";
2025-03-18 22:03:32 +08:00
2025-03-18 22:41:01 +08:00
// Make sure scriptName doesn't contain path separators - extract base name
scriptName = Path.GetFileName(scriptName);
2025-03-18 22:03:32 +08:00
2025-03-18 22:41:01 +08:00
// Determine the script path
string scriptPath;
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
// Handle the script folder parameter
if (string.IsNullOrEmpty(scriptFolder))
2025-03-18 22:01:51 +08:00
{
2025-03-18 22:41:01 +08:00
// Default to Scripts folder within Assets
scriptPath = "Scripts";
EnsureScriptsFolderExists();
}
else
{
// Use provided folder path
scriptPath = scriptFolder;
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
// If scriptFolder starts with "Assets/", remove it for local path operations
if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
scriptPath = scriptPath.Substring(7);
}
}
2025-03-18 22:03:32 +08:00
2025-03-18 22:41:01 +08:00
// Create the full directory path, avoiding Assets/Assets issue
string folderPath = Path.Combine(Application.dataPath, scriptPath);
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
// Create directory if it doesn't exist
if (!Directory.Exists(folderPath))
{
try
2025-03-18 22:01:51 +08:00
{
Directory.CreateDirectory(folderPath);
AssetDatabase.Refresh();
}
2025-03-18 22:41:01 +08:00
catch (Exception ex)
{
2025-03-20 19:24:31 +08:00
throw new Exception($"Failed to create directory '{scriptPath}': {ex.Message}");
2025-03-18 22:41:01 +08:00
}
2025-03-18 22:01:51 +08:00
}
2025-03-18 22:03:32 +08:00
2025-03-18 22:41:01 +08:00
// Check if script already exists
string fullFilePath = Path.Combine(folderPath, scriptName);
if (File.Exists(fullFilePath) && !overwrite)
{
throw new Exception(
$"Script file '{scriptName}' already exists in '{scriptPath}' and overwrite is not enabled."
);
2025-03-18 22:41:01 +08:00
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
try
2025-03-18 19:00:50 +08:00
{
2025-03-18 22:41:01 +08:00
// If content is provided, use it directly
if (!string.IsNullOrEmpty(content))
2025-03-18 22:01:51 +08:00
{
2025-03-18 22:41:01 +08:00
// Create the script file with provided content
File.WriteAllText(fullFilePath, content);
2025-03-18 22:01:51 +08:00
}
2025-03-18 22:41:01 +08:00
else
2025-03-18 19:00:50 +08:00
{
2025-03-18 22:41:01 +08:00
// Otherwise generate content based on template and parameters
2025-03-20 19:24:31 +08:00
StringBuilder contentBuilder = new();
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Add using directives
contentBuilder.AppendLine("using UnityEngine;");
contentBuilder.AppendLine();
2025-03-18 22:03:32 +08:00
2025-03-18 22:41:01 +08:00
// Add namespace if specified
if (!string.IsNullOrEmpty(namespaceName))
{
contentBuilder.AppendLine($"namespace {namespaceName}");
contentBuilder.AppendLine("{");
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Add class definition with indent based on namespace
string indent = string.IsNullOrEmpty(namespaceName) ? "" : " ";
contentBuilder.AppendLine(
$"{indent}public class {Path.GetFileNameWithoutExtension(scriptName)} : {scriptType}"
);
2025-03-18 22:41:01 +08:00
contentBuilder.AppendLine($"{indent}{{");
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Add default Unity methods based on script type
if (scriptType == "MonoBehaviour")
{
contentBuilder.AppendLine($"{indent} private void Start()");
contentBuilder.AppendLine($"{indent} {{");
contentBuilder.AppendLine(
$"{indent} // Initialize your component here"
);
2025-03-18 22:41:01 +08:00
contentBuilder.AppendLine($"{indent} }}");
contentBuilder.AppendLine();
contentBuilder.AppendLine($"{indent} private void Update()");
contentBuilder.AppendLine($"{indent} {{");
contentBuilder.AppendLine($"{indent} // Update your component here");
contentBuilder.AppendLine($"{indent} }}");
}
else if (scriptType == "ScriptableObject")
{
contentBuilder.AppendLine($"{indent} private void OnEnable()");
contentBuilder.AppendLine($"{indent} {{");
contentBuilder.AppendLine(
$"{indent} // Initialize your ScriptableObject here"
);
2025-03-18 22:41:01 +08:00
contentBuilder.AppendLine($"{indent} }}");
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Close class
contentBuilder.AppendLine($"{indent}}}");
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Close namespace if specified
if (!string.IsNullOrEmpty(namespaceName))
{
contentBuilder.AppendLine("}");
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Write the generated content to file
File.WriteAllText(fullFilePath, contentBuilder.ToString());
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Refresh the AssetDatabase to recognize the new script
AssetDatabase.Refresh();
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Return the relative path for easier reference
string relativePath = scriptPath.Replace('\\', '/');
if (!relativePath.StartsWith("Assets/"))
{
relativePath = $"Assets/{relativePath}";
}
2025-03-20 19:24:31 +08:00
return new
{
2025-03-18 22:41:01 +08:00
message = $"Created script: {Path.Combine(relativePath, scriptName).Replace('\\', '/')}",
script_path = Path.Combine(relativePath, scriptName).Replace('\\', '/')
};
}
catch (Exception ex)
{
Debug.LogError($"Failed to create script: {ex.Message}\n{ex.StackTrace}");
2025-03-20 19:24:31 +08:00
throw new Exception($"Failed to create script '{scriptName}': {ex.Message}");
2025-03-18 22:41:01 +08:00
}
2025-03-18 19:00:50 +08:00
}
/// <summary>
/// Updates the contents of an existing Unity script
/// </summary>
public static object UpdateScript(JObject @params)
{
string scriptPath =
(string)@params["script_path"]
?? throw new Exception("Parameter 'script_path' is required.");
string content =
(string)@params["content"]
?? throw new Exception("Parameter 'content' is required.");
2025-03-18 22:01:51 +08:00
bool createIfMissing = (bool?)@params["create_if_missing"] ?? false;
bool createFolderIfMissing = (bool?)@params["create_folder_if_missing"] ?? false;
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Handle path correctly to avoid double "Assets" folder
string relativePath;
if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
// If path already starts with Assets/, remove it for local path operations
relativePath = scriptPath.Substring(7);
}
else
{
relativePath = scriptPath;
}
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
string fullPath = Path.Combine(Application.dataPath, relativePath);
2025-03-18 22:01:51 +08:00
string directory = Path.GetDirectoryName(fullPath);
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
// Debug the paths to help diagnose issues
2025-03-20 19:24:31 +08:00
2025-03-18 19:00:50 +08:00
2025-03-18 22:01:51 +08:00
// Check if file exists, create if requested
2025-03-18 19:00:50 +08:00
if (!File.Exists(fullPath))
2025-03-18 22:01:51 +08:00
{
if (createIfMissing)
{
// Create the directory if requested and needed
if (!Directory.Exists(directory) && createFolderIfMissing)
{
Directory.CreateDirectory(directory);
}
else if (!Directory.Exists(directory))
{
throw new Exception(
$"Directory does not exist: {Path.GetDirectoryName(scriptPath)}"
);
2025-03-18 22:01:51 +08:00
}
2025-03-18 22:03:32 +08:00
2025-03-18 22:01:51 +08:00
// Create the file with content
File.WriteAllText(fullPath, content);
AssetDatabase.Refresh();
return new { message = $"Created script: {scriptPath}" };
}
else
{
2025-03-20 19:24:31 +08:00
throw new Exception($"Script file not found: {scriptPath}");
2025-03-18 22:01:51 +08:00
}
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:01:51 +08:00
// Update existing script
2025-03-18 19:00:50 +08:00
File.WriteAllText(fullPath, content);
// Refresh the AssetDatabase
AssetDatabase.Refresh();
return new { message = $"Updated script: {scriptPath}" };
}
/// <summary>
/// Lists all script files in a specified folder
/// </summary>
public static object ListScripts(JObject @params)
{
string folderPath = (string)@params["folder_path"] ?? "Assets";
2025-03-18 22:03:32 +08:00
2025-03-18 22:01:51 +08:00
// Special handling for "Assets" path since it's already the root
string fullPath;
if (folderPath.Equals("Assets", StringComparison.OrdinalIgnoreCase))
{
fullPath = Application.dataPath;
}
else if (folderPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
// Remove "Assets/" from the path since Application.dataPath already points to it
string relativePath = folderPath.Substring(7);
fullPath = Path.Combine(Application.dataPath, relativePath);
}
else
{
// Assume it's a relative path from Assets
fullPath = Path.Combine(Application.dataPath, folderPath);
}
2025-03-18 19:00:50 +08:00
if (!Directory.Exists(fullPath))
2025-03-20 19:24:31 +08:00
throw new Exception($"Folder not found: {folderPath}");
2025-03-18 19:00:50 +08:00
string[] scripts = Directory
.GetFiles(fullPath, "*.cs", SearchOption.AllDirectories)
2025-03-18 19:00:50 +08:00
.Select(path => path.Replace(Application.dataPath, "Assets"))
.ToArray();
return new { scripts };
}
/// <summary>
/// Attaches a script component to a GameObject
/// </summary>
public static object AttachScript(JObject @params)
{
string objectName =
(string)@params["object_name"]
?? throw new Exception("Parameter 'object_name' is required.");
string scriptName =
(string)@params["script_name"]
?? throw new Exception("Parameter 'script_name' is required.");
2025-03-18 22:41:01 +08:00
string scriptPath = (string)@params["script_path"]; // Optional
2025-03-18 19:00:50 +08:00
// Find the target object
GameObject targetObject = GameObject.Find(objectName);
if (targetObject == null)
2025-03-20 19:24:31 +08:00
throw new Exception($"Object '{objectName}' not found in scene.");
2025-03-18 19:00:50 +08:00
// Ensure script name ends with .cs
2025-03-18 22:41:01 +08:00
if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
2025-03-18 19:00:50 +08:00
scriptName += ".cs";
2025-03-18 22:41:01 +08:00
// Remove the path from the scriptName if it contains path separators
string scriptFileName = Path.GetFileName(scriptName);
string scriptNameWithoutExtension = Path.GetFileNameWithoutExtension(scriptFileName);
2025-03-18 19:00:50 +08:00
// Find the script asset
2025-03-18 22:41:01 +08:00
string[] guids;
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
if (!string.IsNullOrEmpty(scriptPath))
{
// If a specific path is provided, try that first
if (
File.Exists(
Path.Combine(Application.dataPath, scriptPath.Replace("Assets/", ""))
)
)
2025-03-18 22:41:01 +08:00
{
// Use the direct path if it exists
MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);
if (scriptAsset != null)
{
2025-03-20 19:24:31 +08:00
Type scriptType = scriptAsset.GetClass();
2025-03-18 22:41:01 +08:00
if (scriptType != null)
{
try
{
// Try to add the component
Component component = targetObject.AddComponent(scriptType);
if (component != null)
{
return new
{
message = $"Successfully attached script '{scriptFileName}' to object '{objectName}'",
component_type = scriptType.Name
};
}
}
catch (Exception ex)
{
Debug.LogError($"Error attaching script component: {ex.Message}");
2025-03-20 19:24:31 +08:00
throw new Exception($"Failed to add component: {ex.Message}");
2025-03-18 22:41:01 +08:00
}
}
}
}
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Use the file name for searching if direct path didn't work
guids = AssetDatabase.FindAssets(scriptNameWithoutExtension + " t:script");
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
if (guids.Length == 0)
{
// Try a broader search if exact match fails
guids = AssetDatabase.FindAssets(scriptNameWithoutExtension);
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
if (guids.Length == 0)
2025-03-20 19:24:31 +08:00
throw new Exception($"Script '{scriptFileName}' not found in project.");
2025-03-18 22:41:01 +08:00
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Check each potential script until we find one that can be attached
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
// Filter to only consider .cs files
if (!path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
continue;
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
// Double check the file name to avoid false matches
string foundFileName = Path.GetFileName(path);
if (
!string.Equals(
foundFileName,
scriptFileName,
StringComparison.OrdinalIgnoreCase
)
&& !string.Equals(
Path.GetFileNameWithoutExtension(foundFileName),
scriptNameWithoutExtension,
StringComparison.OrdinalIgnoreCase
)
)
2025-03-18 22:41:01 +08:00
continue;
2025-03-20 19:24:31 +08:00
2025-03-18 22:41:01 +08:00
MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(path);
if (scriptAsset == null)
continue;
2025-03-20 19:24:31 +08:00
Type scriptType = scriptAsset.GetClass();
2025-03-18 22:41:01 +08:00
if (scriptType == null || !typeof(MonoBehaviour).IsAssignableFrom(scriptType))
continue;
try
{
// Check if component is already attached
if (targetObject.GetComponent(scriptType) != null)
{
return new
{
message = $"Script '{scriptNameWithoutExtension}' is already attached to object '{objectName}'",
component_type = scriptType.Name
};
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// Add the component
Component component = targetObject.AddComponent(scriptType);
if (component != null)
{
return new
{
message = $"Successfully attached script '{scriptFileName}' to object '{objectName}'",
component_type = scriptType.Name,
script_path = path
};
}
}
catch (Exception ex)
{
Debug.LogError($"Error attaching script '{path}': {ex.Message}");
// Continue trying other matches instead of failing immediately
}
}
2025-03-18 19:00:50 +08:00
2025-03-18 22:41:01 +08:00
// If we've tried all possibilities and nothing worked
throw new Exception(
$"Could not attach script '{scriptFileName}' to object '{objectName}'. No valid script found or component creation failed."
);
2025-03-18 19:00:50 +08:00
}
}
}