using UnityEngine; using System; using System.Collections.Generic; using System.Security.Permissions; namespace UnityEditor.Formats.Fbx.Exporter { internal abstract class DCCIntegration { public abstract string DccDisplayName { get; } public abstract string IntegrationZipPath { get; } private static string s_integrationFolderPath = null; public static string IntegrationFolderPath { get { if (string.IsNullOrEmpty(s_integrationFolderPath)) { s_integrationFolderPath = Application.dataPath; } return s_integrationFolderPath; } set { if (!string.IsNullOrEmpty(value) && System.IO.Directory.Exists(value)) { s_integrationFolderPath = value; } else { Debug.LogError(string.Format("Failed to set integration folder path, invalid directory \"{0}\"", value)); } } } public void SetIntegrationFolderPath(string path) { IntegrationFolderPath = path; } /// /// Gets the integration zip full path as an absolute Unity-style path. /// /// The integration zip full path. public string IntegrationZipFullPath { get { return System.IO.Path.GetFullPath("Packages/com.unity.formats.fbx/Editor/Integrations~").Replace("\\", "/") + "/" + IntegrationZipPath; } } /// /// Gets the project path. /// /// The project path. public static string ProjectPath { get { return System.IO.Directory.GetParent(Application.dataPath).FullName.Replace("\\", "/"); } } /// /// Installs the integration using the provided executable. /// /// The integration. /// Exe. [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] public abstract int InstallIntegration(string exe); /// /// Determines if folder is already unzipped at the specified path. /// /// true if folder is already unzipped at the specified path; otherwise, false. /// Path. public abstract bool FolderAlreadyUnzippedAtPath(string path); /// /// Launches application at given path /// /// [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] public static void LaunchDCCApplication(string AppPath) { System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); myProcess.StartInfo.FileName = AppPath; myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal; myProcess.StartInfo.CreateNoWindow = false; myProcess.StartInfo.UseShellExecute = false; myProcess.EnableRaisingEvents = false; myProcess.Start(); } } internal class MayaIntegration : DCCIntegration { public override string DccDisplayName { get { return "Maya"; } } public override string IntegrationZipPath { get { return "UnityFbxForMaya.7z"; } } private string FBX_EXPORT_SETTINGS_PATH { get { return "/Integrations/Autodesk/maya/scripts/unityFbxExportSettings.mel"; } } private string FBX_IMPORT_SETTINGS_PATH { get { return "/Integrations/Autodesk/maya/scripts/unityFbxImportSettings.mel"; } } private string MODULE_TEMPLATE_PATH { get { return "Integrations/Autodesk/maya/" + MODULE_FILENAME + ".txt"; } } private string MODULE_FILENAME { get { return "UnityFbxForMaya"; } } private const string PACKAGE_NAME = "com.unity.formats.fbx"; private const string VERSION_FIELD = "VERSION"; private const string VERSION_TAG = "{Version}"; private const string PROJECT_TAG = "{UnityProject}"; private const string INTEGRATION_TAG = "{UnityIntegrationsPath}"; private const string MAYA_USER_STARTUP_SCRIPT = "userSetup.mel"; private const string UI_SETUP_FUNCTION = "unitySetupUI"; private string USER_STARTUP_CALL { get { return string.Format("if(`exists {0}`){{ {0}; }}", UI_SETUP_FUNCTION); } } private static string MAYA_DOCUMENTS_PATH { get { switch (Application.platform) { case RuntimePlatform.WindowsEditor: return "maya"; case RuntimePlatform.OSXEditor: return "Library/Preferences/Autodesk/Maya"; default: throw new NotImplementedException(); } } } private static string MAYA_MODULES_PATH { get { return System.IO.Path.Combine(UserFolder, MAYA_DOCUMENTS_PATH + "/modules"); } } private static string MAYA_SCRIPTS_PATH { get { return System.IO.Path.Combine(UserFolder, MAYA_DOCUMENTS_PATH + "/scripts"); } } // Use string to define escaped quote // Windows needs the backslash protected static string EscapedQuote { get { switch (Application.platform) { case RuntimePlatform.WindowsEditor: return "\\\""; case RuntimePlatform.OSXEditor: return "\""; default: throw new NotSupportedException(); } } } protected string MayaConfigCommand { get { return string.Format("unityConfigure {0}{1}{0} {0}{2}{0} {0}{3}{0} {4} {5};", EscapedQuote, ProjectPath, ExportSettingsPath, ImportSettingsPath, (IsHeadlessInstall()), (HideSendToUnityMenu)); } } private string MAYA_CLOSE_COMMAND { get { return string.Format("scriptJob -idleEvent quit;"); } } protected static string UserFolder { get { switch (Application.platform) { case RuntimePlatform.WindowsEditor: return System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); case RuntimePlatform.OSXEditor: return System.Environment.GetEnvironmentVariable("HOME"); default: throw new NotSupportedException(); } } } public static int IsHeadlessInstall() { return 0; } public static int HideSendToUnityMenu { get { return ExportSettings.instance.HideSendToUnityMenuProperty ? 1 : 0; } } public string ModuleTemplatePath { get { return System.IO.Path.Combine(IntegrationFolderPath, MODULE_TEMPLATE_PATH); } } public static string PackagePath { get { return System.IO.Path.Combine(Application.dataPath, PACKAGE_NAME); } } /// /// Gets the path to the export settings file. /// Returns a relative path with forward slashes as path separators. /// /// The export settings path. public string ExportSettingsPath { get { return IntegrationFolderPath + FBX_EXPORT_SETTINGS_PATH; } } /// /// Gets the path to the import settings file. /// Returns a relative path with forward slashes as path separators. /// /// The import settings path. public string ImportSettingsPath { get { return IntegrationFolderPath + FBX_IMPORT_SETTINGS_PATH; } } /// /// Gets the user startup script path. /// Returns a relative path with forward slashes as path separators. /// /// The user startup script path. private static string GetUserStartupScriptPath() { return MAYA_SCRIPTS_PATH + "/" + MAYA_USER_STARTUP_SCRIPT; } public static string PackageVersion { get { return ModelExporter.GetVersionFromReadme(); } } private static List ParseTemplateFile(string FileName, Dictionary Tokens) { List lines = new List(); try { // Pass the file path and file name to the StreamReader constructor System.IO.StreamReader sr = new System.IO.StreamReader(FileName); // Read the first line of text string line = sr.ReadLine(); // Continue to read until you reach end of file while (line != null) { foreach (KeyValuePair entry in Tokens) { line = line.Replace(entry.Key, entry.Value); } lines.Add(line); //Read the next line line = sr.ReadLine(); } //close the file sr.Close(); } catch (Exception e) { Debug.LogError(string.Format("Exception reading module file template ({0})", e.Message)); } return lines; } private static void WriteFile(string FileName, List Lines) { try { //Pass the filepath and filename to the StreamWriter Constructor System.IO.StreamWriter sw = new System.IO.StreamWriter(FileName); foreach (string line in Lines) { //Write a line of text sw.WriteLine(line); } //Close the file sw.Close(); } catch (Exception e) { Debug.LogException(e); Debug.LogError(string.Format("Exception while writing module file ({0})", e.Message)); } } /// /// Creates the missing directories in path. /// /// true, if directory was created, false otherwise. /// Path to create. protected static bool CreateDirectory(string path) { try { System.IO.Directory.CreateDirectory(path); } catch (Exception xcp) { Debug.LogException(xcp); return false; } if (!System.IO.Directory.Exists(path)) { return false; } return true; } [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] public int ConfigureMaya(string mayaPath) { int ExitCode = 0; try { if (!System.IO.File.Exists(mayaPath)) { Debug.LogError(string.Format("No maya installation found at {0}", mayaPath)); return -1; } System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); myProcess.StartInfo.FileName = mayaPath; myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; myProcess.StartInfo.CreateNoWindow = true; myProcess.StartInfo.UseShellExecute = false; if (!ExportSettings.instance.LaunchAfterInstallation) { myProcess.StartInfo.RedirectStandardError = true; } string commandString; switch (Application.platform) { case RuntimePlatform.WindowsEditor: commandString = "-command \"{0}\""; break; case RuntimePlatform.OSXEditor: commandString = @"-command '{0}'"; break; default: throw new NotImplementedException(); } if (ExportSettings.instance.LaunchAfterInstallation) { myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal; myProcess.StartInfo.CreateNoWindow = false; myProcess.StartInfo.Arguments = string.Format(commandString, MayaConfigCommand); } else { myProcess.StartInfo.Arguments = string.Format(commandString, MayaConfigCommand + MAYA_CLOSE_COMMAND); } myProcess.EnableRaisingEvents = true; myProcess.Start(); if (!ExportSettings.instance.LaunchAfterInstallation) { string stderr = myProcess.StandardError.ReadToEnd(); myProcess.WaitForExit(); ExitCode = myProcess.ExitCode; Debug.Log(string.Format("Ran maya: [{0}]\nWith args [{1}]\nResult {2}", mayaPath, myProcess.StartInfo.Arguments, ExitCode)); // see if we got any error messages if (ExitCode != 0) { if (!string.IsNullOrEmpty(stderr)) { Debug.LogError(string.Format("Maya installation error (exit code: {0}): {1}", ExitCode, stderr)); } } } else { ExitCode = 0; } } catch (Exception e) { UnityEngine.Debug.LogError(string.Format("Exception failed to start Maya ({0})", e.Message)); ExitCode = -1; } return ExitCode; } public bool InstallMaya(bool verbose = false) { // What's happening here is that we copy the module template to // the module path, basically: // - copy the template to the user Maya module path // - search-and-replace its tags // - done. // But it's complicated because we can't trust any files actually exist. string moduleTemplatePath = ModuleTemplatePath; if (!System.IO.File.Exists(moduleTemplatePath)) { Debug.LogError(string.Format("Missing Maya module file at: \"{0}\"", moduleTemplatePath)); return false; } // Create the {USER} modules folder and empty it so it's ready to set up. string modulePath = MAYA_MODULES_PATH; string moduleFilePath = System.IO.Path.Combine(modulePath, MODULE_FILENAME + ".mod"); bool installed = false; if (!System.IO.Directory.Exists(modulePath)) { if (verbose) { Debug.Log(string.Format("Creating Maya Modules Folder {0}", modulePath)); } if (!CreateDirectory(modulePath)) { Debug.LogError(string.Format("Failed to create Maya Modules Folder {0}", modulePath)); return false; } installed = false; } else { // detect if UnityFbxForMaya.mod is installed installed = System.IO.File.Exists(moduleFilePath); if (installed) { // (Uni-31606): remove this when we support parsing existing .mod files try { if (verbose) { Debug.Log(string.Format("Deleting module file {0}", moduleFilePath)); } System.IO.File.Delete(moduleFilePath); installed = false; } catch (Exception xcp) { Debug.LogException(xcp); Debug.LogWarning(string.Format("Failed to delete plugin module file {0}", moduleFilePath)); } } } // if not installed if (!installed) { Dictionary Tokens = new Dictionary() { {VERSION_TAG, PackageVersion }, {PROJECT_TAG, ProjectPath }, {INTEGRATION_TAG, IntegrationFolderPath }, }; // parse template, replace "{UnityProject}" with project path List lines = ParseTemplateFile(moduleTemplatePath, Tokens); if (verbose) Debug.Log(string.Format("Copying plugin module file to {0}", moduleFilePath)); // write out .mod file WriteFile(moduleFilePath, lines); } else { throw new NotImplementedException(); // (Uni-31606) Parse maya mod file during installation and find location } return SetupUserStartupScript(verbose); } private bool SetupUserStartupScript(bool verbose = false) { // setup user startup script string mayaStartupScript = GetUserStartupScriptPath(); string fileContents = string.Format("\n{0}", USER_STARTUP_CALL); // make sure scripts directory exists if (!System.IO.Directory.Exists(MAYA_SCRIPTS_PATH)) { if (verbose) { Debug.Log(string.Format("Creating Maya Scripts Folder {0}", MAYA_SCRIPTS_PATH)); } if (!CreateDirectory(MAYA_SCRIPTS_PATH)) { Debug.LogError(string.Format("Failed to create Maya Scripts Folder {0}", MAYA_SCRIPTS_PATH)); return false; } } else if (System.IO.File.Exists(mayaStartupScript)) { // script exists, check that the UI setup is being called try { using (System.IO.StreamReader sr = new System.IO.StreamReader(mayaStartupScript)) { while (sr.Peek() >= 0) { string line = sr.ReadLine(); if (line.Trim().Contains(UI_SETUP_FUNCTION)) { // startup call already in the file, nothing to do return true; } } } } catch (Exception e) { Debug.LogException(e); Debug.LogError(string.Format("Exception while reading user startup file ({0})", e.Message)); return false; } } // append text to file try { System.IO.File.AppendAllText(mayaStartupScript, fileContents); } catch (Exception e) { Debug.LogException(e); Debug.LogError(string.Format("Exception while writing to user startup file ({0})", e.Message)); return false; } return true; } [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] public override int InstallIntegration(string exe) { if (!InstallMaya(verbose: true)) { return -1; } return ConfigureMaya(exe); } /// /// Determines if folder is already unzipped at the specified path /// by checking if UnityFbxForMaya.mod exists at expected location. /// /// true if folder is already unzipped at the specified path; otherwise, false. /// Path. public override bool FolderAlreadyUnzippedAtPath(string path) { if (string.IsNullOrEmpty(path)) { return false; } return System.IO.File.Exists(System.IO.Path.Combine(path, MODULE_TEMPLATE_PATH)); } } internal class MayaLTIntegration : MayaIntegration { public override string DccDisplayName { get { return "Maya LT"; } } } internal class MaxIntegration : DCCIntegration { public override string DccDisplayName { get { return "3Ds Max"; } } private const string MaxScriptsPath = "Integrations/Autodesk/max/scripts/"; private const string PluginName = "UnityFbxForMaxPlugin.ms"; public const string PluginPath = MaxScriptsPath + PluginName; private const string ConfigureMaxScript = MaxScriptsPath + "configureUnityFbxForMax.ms"; private const string ExportSettingsFile = MaxScriptsPath + "unityFbxExportSettings.ms"; private const string ImportSettingsFile = MaxScriptsPath + "unityFbxImportSettings.ms"; private const string PluginSourceTag = "UnityPluginScript_Source"; private const string PluginNameTag = "UnityPluginScript_Name"; private const string ProjectTag = "UnityProject"; private const string ExportSettingsTag = "UnityFbxExportSettings"; private const string ImportSettingsTag = "UnityFbxImportSettings"; public override string IntegrationZipPath { get { return "UnityFbxForMax.7z"; } } /// /// Gets the absolute Unity path for relative path in Integrations folder. /// /// The absolute path. /// Relative path. public static string GetAbsPath(string relPath) { return MayaIntegration.IntegrationFolderPath + "/" + relPath; } private static string GetInstallScript() { Dictionary Tokens = new Dictionary() { {PluginSourceTag, GetAbsPath(PluginPath) }, {PluginNameTag, PluginName }, {ProjectTag, ProjectPath }, {ExportSettingsTag, GetAbsPath(ExportSettingsFile) }, {ImportSettingsTag, GetAbsPath(ImportSettingsFile) } }; var installScript = ""; // setup the variables to be used in the configure max script foreach (var t in Tokens) { installScript += string.Format(@"global {0} = @\""{1}\"";", t.Key, t.Value); } installScript += string.Format(@"filein \""{0}\""", GetAbsPath(ConfigureMaxScript)); return installScript; } [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] public static int InstallMaxPlugin(string maxExe) { if (Application.platform != RuntimePlatform.WindowsEditor) { Debug.LogError("The 3DsMax Unity plugin is Windows only, please try installing a Maya plugin instead"); return -1; } var installScript = GetInstallScript(); int ExitCode = 0; try { if (!System.IO.File.Exists(maxExe)) { Debug.LogError(string.Format("No 3DsMax installation found at {0}", maxExe)); return -1; } System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); myProcess.StartInfo.FileName = maxExe; myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; myProcess.StartInfo.CreateNoWindow = true; myProcess.StartInfo.UseShellExecute = false; myProcess.StartInfo.RedirectStandardOutput = true; myProcess.StartInfo.Arguments = string.Format("-q -silent -mxs \"{0}\"", installScript); myProcess.EnableRaisingEvents = true; myProcess.Start(); string stderr = myProcess.StandardOutput.ReadToEnd(); myProcess.WaitForExit(); ExitCode = myProcess.ExitCode; if (ExportSettings.instance.LaunchAfterInstallation) { LaunchDCCApplication(maxExe); } // TODO (UNI-29910): figure out what exactly causes this exit code + how to resolve if (ExitCode == -1073740791) { Debug.Log(string.Format("Detected 3ds max exitcode {0} -- safe to ignore", ExitCode)); ExitCode = 0; } // print any errors if (ExitCode != 0) { if (!string.IsNullOrEmpty(stderr)) { Debug.LogError(string.Format("3ds Max installation error (exit code: {0}): {1}", ExitCode, stderr)); } } Debug.Log(string.Format("Ran max: [{0}]\nWith args [{1}]\nResult {2}", maxExe, myProcess.StartInfo.Arguments, ExitCode)); } catch (Exception e) { UnityEngine.Debug.LogError(string.Format("Exception failed to start Max ({0})", e.Message)); ExitCode = -1; } return ExitCode; } [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] public override int InstallIntegration(string exe) { return MaxIntegration.InstallMaxPlugin(exe); } /// /// Determines if folder is already unzipped at the specified path /// by checking if plugin exists at expected location. /// /// true if folder is already unzipped at the specified path; otherwise, false. /// Path. public override bool FolderAlreadyUnzippedAtPath(string path) { if (string.IsNullOrEmpty(path)) { return false; } return System.IO.File.Exists(System.IO.Path.Combine(path, MaxIntegration.PluginPath)); } } static class IntegrationsUI { /// /// The path of the DCC executable. /// public static string GetDCCExe() { return ExportSettings.SelectedDCCPath; } /// /// Gets the name of the selected DCC. /// /// The DCC name. public static string GetDCCName() { return ExportSettings.SelectedDCCName; } /// /// Opens a dialog showing whether the installation succeeded. /// /// Dcc name. private static void ShowSuccessDialog(string dcc, int exitCode) { string title, message, customMessage; if (exitCode != 0) { title = string.Format("Failed to install {0} Integration.", dcc); message = string.Format("Failed to configure {0}, please check logs (exitcode={1}).", dcc, exitCode); } else { if (ExportSettings.instance.LaunchAfterInstallation) { customMessage = "Installing Unity menu in {0}, application will open once installation is complete"; } else { customMessage = "Enjoy the new Unity menu in {0}."; } title = string.Format("Completing installation of {0} Integration.", dcc); message = string.Format(customMessage, dcc); } UnityEditor.EditorUtility.DisplayDialog(title, message, "Ok"); } public static void InstallDCCIntegration() { var dccExe = GetDCCExe(); if (string.IsNullOrEmpty(dccExe)) { return; } string dccType = System.IO.Path.GetFileNameWithoutExtension(dccExe).ToLower(); DCCIntegration dccIntegration; if (dccType.Equals("maya")) { // could be Maya or Maya LT if (GetDCCName().ToLower().Contains("lt")) { dccIntegration = new MayaLTIntegration(); } else { dccIntegration = new MayaIntegration(); } } else if (dccType.Equals("3dsmax")) { dccIntegration = new MaxIntegration(); } else { throw new System.NotImplementedException(); } if (!GetIntegrationFolder(dccIntegration)) { // failed to get integration folder return; } int exitCode = dccIntegration.InstallIntegration(dccExe); ShowSuccessDialog(dccIntegration.DccDisplayName, exitCode); } private static bool GetIntegrationFolder(DCCIntegration dcc) { // decompress zip file if it exists, otherwise try using default location var zipPath = dcc.IntegrationZipFullPath; if (System.IO.File.Exists(zipPath)) { return DecompressIntegrationZipFile(zipPath, dcc); } dcc.SetIntegrationFolderPath(ExportSettings.IntegrationSavePath); return true; } private static bool DecompressIntegrationZipFile(string zipPath, DCCIntegration dcc) { // prompt user to enter location to unzip file var unzipFolder = EditorUtility.OpenFolderPanel(string.Format("Select Location to Save {0} Integration", dcc.DccDisplayName), ExportSettings.IntegrationSavePath, ""); if (string.IsNullOrEmpty(unzipFolder)) { // user has cancelled, do nothing return false; } ExportSettings.IntegrationSavePath = unzipFolder; // check that this is a valid location to unzip the file if (!DirectoryHasWritePermission(unzipFolder)) { // display dialog to try again or cancel var result = UnityEditor.EditorUtility.DisplayDialog("No Write Permission", string.Format("Directory \"{0}\" does not have write access", unzipFolder), "Select another Directory", "Cancel" ); if (result) { InstallDCCIntegration(); } else { return false; } } // if file already unzipped in this location, then prompt user // if they would like to continue unzipping or use what is there if (dcc.FolderAlreadyUnzippedAtPath(unzipFolder)) { var result = UnityEditor.EditorUtility.DisplayDialogComplex("Integrations Exist at Path", string.Format("Directory \"{0}\" already contains the decompressed integration", unzipFolder), "Overwrite", "Use Existing", "Cancel" ); if (result == 0) { DecompressZip(zipPath, unzipFolder); } else if (result == 2) { return false; } } else { // unzip Integration folder DecompressZip(zipPath, unzipFolder); } dcc.SetIntegrationFolderPath(unzipFolder); return true; } /// /// Make sure we can write to this directory. /// Try creating a file in path directory, if it raises an error, then we can't /// write here. /// TODO: find a more reliable way to check this /// /// true, if possible to write to path, false otherwise. /// Path. public static bool DirectoryHasWritePermission(string path) { try { using (System.IO.FileStream fs = System.IO.File.Create( System.IO.Path.Combine( path, System.IO.Path.GetRandomFileName() ), 1, System.IO.FileOptions.DeleteOnClose) ) {} return true; } catch (Exception) { return false; } } public static void DecompressZip(string zipPath, string destPath) { System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); string zipApp; switch (Application.platform) { case RuntimePlatform.WindowsEditor: zipApp = "7z.exe"; break; case RuntimePlatform.OSXEditor: case RuntimePlatform.LinuxEditor: zipApp = "7za"; break; default: throw new NotImplementedException(); } myProcess.StartInfo.FileName = EditorApplication.applicationContentsPath + "/Tools/" + zipApp; myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; myProcess.StartInfo.CreateNoWindow = true; myProcess.StartInfo.UseShellExecute = false; // Command line flags used: // x : extract the zip contents so that they maintain the file hierarchy // -o : specify where to extract contents // -r : recurse subdirectories // -y : auto yes to all questions (without this Unity freezes as the process waits for a response) myProcess.StartInfo.Arguments = string.Format("x \"{0}\" -o\"{1}\" -r -y", zipPath, destPath); myProcess.EnableRaisingEvents = true; myProcess.Start(); myProcess.WaitForExit(); // in case we unzip inside the Assets folder, make sure it updates AssetDatabase.Refresh(); } } }