ScriptCodeManager.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. using System;
  4. using System.IO;
  5. using System.Text;
  6. using System.Text.RegularExpressions;
  7. using bs;
  8. namespace bs.Editor
  9. {
  10. /** @addtogroup Script
  11. * @{
  12. */
  13. /// <summary>
  14. /// Handles various operations related to script code in the active project, like compilation and code editor syncing.
  15. /// </summary>
  16. public sealed class ScriptCodeManager
  17. {
  18. private const int CompilerLogCategory = 100;
  19. private bool isGameAssemblyDirty;
  20. private bool isEditorAssemblyDirty;
  21. private CompilerInstance compilerInstance;
  22. /// <summary>
  23. /// Constructs a new script code manager.
  24. /// </summary>
  25. internal ScriptCodeManager()
  26. {
  27. ProjectLibrary.OnEntryAdded += OnEntryAdded;
  28. ProjectLibrary.OnEntryRemoved += OnEntryRemoved;
  29. ProjectLibrary.OnEntryImported += OnEntryImported;
  30. // Check for missing or out of date assemblies
  31. DateTime lastModifiedGameScript = DateTime.MinValue;
  32. DateTime lastModifiedEditorScript = DateTime.MinValue;
  33. LibraryEntry[] scriptEntries = ProjectLibrary.Search("*.cs", new ResourceType[] { ResourceType.ScriptCode });
  34. for (int i = 0; i < scriptEntries.Length; i++)
  35. {
  36. if(scriptEntries[i].Type != LibraryEntryType.File)
  37. continue;
  38. FileEntry fileEntry = (FileEntry)scriptEntries[i];
  39. string absPath = Path.Combine(ProjectLibrary.ResourceFolder, fileEntry.Path);
  40. ScriptCodeImportOptions io = (ScriptCodeImportOptions) fileEntry.Options;
  41. if (io.EditorScript)
  42. lastModifiedEditorScript = File.GetLastWriteTime(absPath);
  43. else
  44. lastModifiedGameScript = File.GetLastWriteTime(absPath);
  45. }
  46. DateTime lastCompileTime = new DateTime(EditorApplication.PersistentData.lastCompileTime);
  47. if (lastModifiedGameScript != DateTime.MinValue)
  48. {
  49. string gameAssemblyPath = Path.Combine(EditorApplication.ScriptAssemblyPath,
  50. EditorApplication.ScriptGameAssemblyName);
  51. isGameAssemblyDirty = (!File.Exists(gameAssemblyPath) ||
  52. File.GetLastWriteTime(gameAssemblyPath) < lastModifiedGameScript) &&
  53. (lastModifiedGameScript > lastCompileTime);
  54. }
  55. if (lastModifiedEditorScript != DateTime.MinValue)
  56. {
  57. string editorAssemblyPath = Path.Combine(EditorApplication.ScriptAssemblyPath,
  58. EditorApplication.ScriptEditorAssemblyName);
  59. isEditorAssemblyDirty = (!File.Exists(editorAssemblyPath) ||
  60. File.GetLastWriteTime(editorAssemblyPath) < lastModifiedEditorScript) &&
  61. (lastModifiedEditorScript > lastCompileTime);
  62. }
  63. }
  64. /// <summary>
  65. /// Triggers required compilation or code editor syncing if needed.
  66. /// </summary>
  67. internal void Update()
  68. {
  69. if (EditorApplication.HasFocus && CodeEditor.IsSolutionDirty)
  70. CodeEditor.SyncSolution();
  71. if (PlayInEditor.State == PlayInEditorState.Stopped && !ProjectLibrary.ImportInProgress)
  72. {
  73. if (compilerInstance == null)
  74. {
  75. if (EditorApplication.HasFocus)
  76. {
  77. string outputDir = EditorApplication.ScriptAssemblyPath;
  78. if (isGameAssemblyDirty)
  79. {
  80. compilerInstance = ScriptCompiler.CompileAsync(
  81. ScriptAssemblyType.Game, BuildManager.ActivePlatform, true, outputDir);
  82. EditorApplication.SetStatusCompiling(true);
  83. EditorApplication.PersistentData.lastCompileTime = DateTime.Now.Ticks;
  84. isGameAssemblyDirty = false;
  85. }
  86. else if (isEditorAssemblyDirty)
  87. {
  88. compilerInstance = ScriptCompiler.CompileAsync(
  89. ScriptAssemblyType.Editor, BuildManager.ActivePlatform, true, outputDir);
  90. EditorApplication.SetStatusCompiling(true);
  91. EditorApplication.PersistentData.lastCompileTime = DateTime.Now.Ticks;
  92. isEditorAssemblyDirty = false;
  93. }
  94. }
  95. }
  96. else
  97. {
  98. if (compilerInstance.IsDone)
  99. {
  100. Debug.Clear(LogVerbosity.Any, CompilerLogCategory);
  101. LogWindow window = EditorWindow.GetWindow<LogWindow>();
  102. if (window != null)
  103. window.Refresh();
  104. if (compilerInstance.HasErrors)
  105. {
  106. foreach (var msg in compilerInstance.WarningMessages)
  107. Debug.LogMessage(FormMessage(msg), LogVerbosity.Warning, CompilerLogCategory);
  108. foreach (var msg in compilerInstance.ErrorMessages)
  109. Debug.LogMessage(FormMessage(msg), LogVerbosity.Error, CompilerLogCategory);
  110. }
  111. compilerInstance.Dispose();
  112. compilerInstance = null;
  113. EditorApplication.SetStatusCompiling(false);
  114. EditorApplication.ReloadAssemblies();
  115. }
  116. }
  117. }
  118. }
  119. /// <summary>
  120. /// Triggered when a new resource is added to the project library.
  121. /// </summary>
  122. /// <param name="path">Path of the added resource, relative to the project's resource folder.</param>
  123. private void OnEntryAdded(string path)
  124. {
  125. if (IsCodeEditorFile(path))
  126. CodeEditor.MarkSolutionDirty();
  127. }
  128. /// <summary>
  129. /// Triggered when a resource is removed from the project library.
  130. /// </summary>
  131. /// <param name="path">Path of the removed resource, relative to the project's resource folder.</param>
  132. private void OnEntryRemoved(string path)
  133. {
  134. if (IsCodeEditorFile(path))
  135. CodeEditor.MarkSolutionDirty();
  136. }
  137. /// <summary>
  138. /// Triggered when a resource is (re)imported in the project library.
  139. /// </summary>
  140. /// <param name="path">Path of the imported resource, relative to the project's resource folder.</param>
  141. private void OnEntryImported(string path)
  142. {
  143. LibraryEntry entry = ProjectLibrary.GetEntry(path);
  144. if (entry == null || entry.Type != LibraryEntryType.File)
  145. return;
  146. FileEntry fileEntry = (FileEntry)entry;
  147. ResourceMeta[] resourceMetas = fileEntry.ResourceMetas;
  148. bool found = false;
  149. foreach (var meta in resourceMetas)
  150. {
  151. if (meta.ResType == ResourceType.ScriptCode)
  152. {
  153. found = true;
  154. break;
  155. }
  156. }
  157. if (!found)
  158. return;
  159. ScriptCode codeFile = ProjectLibrary.Load<ScriptCode>(path);
  160. if(codeFile == null)
  161. return;
  162. if(codeFile.EditorScript)
  163. isEditorAssemblyDirty = true;
  164. else
  165. isGameAssemblyDirty = true;
  166. }
  167. /// <summary>
  168. /// Checks is the resource at the provided path a file relevant to the code editor.
  169. /// </summary>
  170. /// <param name="path">Path to the resource, absolute or relative to the project's resources folder.</param>
  171. /// <returns>True if the file is relevant to the code editor, false otherwise.</returns>
  172. private bool IsCodeEditorFile(string path)
  173. {
  174. LibraryEntry entry = ProjectLibrary.GetEntry(path);
  175. if (entry != null && entry.Type == LibraryEntryType.File)
  176. {
  177. FileEntry fileEntry = (FileEntry)entry;
  178. ResourceMeta[] resourceMetas = fileEntry.ResourceMetas;
  179. foreach (var codeType in CodeEditor.CodeTypes)
  180. {
  181. foreach (var meta in resourceMetas)
  182. {
  183. if (meta.ResType == codeType)
  184. return true;
  185. }
  186. }
  187. }
  188. return false;
  189. }
  190. /// <summary>
  191. /// Converts data reported by the compiler into a readable string.
  192. /// </summary>
  193. /// <param name="msg">Message data as reported by the compiler.</param>
  194. /// <returns>Readable message string.</returns>
  195. private string FormMessage(CompilerMessage msg)
  196. {
  197. StringBuilder sb = new StringBuilder();
  198. if (msg.type == CompilerMessageType.Error)
  199. sb.AppendLine("Compiler error: " + msg.message);
  200. else
  201. sb.AppendLine("Compiler warning: " + msg.message);
  202. sb.AppendLine("\tin " + msg.file + "[" + msg.line + ":" + msg.column + "]");
  203. return sb.ToString();
  204. }
  205. /// <summary>
  206. /// Parses a log message and outputs a data object with a separate message and callstack entries. If the message
  207. /// is not a valid compiler message null is returned.
  208. /// </summary>
  209. /// <param name="message">Message to parse.</param>
  210. /// <returns>Parsed log message or null if not a valid compiler message.</returns>
  211. public static ParsedLogEntry ParseCompilerMessage(string message)
  212. {
  213. // Note: If modifying FormMessage method make sure to update this one as well to match the formattting
  214. // Check for error
  215. Regex regex = new Regex(@"Compiler error: (.*)\n\tin (.*)\[(.*):.*\]");
  216. var match = regex.Match(message);
  217. // Check for warning
  218. if (!match.Success)
  219. {
  220. regex = new Regex(@"Compiler warning: (.*)\n\tin (.*)\[(.*):.*\]");
  221. match = regex.Match(message);
  222. }
  223. // No match
  224. if (!match.Success)
  225. return null;
  226. ParsedLogEntry entry = new ParsedLogEntry();
  227. entry.callstack = new CallStackEntry[1];
  228. entry.message = match.Groups[1].Value;
  229. CallStackEntry callstackEntry = new CallStackEntry();
  230. callstackEntry.method = "";
  231. callstackEntry.file = match.Groups[2].Value;
  232. int.TryParse(match.Groups[3].Value, out callstackEntry.line);
  233. entry.callstack[0] = callstackEntry;
  234. return entry;
  235. }
  236. }
  237. /** @} */
  238. }