//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using System; using System.IO; using System.Text; using System.Text.RegularExpressions; using bs; namespace bs.Editor { /** @addtogroup Script * @{ */ /// /// Handles various operations related to script code in the active project, like compilation and code editor syncing. /// public sealed class ScriptCodeManager { private const int CompilerLogCategory = 100; private bool isGameAssemblyDirty; private bool isEditorAssemblyDirty; private CompilerInstance compilerInstance; /// /// Constructs a new script code manager. /// internal ScriptCodeManager() { ProjectLibrary.OnEntryAdded += OnEntryAdded; ProjectLibrary.OnEntryRemoved += OnEntryRemoved; ProjectLibrary.OnEntryImported += OnEntryImported; // Check for missing or out of date assemblies DateTime lastModifiedGameScript = DateTime.MinValue; DateTime lastModifiedEditorScript = DateTime.MinValue; LibraryEntry[] scriptEntries = ProjectLibrary.Search("*.cs", new ResourceType[] { ResourceType.ScriptCode }); for (int i = 0; i < scriptEntries.Length; i++) { if(scriptEntries[i].Type != LibraryEntryType.File) continue; FileEntry fileEntry = (FileEntry)scriptEntries[i]; string absPath = Path.Combine(ProjectLibrary.ResourceFolder, fileEntry.Path); ScriptCodeImportOptions io = (ScriptCodeImportOptions) fileEntry.Options; if (io.EditorScript) lastModifiedEditorScript = File.GetLastWriteTime(absPath); else lastModifiedGameScript = File.GetLastWriteTime(absPath); } DateTime lastCompileTime = new DateTime(EditorApplication.PersistentData.lastCompileTime); if (lastModifiedGameScript != DateTime.MinValue) { string gameAssemblyPath = Path.Combine(EditorApplication.ScriptAssemblyPath, EditorApplication.ScriptGameAssemblyName); isGameAssemblyDirty = (!File.Exists(gameAssemblyPath) || File.GetLastWriteTime(gameAssemblyPath) < lastModifiedGameScript) && (lastModifiedGameScript > lastCompileTime); } if (lastModifiedEditorScript != DateTime.MinValue) { string editorAssemblyPath = Path.Combine(EditorApplication.ScriptAssemblyPath, EditorApplication.ScriptEditorAssemblyName); isEditorAssemblyDirty = (!File.Exists(editorAssemblyPath) || File.GetLastWriteTime(editorAssemblyPath) < lastModifiedEditorScript) && (lastModifiedEditorScript > lastCompileTime); } } /// /// Triggers required compilation or code editor syncing if needed. /// internal void Update() { if (EditorApplication.HasFocus && CodeEditor.IsSolutionDirty) CodeEditor.SyncSolution(); if (PlayInEditor.State == PlayInEditorState.Stopped && !ProjectLibrary.ImportInProgress) { if (compilerInstance == null) { if (EditorApplication.HasFocus) { string outputDir = EditorApplication.ScriptAssemblyPath; if (isGameAssemblyDirty) { compilerInstance = ScriptCompiler.CompileAsync( ScriptAssemblyType.Game, BuildManager.ActivePlatform, true, outputDir); EditorApplication.SetStatusCompiling(true); EditorApplication.PersistentData.lastCompileTime = DateTime.Now.Ticks; isGameAssemblyDirty = false; } else if (isEditorAssemblyDirty) { compilerInstance = ScriptCompiler.CompileAsync( ScriptAssemblyType.Editor, BuildManager.ActivePlatform, true, outputDir); EditorApplication.SetStatusCompiling(true); EditorApplication.PersistentData.lastCompileTime = DateTime.Now.Ticks; isEditorAssemblyDirty = false; } } } else { if (compilerInstance.IsDone) { Debug.Clear(LogVerbosity.Any, CompilerLogCategory); LogWindow window = EditorWindow.GetWindow(); if (window != null) window.Refresh(); if (compilerInstance.HasErrors) { foreach (var msg in compilerInstance.WarningMessages) Debug.LogMessage(FormMessage(msg), LogVerbosity.Warning, CompilerLogCategory); foreach (var msg in compilerInstance.ErrorMessages) Debug.LogMessage(FormMessage(msg), LogVerbosity.Error, CompilerLogCategory); } compilerInstance.Dispose(); compilerInstance = null; EditorApplication.SetStatusCompiling(false); EditorApplication.ReloadAssemblies(); } } } } /// /// Triggered when a new resource is added to the project library. /// /// Path of the added resource, relative to the project's resource folder. private void OnEntryAdded(string path) { if (IsCodeEditorFile(path)) CodeEditor.MarkSolutionDirty(); } /// /// Triggered when a resource is removed from the project library. /// /// Path of the removed resource, relative to the project's resource folder. private void OnEntryRemoved(string path) { if (IsCodeEditorFile(path)) CodeEditor.MarkSolutionDirty(); } /// /// Triggered when a resource is (re)imported in the project library. /// /// Path of the imported resource, relative to the project's resource folder. private void OnEntryImported(string path) { LibraryEntry entry = ProjectLibrary.GetEntry(path); if (entry == null || entry.Type != LibraryEntryType.File) return; FileEntry fileEntry = (FileEntry)entry; ResourceMeta[] resourceMetas = fileEntry.ResourceMetas; bool found = false; foreach (var meta in resourceMetas) { if (meta.ResType == ResourceType.ScriptCode) { found = true; break; } } if (!found) return; ScriptCode codeFile = ProjectLibrary.Load(path); if(codeFile == null) return; if(codeFile.EditorScript) isEditorAssemblyDirty = true; else isGameAssemblyDirty = true; } /// /// Checks is the resource at the provided path a file relevant to the code editor. /// /// Path to the resource, absolute or relative to the project's resources folder. /// True if the file is relevant to the code editor, false otherwise. private bool IsCodeEditorFile(string path) { LibraryEntry entry = ProjectLibrary.GetEntry(path); if (entry != null && entry.Type == LibraryEntryType.File) { FileEntry fileEntry = (FileEntry)entry; ResourceMeta[] resourceMetas = fileEntry.ResourceMetas; foreach (var codeType in CodeEditor.CodeTypes) { foreach (var meta in resourceMetas) { if (meta.ResType == codeType) return true; } } } return false; } /// /// Converts data reported by the compiler into a readable string. /// /// Message data as reported by the compiler. /// Readable message string. private string FormMessage(CompilerMessage msg) { StringBuilder sb = new StringBuilder(); if (msg.type == CompilerMessageType.Error) sb.AppendLine("Compiler error: " + msg.message); else sb.AppendLine("Compiler warning: " + msg.message); sb.AppendLine("\tin " + msg.file + "[" + msg.line + ":" + msg.column + "]"); return sb.ToString(); } /// /// Parses a log message and outputs a data object with a separate message and callstack entries. If the message /// is not a valid compiler message null is returned. /// /// Message to parse. /// Parsed log message or null if not a valid compiler message. public static ParsedLogEntry ParseCompilerMessage(string message) { // Note: If modifying FormMessage method make sure to update this one as well to match the formattting // Check for error Regex regex = new Regex(@"Compiler error: (.*)\n\tin (.*)\[(.*):.*\]"); var match = regex.Match(message); // Check for warning if (!match.Success) { regex = new Regex(@"Compiler warning: (.*)\n\tin (.*)\[(.*):.*\]"); match = regex.Match(message); } // No match if (!match.Success) return null; ParsedLogEntry entry = new ParsedLogEntry(); entry.callstack = new CallStackEntry[1]; entry.message = match.Groups[1].Value; CallStackEntry callstackEntry = new CallStackEntry(); callstackEntry.method = ""; callstackEntry.file = match.Groups[2].Value; int.TryParse(match.Groups[3].Value, out callstackEntry.line); entry.callstack[0] = callstackEntry; return entry; } } /** @} */ }