/// <summary> /// Creates a new .yarnproject asset in the same directory as the /// Yarn script represented by serializedObject, and configures the /// script's importer to use the new Yarn Project. /// </summary> /// <param name="serializedObject">A serialized object that /// represents a <see cref="YarnImporter"/>.</param> /// <returns>The path to the created asset.</returns> internal static string CreateYarnProject(YarnImporter initialSourceAsset) { // Figure out where on disk this asset is var path = initialSourceAsset.assetPath; var directory = Path.GetDirectoryName(path); // Figure out a new, unique path for the localization we're // creating var databaseFileName = $"Project.yarnproject"; var destinationPath = Path.Combine(directory, databaseFileName); destinationPath = AssetDatabase.GenerateUniqueAssetPath(destinationPath); // Create the program YarnEditorUtility.CreateYarnAsset(destinationPath); AssetDatabase.ImportAsset(destinationPath); AssetDatabase.SaveAssets(); var programImporter = AssetImporter.GetAtPath(destinationPath) as YarnProjectImporter; programImporter.sourceScripts.Add(AssetDatabase.LoadAssetAtPath <TextAsset>(path)); EditorUtility.SetDirty(programImporter); // Reimport the program to make it generate its default string // table, if needed programImporter.SaveAndReimport(); return(destinationPath); }
/// <summary> /// Generates a collection of <see cref="StringTableEntry"/> /// objects, one for each line in this Yarn Project's scripts. /// </summary> /// <returns>An IEnumerable containing a <see /// cref="StringTableEntry"/> for each of the lines in the Yarn /// Project, or <see langword="null"/> if the Yarn Project contains /// errors.</returns> internal IEnumerable <StringTableEntry> GenerateStringsTable() { var pathsToImporters = sourceScripts.Where(s => s != null).Select(s => AssetDatabase.GetAssetPath(s)); if (pathsToImporters.Count() == 0) { // We have no scripts to work with - return an empty // collection - there's no error, but there's no content // either return(new List <StringTableEntry>()); } // We now now compile! var job = CompilationJob.CreateFromFiles(pathsToImporters); job.CompilationType = CompilationJob.Type.StringsOnly; CompilationResult compilationResult; try { compilationResult = Compiler.Compiler.Compile(job); } catch (ParseException) { Debug.LogError($"Can't generate a strings table from a Yarn Project that contains compile errors", null); return(null); } IEnumerable <StringTableEntry> stringTableEntries = compilationResult.StringTable.Select(x => new StringTableEntry { ID = x.Key, Language = defaultLanguage, Text = x.Value.text, File = x.Value.fileName, Node = x.Value.nodeName, LineNumber = x.Value.lineNumber.ToString(), Lock = YarnImporter.GetHashString(x.Value.text, 8), }); return(stringTableEntries); }
public override void OnImportAsset(AssetImportContext ctx) { #if YARNSPINNER_DEBUG UnityEngine.Profiling.Profiler.enabled = true; #endif var project = ScriptableObject.CreateInstance <YarnProject>(); project.name = Path.GetFileNameWithoutExtension(ctx.assetPath); // Start by creating the asset - no matter what, we need to // produce an asset, even if it doesn't contain valid Yarn // bytecode, so that other assets don't lose their references. ctx.AddObjectToAsset("Project", project); ctx.SetMainObject(project); foreach (var script in sourceScripts) { string path = AssetDatabase.GetAssetPath(script); if (string.IsNullOrEmpty(path)) { // This is, for some reason, not a valid script we can // use. Don't add a dependency on it. continue; } ctx.DependsOnSourceAsset(path); } // Parse declarations var localDeclarationsCompileJob = CompilationJob.CreateFromFiles(ctx.assetPath); localDeclarationsCompileJob.CompilationType = CompilationJob.Type.DeclarationsOnly; IEnumerable <Declaration> localDeclarations; compileError = null; try { var result = Compiler.Compiler.Compile(localDeclarationsCompileJob); localDeclarations = result.Declarations; } catch (ParseException e) { ctx.LogImportError($"Error in Yarn Project: {e.Message}"); compileError = $"Error in Yarn Project {ctx.assetPath}: {e.Message}"; return; } // Store these so that we can continue displaying them after // this import step, in case there are compile errors later. // We'll replace this with a more complete list later if // compilation succeeds. serializedDeclarations = localDeclarations .Where(decl => decl.DeclarationType == Declaration.Type.Variable) .Select(decl => new SerializedDeclaration(decl)).ToList(); // We're done processing this file - we've parsed it, and // pulled any information out of it that we need to. Now to // compile the scripts associated with this project. var scriptImporters = sourceScripts.Where(s => s != null).Select(s => AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(s)) as YarnImporter); // First step: check to see if there's any parse errors in the // files. var scriptsWithParseErrors = scriptImporters.Where(script => script.isSuccessfullyParsed == false); if (scriptsWithParseErrors.Count() != 0) { // Parse errors! We can't continue. string failingScriptNameList = string.Join("\n", scriptsWithParseErrors.Select(script => script.assetPath)); compileError = $"Parse errors exist in the following files:\n{failingScriptNameList}"; return; } // Get paths to the scripts we're importing, and also map them // to their corresponding importer var pathsToImporters = scriptImporters.ToDictionary(script => script.assetPath, script => script); if (pathsToImporters.Count == 0) { return; // nothing further to do here } // We now now compile! var job = CompilationJob.CreateFromFiles(pathsToImporters.Keys); job.VariableDeclarations = localDeclarations; CompilationResult compilationResult; try { compilationResult = Compiler.Compiler.Compile(job); } catch (TypeException e) { ctx.LogImportError($"Error compiling: {e.Message}"); compileError = e.Message; var importer = pathsToImporters[e.FileName]; importer.parseErrorMessage = e.Message; EditorUtility.SetDirty(importer); return; } catch (ParseException e) { ctx.LogImportError(e.Message); compileError = e.Message; var importer = pathsToImporters[e.FileName]; importer.parseErrorMessage = e.Message; EditorUtility.SetDirty(importer); return; } if (compilationResult.Program == null) { ctx.LogImportError("Internal error: Failed to compile: resulting program was null, but compiler did not throw a parse exception."); return; } // Store _all_ declarations - both the ones in this // .yarnproject file, and the ones inside the .yarn files serializedDeclarations = localDeclarations .Concat(compilationResult.Declarations) .Where(decl => decl.DeclarationType == Declaration.Type.Variable) .Select(decl => new SerializedDeclaration(decl)).ToList(); // Clear error messages from all scripts - they've all passed // compilation foreach (var importer in pathsToImporters.Values) { importer.parseErrorMessage = null; } // Will we need to create a default localization? This variable // will be set to false if any of the languages we've // configured in languagesToSourceAssets is the default // language. var shouldAddDefaultLocalization = true; foreach (var pair in languagesToSourceAssets) { // Don't create a localization if the language ID was not // provided if (string.IsNullOrEmpty(pair.languageID)) { Debug.LogWarning($"Not creating a localization for {project.name} because the language ID wasn't provided. Add the language ID to the localization in the Yarn Project's inspector."); continue; } IEnumerable <StringTableEntry> stringTable; // Where do we get our strings from? If it's the default // language, we'll pull it from the scripts. If it's from // any other source, we'll pull it from the CSVs. if (pair.languageID == defaultLanguage) { // We'll use the program-supplied string table. stringTable = GenerateStringsTable(); // We don't need to add a default localization. shouldAddDefaultLocalization = false; } else { try { if (pair.stringsFile == null) { // We can't create this localization because we // don't have any data for it. Debug.LogWarning($"Not creating a localization for {pair.languageID} in the Yarn Project {project.name} because a text asset containing the strings wasn't found. Add a .csv file containing the translated lines to the Yarn Project's inspector."); continue; } stringTable = StringTableEntry.ParseFromCSV(pair.stringsFile.text); } catch (System.ArgumentException e) { Debug.LogWarning($"Not creating a localization for {pair.languageID} in the Yarn Project {project.name} because an error was encountered during text parsing: {e}"); continue; } } var newLocalization = ScriptableObject.CreateInstance <Localization>(); newLocalization.LocaleCode = pair.languageID; newLocalization.AddLocalizedStrings(stringTable); project.localizations.Add(newLocalization); newLocalization.name = pair.languageID; if (pair.assetsFolder != null) { var assetsFolderPath = AssetDatabase.GetAssetPath(pair.assetsFolder); if (assetsFolderPath == null) { // This was somehow not a valid reference? Debug.LogWarning($"Can't find assets for localization {pair.languageID} in {project.name} because a path for the provided assets folder couldn't be found."); } else { var stringIDsToAssets = FindAssetsForLineIDs(stringTable.Select(s => s.ID), assetsFolderPath); #if YARNSPINNER_DEBUG var stopwatch = System.Diagnostics.Stopwatch.StartNew(); #endif newLocalization.AddLocalizedObjects(stringIDsToAssets.AsEnumerable()); #if YARNSPINNER_DEBUG stopwatch.Stop(); Debug.Log($"Imported {stringIDsToAssets.Count()} assets for {project.name} \"{pair.languageID}\" in {stopwatch.ElapsedMilliseconds}ms"); #endif } } ctx.AddObjectToAsset("localization-" + pair.languageID, newLocalization); if (pair.languageID == defaultLanguage) { // If this is our default language, set it as such project.baseLocalization = newLocalization; } else { // This localization depends upon a source asset. Make // this asset get re-imported if this source asset was // modified ctx.DependsOnSourceAsset(AssetDatabase.GetAssetPath(pair.stringsFile)); } } if (shouldAddDefaultLocalization) { // We didn't add a localization for the default language. // Create one for it now. var developmentLocalization = ScriptableObject.CreateInstance <Localization>(); developmentLocalization.LocaleCode = defaultLanguage; var stringTableEntries = compilationResult.StringTable.Select(x => new StringTableEntry { ID = x.Key, Language = defaultLanguage, Text = x.Value.text, File = x.Value.fileName, Node = x.Value.nodeName, LineNumber = x.Value.lineNumber.ToString(), Lock = YarnImporter.GetHashString(x.Value.text, 8), }); developmentLocalization.AddLocalizedStrings(stringTableEntries); project.baseLocalization = developmentLocalization; project.localizations.Add(project.baseLocalization); developmentLocalization.name = $"Default ({defaultLanguage})"; ctx.AddObjectToAsset("default-language", developmentLocalization); } // Store the compiled program byte[] compiledBytes = null; using (var memoryStream = new MemoryStream()) using (var outputStream = new Google.Protobuf.CodedOutputStream(memoryStream)) { // Serialize the compiled program to memory compilationResult.Program.WriteTo(outputStream); outputStream.Flush(); compiledBytes = memoryStream.ToArray(); } project.compiledYarnProgram = compiledBytes; #if YARNSPINNER_DEBUG UnityEngine.Profiling.Profiler.enabled = false; #endif }