InstallIntegration.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  1. using UnityEngine;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Security.Permissions;
  5. namespace UnityEditor.Formats.Fbx.Exporter
  6. {
  7. internal abstract class DCCIntegration
  8. {
  9. public abstract string DccDisplayName { get; }
  10. public abstract string IntegrationZipPath { get; }
  11. private static string s_integrationFolderPath = null;
  12. public static string IntegrationFolderPath
  13. {
  14. get{
  15. if (string.IsNullOrEmpty (s_integrationFolderPath)) {
  16. s_integrationFolderPath = Application.dataPath;
  17. }
  18. return s_integrationFolderPath;
  19. }
  20. set{
  21. if (!string.IsNullOrEmpty (value) && System.IO.Directory.Exists (value)) {
  22. s_integrationFolderPath = value;
  23. } else {
  24. Debug.LogError (string.Format("Failed to set integration folder path, invalid directory \"{0}\"", value));
  25. }
  26. }
  27. }
  28. public void SetIntegrationFolderPath(string path){
  29. IntegrationFolderPath = path;
  30. }
  31. /// <summary>
  32. /// Gets the integration zip full path as an absolute Unity-style path.
  33. /// </summary>
  34. /// <returns>The integration zip full path.</returns>
  35. public string IntegrationZipFullPath
  36. {
  37. get
  38. {
  39. return System.IO.Path.GetFullPath("Packages/com.unity.formats.fbx/Editor/Integrations~").Replace("\\", "/") + "/" + IntegrationZipPath;
  40. }
  41. }
  42. /// <summary>
  43. /// Gets the project path.
  44. /// </summary>
  45. /// <returns>The project path.</returns>
  46. public static string ProjectPath
  47. {
  48. get
  49. {
  50. return System.IO.Directory.GetParent(Application.dataPath).FullName.Replace("\\", "/");
  51. }
  52. }
  53. /// <summary>
  54. /// Installs the integration using the provided executable.
  55. /// </summary>
  56. /// <returns>The integration.</returns>
  57. /// <param name="exe">Exe.</param>
  58. [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  59. [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  60. public abstract int InstallIntegration(string exe);
  61. /// <summary>
  62. /// Determines if folder is already unzipped at the specified path.
  63. /// </summary>
  64. /// <returns><c>true</c> if folder is already unzipped at the specified path; otherwise, <c>false</c>.</returns>
  65. /// <param name="path">Path.</param>
  66. public abstract bool FolderAlreadyUnzippedAtPath (string path);
  67. /// <summary>
  68. /// Launches application at given path
  69. /// </summary>
  70. /// <param name="AppPath"></param>
  71. [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  72. [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  73. public static void LaunchDCCApplication(string AppPath)
  74. {
  75. System.Diagnostics.Process myProcess = new System.Diagnostics.Process();
  76. myProcess.StartInfo.FileName = AppPath;
  77. myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
  78. myProcess.StartInfo.CreateNoWindow = false;
  79. myProcess.StartInfo.UseShellExecute = false;
  80. myProcess.EnableRaisingEvents = false;
  81. myProcess.Start();
  82. }
  83. }
  84. internal class MayaIntegration : DCCIntegration
  85. {
  86. public override string DccDisplayName { get { return "Maya"; } }
  87. public override string IntegrationZipPath { get { return "UnityFbxForMaya.7z"; } }
  88. private string FBX_EXPORT_SETTINGS_PATH { get { return "/Integrations/Autodesk/maya/scripts/unityFbxExportSettings.mel"; } }
  89. private string FBX_IMPORT_SETTINGS_PATH { get { return "/Integrations/Autodesk/maya/scripts/unityFbxImportSettings.mel"; } }
  90. private string MODULE_TEMPLATE_PATH { get { return "Integrations/Autodesk/maya/" + MODULE_FILENAME + ".txt"; } }
  91. private string MODULE_FILENAME { get { return "UnityFbxForMaya"; } }
  92. private const string PACKAGE_NAME = "com.unity.formats.fbx";
  93. private const string VERSION_FIELD = "VERSION";
  94. private const string VERSION_TAG = "{Version}";
  95. private const string PROJECT_TAG = "{UnityProject}";
  96. private const string INTEGRATION_TAG = "{UnityIntegrationsPath}";
  97. private const string MAYA_USER_STARTUP_SCRIPT = "userSetup.mel";
  98. private const string UI_SETUP_FUNCTION = "unitySetupUI";
  99. private string USER_STARTUP_CALL { get { return string.Format ("if(`exists {0}`){{ {0}; }}", UI_SETUP_FUNCTION); } }
  100. private static string MAYA_DOCUMENTS_PATH {
  101. get {
  102. switch (Application.platform) {
  103. case RuntimePlatform.WindowsEditor:
  104. return "maya";
  105. case RuntimePlatform.OSXEditor:
  106. return "Library/Preferences/Autodesk/Maya";
  107. default:
  108. throw new NotImplementedException ();
  109. }
  110. }
  111. }
  112. private static string MAYA_MODULES_PATH {
  113. get {
  114. return System.IO.Path.Combine(UserFolder, MAYA_DOCUMENTS_PATH + "/modules");
  115. }
  116. }
  117. private static string MAYA_SCRIPTS_PATH {
  118. get {
  119. return System.IO.Path.Combine(UserFolder, MAYA_DOCUMENTS_PATH + "/scripts");
  120. }
  121. }
  122. // Use string to define escaped quote
  123. // Windows needs the backslash
  124. protected static string EscapedQuote {
  125. get {
  126. switch (Application.platform) {
  127. case RuntimePlatform.WindowsEditor:
  128. return "\\\"";
  129. case RuntimePlatform.OSXEditor:
  130. return "\"";
  131. default:
  132. throw new NotSupportedException ();
  133. }
  134. }
  135. }
  136. protected string MayaConfigCommand { get {
  137. return string.Format("unityConfigure {0}{1}{0} {0}{2}{0} {0}{3}{0} {4} {5};",
  138. EscapedQuote, ProjectPath, ExportSettingsPath, ImportSettingsPath, (IsHeadlessInstall()), (HideSendToUnityMenu));
  139. }}
  140. private string MAYA_CLOSE_COMMAND { get {
  141. return string.Format("scriptJob -idleEvent quit;");
  142. }}
  143. protected static string UserFolder
  144. {
  145. get
  146. {
  147. switch (Application.platform)
  148. {
  149. case RuntimePlatform.WindowsEditor:
  150. return System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
  151. case RuntimePlatform.OSXEditor:
  152. return System.Environment.GetEnvironmentVariable("HOME");
  153. default:
  154. throw new NotSupportedException();
  155. }
  156. }
  157. }
  158. public static int IsHeadlessInstall ()
  159. {
  160. return 0;
  161. }
  162. public static int HideSendToUnityMenu
  163. {
  164. get{
  165. return ExportSettings.instance.HideSendToUnityMenuProperty?1:0;
  166. }
  167. }
  168. public string ModuleTemplatePath
  169. {
  170. get
  171. {
  172. return System.IO.Path.Combine(IntegrationFolderPath, MODULE_TEMPLATE_PATH);
  173. }
  174. }
  175. public static string PackagePath
  176. {
  177. get
  178. {
  179. return System.IO.Path.Combine(Application.dataPath, PACKAGE_NAME);
  180. }
  181. }
  182. /// <summary>
  183. /// Gets the path to the export settings file.
  184. /// Returns a relative path with forward slashes as path separators.
  185. /// </summary>
  186. /// <returns>The export settings path.</returns>
  187. public string ExportSettingsPath
  188. {
  189. get
  190. {
  191. return IntegrationFolderPath + FBX_EXPORT_SETTINGS_PATH;
  192. }
  193. }
  194. /// <summary>
  195. /// Gets the path to the import settings file.
  196. /// Returns a relative path with forward slashes as path separators.
  197. /// </summary>
  198. /// <returns>The import settings path.</returns>
  199. public string ImportSettingsPath{
  200. get
  201. {
  202. return IntegrationFolderPath + FBX_IMPORT_SETTINGS_PATH;
  203. }
  204. }
  205. /// <summary>
  206. /// Gets the user startup script path.
  207. /// Returns a relative path with forward slashes as path separators.
  208. /// </summary>
  209. /// <returns>The user startup script path.</returns>
  210. private static string GetUserStartupScriptPath(){
  211. return MAYA_SCRIPTS_PATH + "/" + MAYA_USER_STARTUP_SCRIPT;
  212. }
  213. public static string PackageVersion
  214. {
  215. get
  216. {
  217. return ModelExporter.GetVersionFromReadme();
  218. }
  219. }
  220. private static List<string> ParseTemplateFile(string FileName, Dictionary<string,string> Tokens )
  221. {
  222. List<string> lines = new List<string>();
  223. try
  224. {
  225. // Pass the file path and file name to the StreamReader constructor
  226. System.IO.StreamReader sr = new System.IO.StreamReader(FileName);
  227. // Read the first line of text
  228. string line = sr.ReadLine();
  229. // Continue to read until you reach end of file
  230. while (line != null)
  231. {
  232. foreach(KeyValuePair<string, string> entry in Tokens)
  233. {
  234. line = line.Replace(entry.Key, entry.Value);
  235. }
  236. lines.Add(line);
  237. //Read the next line
  238. line = sr.ReadLine();
  239. }
  240. //close the file
  241. sr.Close();
  242. }
  243. catch(Exception e)
  244. {
  245. Debug.LogError(string.Format("Exception reading module file template ({0})", e.Message));
  246. }
  247. return lines;
  248. }
  249. private static void WriteFile(string FileName, List<string> Lines )
  250. {
  251. try
  252. {
  253. //Pass the filepath and filename to the StreamWriter Constructor
  254. System.IO.StreamWriter sw = new System.IO.StreamWriter(FileName);
  255. foreach (string line in Lines)
  256. {
  257. //Write a line of text
  258. sw.WriteLine(line);
  259. }
  260. //Close the file
  261. sw.Close();
  262. }
  263. catch(Exception e)
  264. {
  265. Debug.LogException(e);
  266. Debug.LogError(string.Format("Exception while writing module file ({0})", e.Message));
  267. }
  268. }
  269. /// <summary>
  270. /// Creates the missing directories in path.
  271. /// </summary>
  272. /// <returns><c>true</c>, if directory was created, <c>false</c> otherwise.</returns>
  273. /// <param name="path">Path to create.</param>
  274. protected static bool CreateDirectory(string path){
  275. try
  276. {
  277. System.IO.Directory.CreateDirectory(path);
  278. }
  279. catch (Exception xcp)
  280. {
  281. Debug.LogException(xcp);
  282. return false;
  283. }
  284. if (!System.IO.Directory.Exists(path)) {
  285. return false;
  286. }
  287. return true;
  288. }
  289. [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  290. [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  291. public int ConfigureMaya(string mayaPath)
  292. {
  293. int ExitCode = 0;
  294. try {
  295. if (!System.IO.File.Exists(mayaPath))
  296. {
  297. Debug.LogError (string.Format ("No maya installation found at {0}", mayaPath));
  298. return -1;
  299. }
  300. System.Diagnostics.Process myProcess = new System.Diagnostics.Process();
  301. myProcess.StartInfo.FileName = mayaPath;
  302. myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
  303. myProcess.StartInfo.CreateNoWindow = true;
  304. myProcess.StartInfo.UseShellExecute = false;
  305. if (!ExportSettings.instance.LaunchAfterInstallation)
  306. {
  307. myProcess.StartInfo.RedirectStandardError = true;
  308. }
  309. string commandString;
  310. switch (Application.platform) {
  311. case RuntimePlatform.WindowsEditor:
  312. commandString = "-command \"{0}\"";
  313. break;
  314. case RuntimePlatform.OSXEditor:
  315. commandString = @"-command '{0}'";
  316. break;
  317. default:
  318. throw new NotImplementedException ();
  319. }
  320. if (ExportSettings.instance.LaunchAfterInstallation)
  321. {
  322. myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
  323. myProcess.StartInfo.CreateNoWindow = false;
  324. myProcess.StartInfo.Arguments = string.Format(commandString, MayaConfigCommand);
  325. }
  326. else
  327. {
  328. myProcess.StartInfo.Arguments = string.Format(commandString, MayaConfigCommand + MAYA_CLOSE_COMMAND);
  329. }
  330. myProcess.EnableRaisingEvents = true;
  331. myProcess.Start();
  332. if (!ExportSettings.instance.LaunchAfterInstallation)
  333. {
  334. string stderr = myProcess.StandardError.ReadToEnd();
  335. myProcess.WaitForExit();
  336. ExitCode = myProcess.ExitCode;
  337. Debug.Log(string.Format("Ran maya: [{0}]\nWith args [{1}]\nResult {2}",
  338. mayaPath, myProcess.StartInfo.Arguments, ExitCode));
  339. // see if we got any error messages
  340. if(ExitCode != 0){
  341. if(!string.IsNullOrEmpty(stderr)){
  342. Debug.LogError(string.Format("Maya installation error (exit code: {0}): {1}", ExitCode, stderr));
  343. }
  344. }
  345. }
  346. else
  347. {
  348. ExitCode = 0;
  349. }
  350. }
  351. catch (Exception e)
  352. {
  353. UnityEngine.Debug.LogError(string.Format ("Exception failed to start Maya ({0})", e.Message));
  354. ExitCode = -1;
  355. }
  356. return ExitCode;
  357. }
  358. public bool InstallMaya(bool verbose = false)
  359. {
  360. // What's happening here is that we copy the module template to
  361. // the module path, basically:
  362. // - copy the template to the user Maya module path
  363. // - search-and-replace its tags
  364. // - done.
  365. // But it's complicated because we can't trust any files actually exist.
  366. string moduleTemplatePath = ModuleTemplatePath;
  367. if (!System.IO.File.Exists(moduleTemplatePath))
  368. {
  369. Debug.LogError(string.Format("Missing Maya module file at: \"{0}\"", moduleTemplatePath));
  370. return false;
  371. }
  372. // Create the {USER} modules folder and empty it so it's ready to set up.
  373. string modulePath = MAYA_MODULES_PATH;
  374. string moduleFilePath = System.IO.Path.Combine(modulePath, MODULE_FILENAME + ".mod");
  375. bool installed = false;
  376. if (!System.IO.Directory.Exists(modulePath))
  377. {
  378. if (verbose) { Debug.Log(string.Format("Creating Maya Modules Folder {0}", modulePath)); }
  379. if (!CreateDirectory (modulePath)) {
  380. Debug.LogError(string.Format("Failed to create Maya Modules Folder {0}", modulePath));
  381. return false;
  382. }
  383. installed = false;
  384. }
  385. else
  386. {
  387. // detect if UnityFbxForMaya.mod is installed
  388. installed = System.IO.File.Exists(moduleFilePath);
  389. if (installed)
  390. {
  391. // (Uni-31606): remove this when we support parsing existing .mod files
  392. try
  393. {
  394. if (verbose) { Debug.Log(string.Format("Deleting module file {0}", moduleFilePath)); }
  395. System.IO.File.Delete(moduleFilePath);
  396. installed = false;
  397. }
  398. catch (Exception xcp)
  399. {
  400. Debug.LogException(xcp);
  401. Debug.LogWarning(string.Format ("Failed to delete plugin module file {0}", moduleFilePath));
  402. }
  403. }
  404. }
  405. // if not installed
  406. if (!installed)
  407. {
  408. Dictionary<string,string> Tokens = new Dictionary<string,string>()
  409. {
  410. {VERSION_TAG, PackageVersion },
  411. {PROJECT_TAG, ProjectPath },
  412. {INTEGRATION_TAG, IntegrationFolderPath },
  413. };
  414. // parse template, replace "{UnityProject}" with project path
  415. List<string> lines = ParseTemplateFile(moduleTemplatePath, Tokens);
  416. if (verbose) Debug.Log(string.Format("Copying plugin module file to {0}", moduleFilePath));
  417. // write out .mod file
  418. WriteFile(moduleFilePath, lines);
  419. }
  420. else
  421. {
  422. throw new NotImplementedException();
  423. // (Uni-31606) Parse maya mod file during installation and find location
  424. }
  425. return SetupUserStartupScript (verbose);
  426. }
  427. private bool SetupUserStartupScript(bool verbose = false){
  428. // setup user startup script
  429. string mayaStartupScript = GetUserStartupScriptPath ();
  430. string fileContents = string.Format("\n{0}", USER_STARTUP_CALL);
  431. // make sure scripts directory exists
  432. if (!System.IO.Directory.Exists(MAYA_SCRIPTS_PATH))
  433. {
  434. if (verbose) { Debug.Log(string.Format("Creating Maya Scripts Folder {0}", MAYA_SCRIPTS_PATH)); }
  435. if (!CreateDirectory (MAYA_SCRIPTS_PATH)) {
  436. Debug.LogError(string.Format("Failed to create Maya Scripts Folder {0}", MAYA_SCRIPTS_PATH));
  437. return false;
  438. }
  439. }
  440. else if (System.IO.File.Exists (mayaStartupScript)) {
  441. // script exists, check that the UI setup is being called
  442. try{
  443. using (System.IO.StreamReader sr = new System.IO.StreamReader (mayaStartupScript)) {
  444. while (sr.Peek () >= 0) {
  445. string line = sr.ReadLine ();
  446. if (line.Trim().Contains (UI_SETUP_FUNCTION)) {
  447. // startup call already in the file, nothing to do
  448. return true;
  449. }
  450. }
  451. }
  452. }
  453. catch(Exception e){
  454. Debug.LogException(e);
  455. Debug.LogError(string.Format("Exception while reading user startup file ({0})", e.Message));
  456. return false;
  457. }
  458. }
  459. // append text to file
  460. try{
  461. System.IO.File.AppendAllText (mayaStartupScript, fileContents);
  462. }
  463. catch(Exception e)
  464. {
  465. Debug.LogException(e);
  466. Debug.LogError(string.Format("Exception while writing to user startup file ({0})", e.Message));
  467. return false;
  468. }
  469. return true;
  470. }
  471. [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  472. [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  473. public override int InstallIntegration (string exe)
  474. {
  475. if (!InstallMaya(verbose: true)) {
  476. return -1;
  477. }
  478. return ConfigureMaya (exe);
  479. }
  480. /// <summary>
  481. /// Determines if folder is already unzipped at the specified path
  482. /// by checking if UnityFbxForMaya.mod exists at expected location.
  483. /// </summary>
  484. /// <returns><c>true</c> if folder is already unzipped at the specified path; otherwise, <c>false</c>.</returns>
  485. /// <param name="path">Path.</param>
  486. public override bool FolderAlreadyUnzippedAtPath(string path)
  487. {
  488. if (string.IsNullOrEmpty (path)) {
  489. return false;
  490. }
  491. return System.IO.File.Exists (System.IO.Path.Combine (path, MODULE_TEMPLATE_PATH));
  492. }
  493. }
  494. internal class MayaLTIntegration : MayaIntegration
  495. {
  496. public override string DccDisplayName { get { return "Maya LT"; } }
  497. }
  498. internal class MaxIntegration : DCCIntegration
  499. {
  500. public override string DccDisplayName { get { return "3Ds Max"; } }
  501. private const string MaxScriptsPath = "Integrations/Autodesk/max/scripts/";
  502. private const string PluginName = "UnityFbxForMaxPlugin.ms";
  503. public const string PluginPath = MaxScriptsPath + PluginName;
  504. private const string ConfigureMaxScript = MaxScriptsPath + "configureUnityFbxForMax.ms";
  505. private const string ExportSettingsFile = MaxScriptsPath + "unityFbxExportSettings.ms";
  506. private const string ImportSettingsFile = MaxScriptsPath + "unityFbxImportSettings.ms";
  507. private const string PluginSourceTag = "UnityPluginScript_Source";
  508. private const string PluginNameTag = "UnityPluginScript_Name";
  509. private const string ProjectTag = "UnityProject";
  510. private const string ExportSettingsTag = "UnityFbxExportSettings";
  511. private const string ImportSettingsTag = "UnityFbxImportSettings";
  512. public override string IntegrationZipPath { get { return "UnityFbxForMax.7z"; } }
  513. /// <summary>
  514. /// Gets the absolute Unity path for relative path in Integrations folder.
  515. /// </summary>
  516. /// <returns>The absolute path.</returns>
  517. /// <param name="relPath">Relative path.</param>
  518. public static string GetAbsPath(string relPath){
  519. return MayaIntegration.IntegrationFolderPath + "/" + relPath;
  520. }
  521. private static string GetInstallScript(){
  522. Dictionary<string,string> Tokens = new Dictionary<string,string>()
  523. {
  524. {PluginSourceTag, GetAbsPath(PluginPath) },
  525. {PluginNameTag, PluginName },
  526. {ProjectTag, ProjectPath },
  527. {ExportSettingsTag, GetAbsPath(ExportSettingsFile) },
  528. {ImportSettingsTag, GetAbsPath(ImportSettingsFile) }
  529. };
  530. var installScript = "";
  531. // setup the variables to be used in the configure max script
  532. foreach (var t in Tokens) {
  533. installScript += string.Format (@"global {0} = @\""{1}\"";", t.Key, t.Value);
  534. }
  535. installScript += string.Format(@"filein \""{0}\""", GetAbsPath(ConfigureMaxScript));
  536. return installScript;
  537. }
  538. [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  539. [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  540. public static int InstallMaxPlugin(string maxExe){
  541. if (Application.platform != RuntimePlatform.WindowsEditor) {
  542. Debug.LogError ("The 3DsMax Unity plugin is Windows only, please try installing a Maya plugin instead");
  543. return -1;
  544. }
  545. var installScript = GetInstallScript ();
  546. int ExitCode = 0;
  547. try {
  548. if (!System.IO.File.Exists(maxExe))
  549. {
  550. Debug.LogError (string.Format ("No 3DsMax installation found at {0}", maxExe));
  551. return -1;
  552. }
  553. System.Diagnostics.Process myProcess = new System.Diagnostics.Process();
  554. myProcess.StartInfo.FileName = maxExe;
  555. myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
  556. myProcess.StartInfo.CreateNoWindow = true;
  557. myProcess.StartInfo.UseShellExecute = false;
  558. myProcess.StartInfo.RedirectStandardOutput = true;
  559. myProcess.StartInfo.Arguments = string.Format("-q -silent -mxs \"{0}\"", installScript);
  560. myProcess.EnableRaisingEvents = true;
  561. myProcess.Start();
  562. string stderr = myProcess.StandardOutput.ReadToEnd();
  563. myProcess.WaitForExit();
  564. ExitCode = myProcess.ExitCode;
  565. if (ExportSettings.instance.LaunchAfterInstallation)
  566. {
  567. LaunchDCCApplication(maxExe);
  568. }
  569. // TODO (UNI-29910): figure out what exactly causes this exit code + how to resolve
  570. if (ExitCode == -1073740791){
  571. Debug.Log(string.Format("Detected 3ds max exitcode {0} -- safe to ignore", ExitCode));
  572. ExitCode = 0;
  573. }
  574. // print any errors
  575. if(ExitCode != 0){
  576. if(!string.IsNullOrEmpty(stderr)){
  577. Debug.LogError(string.Format("3ds Max installation error (exit code: {0}): {1}", ExitCode, stderr));
  578. }
  579. }
  580. Debug.Log(string.Format("Ran max: [{0}]\nWith args [{1}]\nResult {2}",
  581. maxExe, myProcess.StartInfo.Arguments, ExitCode));
  582. }
  583. catch (Exception e)
  584. {
  585. UnityEngine.Debug.LogError(string.Format ("Exception failed to start Max ({0})", e.Message));
  586. ExitCode = -1;
  587. }
  588. return ExitCode;
  589. }
  590. [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  591. [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  592. public override int InstallIntegration(string exe){
  593. return MaxIntegration.InstallMaxPlugin (exe);
  594. }
  595. /// <summary>
  596. /// Determines if folder is already unzipped at the specified path
  597. /// by checking if plugin exists at expected location.
  598. /// </summary>
  599. /// <returns><c>true</c> if folder is already unzipped at the specified path; otherwise, <c>false</c>.</returns>
  600. /// <param name="path">Path.</param>
  601. public override bool FolderAlreadyUnzippedAtPath(string path)
  602. {
  603. if (string.IsNullOrEmpty (path)) {
  604. return false;
  605. }
  606. return System.IO.File.Exists (System.IO.Path.Combine (path, MaxIntegration.PluginPath));
  607. }
  608. }
  609. static class IntegrationsUI
  610. {
  611. /// <summary>
  612. /// The path of the DCC executable.
  613. /// </summary>
  614. public static string GetDCCExe () {
  615. return ExportSettings.SelectedDCCPath;
  616. }
  617. /// <summary>
  618. /// Gets the name of the selected DCC.
  619. /// </summary>
  620. /// <returns>The DCC name.</returns>
  621. public static string GetDCCName() {
  622. return ExportSettings.SelectedDCCName;
  623. }
  624. /// <summary>
  625. /// Opens a dialog showing whether the installation succeeded.
  626. /// </summary>
  627. /// <param name="dcc">Dcc name.</param>
  628. private static void ShowSuccessDialog(string dcc, int exitCode){
  629. string title, message, customMessage;
  630. if (exitCode != 0) {
  631. title = string.Format("Failed to install {0} Integration.", dcc);
  632. message = string.Format("Failed to configure {0}, please check logs (exitcode={1}).", dcc, exitCode);
  633. } else {
  634. if (ExportSettings.instance.LaunchAfterInstallation)
  635. {
  636. customMessage = "Installing Unity menu in {0}, application will open once installation is complete";
  637. }
  638. else
  639. {
  640. customMessage = "Enjoy the new Unity menu in {0}.";
  641. }
  642. title = string.Format("Completing installation of {0} Integration.", dcc);
  643. message = string.Format(customMessage, dcc);
  644. }
  645. UnityEditor.EditorUtility.DisplayDialog (title, message, "Ok");
  646. }
  647. public static void InstallDCCIntegration ()
  648. {
  649. var dccExe = GetDCCExe ();
  650. if (string.IsNullOrEmpty (dccExe)) {
  651. return;
  652. }
  653. string dccType = System.IO.Path.GetFileNameWithoutExtension (dccExe).ToLower();
  654. DCCIntegration dccIntegration;
  655. if (dccType.Equals ("maya")) {
  656. // could be Maya or Maya LT
  657. if (GetDCCName ().ToLower ().Contains ("lt")) {
  658. dccIntegration = new MayaLTIntegration ();
  659. } else {
  660. dccIntegration = new MayaIntegration ();
  661. }
  662. } else if (dccType.Equals ("3dsmax")) {
  663. dccIntegration = new MaxIntegration ();
  664. } else {
  665. throw new System.NotImplementedException ();
  666. }
  667. if (!GetIntegrationFolder (dccIntegration)) {
  668. // failed to get integration folder
  669. return;
  670. }
  671. int exitCode = dccIntegration.InstallIntegration (dccExe);
  672. ShowSuccessDialog (dccIntegration.DccDisplayName, exitCode);
  673. }
  674. private static bool GetIntegrationFolder(DCCIntegration dcc){
  675. // decompress zip file if it exists, otherwise try using default location
  676. var zipPath = dcc.IntegrationZipFullPath;
  677. if (System.IO.File.Exists (zipPath)) {
  678. return DecompressIntegrationZipFile (zipPath, dcc);
  679. }
  680. dcc.SetIntegrationFolderPath (ExportSettings.IntegrationSavePath);
  681. return true;
  682. }
  683. private static bool DecompressIntegrationZipFile(string zipPath, DCCIntegration dcc)
  684. {
  685. // prompt user to enter location to unzip file
  686. var unzipFolder = EditorUtility.OpenFolderPanel(string.Format("Select Location to Save {0} Integration", dcc.DccDisplayName), ExportSettings.IntegrationSavePath, "");
  687. if (string.IsNullOrEmpty(unzipFolder))
  688. {
  689. // user has cancelled, do nothing
  690. return false;
  691. }
  692. ExportSettings.IntegrationSavePath = unzipFolder;
  693. // check that this is a valid location to unzip the file
  694. if (!DirectoryHasWritePermission (unzipFolder)) {
  695. // display dialog to try again or cancel
  696. var result = UnityEditor.EditorUtility.DisplayDialog ("No Write Permission",
  697. string.Format("Directory \"{0}\" does not have write access", unzipFolder),
  698. "Select another Directory",
  699. "Cancel"
  700. );
  701. if (result) {
  702. InstallDCCIntegration ();
  703. } else {
  704. return false;
  705. }
  706. }
  707. // if file already unzipped in this location, then prompt user
  708. // if they would like to continue unzipping or use what is there
  709. if (dcc.FolderAlreadyUnzippedAtPath (unzipFolder)) {
  710. var result = UnityEditor.EditorUtility.DisplayDialogComplex ("Integrations Exist at Path",
  711. string.Format ("Directory \"{0}\" already contains the decompressed integration", unzipFolder),
  712. "Overwrite",
  713. "Use Existing",
  714. "Cancel"
  715. );
  716. if (result == 0) {
  717. DecompressZip (zipPath, unzipFolder);
  718. } else if (result == 2) {
  719. return false;
  720. }
  721. } else {
  722. // unzip Integration folder
  723. DecompressZip (zipPath, unzipFolder);
  724. }
  725. dcc.SetIntegrationFolderPath(unzipFolder);
  726. return true;
  727. }
  728. /// <summary>
  729. /// Make sure we can write to this directory.
  730. /// Try creating a file in path directory, if it raises an error, then we can't
  731. /// write here.
  732. /// TODO: find a more reliable way to check this
  733. /// </summary>
  734. /// <returns><c>true</c>, if possible to write to path, <c>false</c> otherwise.</returns>
  735. /// <param name="path">Path.</param>
  736. public static bool DirectoryHasWritePermission(string path)
  737. {
  738. try
  739. {
  740. using (System.IO.FileStream fs = System.IO.File.Create(
  741. System.IO.Path.Combine(
  742. path,
  743. System.IO.Path.GetRandomFileName()
  744. ),
  745. 1,
  746. System.IO.FileOptions.DeleteOnClose)
  747. )
  748. { }
  749. return true;
  750. }
  751. catch(Exception)
  752. {
  753. return false;
  754. }
  755. }
  756. public static void DecompressZip(string zipPath, string destPath){
  757. System.Diagnostics.Process myProcess = new System.Diagnostics.Process();
  758. var ZIPAPP = "7z.exe";
  759. if (Application.platform == RuntimePlatform.OSXEditor) {
  760. ZIPAPP = "7za";
  761. }
  762. myProcess.StartInfo.FileName = EditorApplication.applicationContentsPath + "/Tools/" + ZIPAPP;
  763. myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
  764. myProcess.StartInfo.CreateNoWindow = true;
  765. myProcess.StartInfo.UseShellExecute = false;
  766. // Command line flags used:
  767. // x : extract the zip contents so that they maintain the file hierarchy
  768. // -o : specify where to extract contents
  769. // -r : recurse subdirectories
  770. // -y : auto yes to all questions (without this Unity freezes as the process waits for a response)
  771. myProcess.StartInfo.Arguments = string.Format("x \"{0}\" -o\"{1}\" -r -y", zipPath, destPath);
  772. myProcess.EnableRaisingEvents = true;
  773. myProcess.Start();
  774. myProcess.WaitForExit();
  775. // in case we unzip inside the Assets folder, make sure it updates
  776. AssetDatabase.Refresh ();
  777. }
  778. }
  779. }