/* Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. */ using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using UnityEditor; using UnityEditor.AssetImporters; using UnityEngine; #nullable enable namespace Yarn.Unity.Editor { /// /// A for Yarn assets. /// [ScriptedImporter(6, new[] { "yarn", "yarnc" }, -1), HelpURL("https://docs.yarnspinner.dev/using-yarnspinner-with-unity/importing-yarn-files/yarn-projects")] [InitializeOnLoad] public class YarnImporter : ScriptedImporter { /// /// Get the collection of assets that /// reference this Yarn script. /// public IEnumerable DestinationProjects { get { return DestinationProjectImporters .Select(importer => AssetDatabase.LoadAssetAtPath(AssetDatabase.GetAssetPath(importer))); } } /// /// Get the collection of importers for the assets that /// reference this Yarn script. /// public IEnumerable DestinationProjectImporters { get { var myAssetPath = assetPath; var destinationProjectImporters = YarnEditorUtility.GetAllAssetsOf("t:YarnProject") .Where(importer => { // Does this importer depend on this asset? If so, // then this is our destination asset. string[] dependencies = AssetDatabase.GetDependencies(importer.assetPath); var importerDependsOnThisAsset = dependencies.Contains(myAssetPath); return importerDependsOnThisAsset; }); return destinationProjectImporters; } } /// /// Gets a value indicating whether any of the Yarn Projects in reported any errors in this file. /// public bool HasErrors { get { foreach (var projectImporter in DestinationProjectImporters) { if (projectImporter.GetErrorsForScript(ImportedScript).Any()) { return true; } } return false; } } private TextAsset ImportedScript => AssetDatabase.LoadAssetAtPath(this.assetPath); /// /// Called by Unity to import an asset. /// /// The context for the asset import /// operation. public override void OnImportAsset(AssetImportContext ctx) { var stopwatch = new System.Diagnostics.Stopwatch(); stopwatch.Start(); var extension = System.IO.Path.GetExtension(ctx.assetPath); if (extension == ".yarn") { // Import this file as a TextAsset. var textAsset = new TextAsset(File.ReadAllText(ctx.assetPath)); ctx.AddObjectToAsset("Script", textAsset, YarnEditorUtility.GetYarnDocumentIconTexture()); ctx.SetMainObject(textAsset); // Next, if we're a brand-new script, ensure that project // importers that need to depend on this script have a chance to // re-import. // Find all Yarn Project importers that _should_ be using this file. var projectsThatReferenceThisFile = YarnEditorUtility .GetAllAssetsOf("t:YarnProject") .Where(importer => importer.GetProjectReferencesYarnFile(this)); var missingProjectImporters = projectsThatReferenceThisFile .Where(importer => { var dependencies = AssetDatabase.GetDependencies(AssetDatabase.GetAssetPath(importer)); var importerDependsOnThisAsset = dependencies.Contains(ctx.assetPath); return importerDependsOnThisAsset == false; }); // We now have a list of project importers that SHOULD be // depending on this script, but currently aren't (because this // script was created after the project was last imported.) // Reimport these assets, which will cause them to add a // dependency on us. Do this on the next frame, because we're in // the middle of an import now, and aren't allowed to start // another one. EditorApplication.delayCall += () => { // Re-import each project. foreach (var importer in missingProjectImporters) { EditorUtility.SetDirty(importer); importer.SaveAndReimport(); } }; } else if (extension == ".yarnc") { ImportCompiledYarn(ctx); } } /// /// Returns a byte array containing a SHA-256 hash of . /// /// The string to produce a hash value /// for. /// The hash of . private static byte[] GetHash(string inputString) { using (HashAlgorithm algorithm = SHA256.Create()) { return algorithm.ComputeHash(Encoding.UTF8.GetBytes(inputString)); } } /// /// Returns a string containing the hexadecimal representation of a /// SHA-256 hash of . /// /// The string to produce a hash /// for. /// The length of the string to /// return. The returned string will be at most characters long. If this is set to -1, /// the entire string will be returned. /// A string version of the hash. public static string GetHashString(string inputString, int limitCharacters = -1) { var sb = new StringBuilder(); foreach (byte b in GetHash(inputString)) { sb.Append(b.ToString("x2")); } if (limitCharacters == -1) { // Return the entire string return sb.ToString(); } else { // Return a substring (or the entire string, if // limitCharacters is longer than the string) return sb.ToString(0, Mathf.Min(sb.Length, limitCharacters)); } } private void ImportCompiledYarn(AssetImportContext ctx) { var bytes = File.ReadAllBytes(ctx.assetPath); try { // Validate that this can be parsed as a Program protobuf var _ = Program.Parser.ParseFrom(bytes); } catch (Google.Protobuf.InvalidProtocolBufferException) { ctx.LogImportError("Invalid compiled yarn file. Please re-compile the source code."); return; } // Create a container for storing the bytes var programContainer = new TextAsset(""); // Add this container to the imported asset; it will be what // the user interacts with in Unity ctx.AddObjectToAsset("Program", programContainer, YarnEditorUtility.GetYarnDocumentIconTexture()); ctx.SetMainObject(programContainer); } } }