/// <summary> /// Writes a .csv file to disk at the path indicated by <paramref /// name="destination"/>, containing all of the lines found in the /// scripts referred to by <paramref name="yarnProjectImporter"/>. /// </summary> /// <remarks> /// The file generated is in a format ready to be added to the <see /// cref="YarnProjectImporter.languagesToSourceAssets"/> list. /// </remarks> /// <param name="yarnProjectImporter">The YarnProjectImporter to /// extract strings from.</param> /// <param name="destination">The path to write the file /// to.</param> /// <returns><see langword="true"/> if the file was written /// successfully, <see langword="false"/> otherwise.</returns> /// <exception cref="CsvHelper.CsvHelperException">Thrown when an /// error is encountered when generating the CSV data.</exception> /// <exception cref="IOException">Thrown when an error is /// encountered when writing the data to disk.</exception> internal static bool WriteStringsFile(string destination, YarnProjectImporter yarnProjectImporter) { // Perform a strings-only compilation to get a full strings // table, and generate the CSV. var stringTable = yarnProjectImporter.GenerateStringsTable(); // If there was an error, bail out here if (stringTable == null) { return(false); } // Convert the string tables to CSV... var outputCSV = StringTableEntry.CreateCSV(stringTable); // ...and write it to disk. File.WriteAllText(destination, outputCSV); return(true); }
/// <summary> /// Verifies the TextAsset referred to by <paramref /// name="destinationLocalizationAsset"/>, and updates it if /// necessary. /// </summary> /// <param name="baseLocalizationStrings">A collection of <see /// cref="StringTableEntry"/></param> /// <param name="language">The language that <paramref /// name="destinationLocalizationAsset"/> provides strings /// for.false</param> /// <param name="destinationLocalizationAsset">A TextAsset /// containing localized strings in CSV format.</param> /// <returns>Whether <paramref /// name="destinationLocalizationAsset"/> was modified.</returns> private static bool UpdateLocalizationFile(IEnumerable <StringTableEntry> baseLocalizationStrings, string language, TextAsset destinationLocalizationAsset) { var translatedStrings = StringTableEntry.ParseFromCSV(destinationLocalizationAsset.text); // Convert both enumerables to dictionaries, for easier lookup var baseDictionary = baseLocalizationStrings.ToDictionary(entry => entry.ID); var translatedDictionary = translatedStrings.ToDictionary(entry => entry.ID); // The list of line IDs present in each localisation var baseIDs = baseLocalizationStrings.Select(entry => entry.ID); var translatedIDs = translatedStrings.Select(entry => entry.ID); // The list of line IDs that are ONLY present in each // localisation var onlyInBaseIDs = baseIDs.Except(translatedIDs); var onlyInTranslatedIDs = translatedIDs.Except(baseIDs); // Tracks if the translated localisation needed modifications // (either new lines added, old lines removed, or changed lines // flagged) var modificationsNeeded = false; // Remove every entry whose ID is only present in the // translated set. This entry has been removed from the base // localization. foreach (var id in onlyInTranslatedIDs.ToList()) { translatedDictionary.Remove(id); modificationsNeeded = true; } // Conversely, for every entry that is only present in the base // localisation, we need to create a new entry for it. foreach (var id in onlyInBaseIDs) { StringTableEntry baseEntry = baseDictionary[id]; var newEntry = new StringTableEntry(baseEntry) { // Empty this text, so that it's apparent that a // translated version needs to be provided. Text = string.Empty, Language = language, }; translatedDictionary.Add(id, newEntry); modificationsNeeded = true; } // Finally, we need to check for any entries in the translated // localisation that: // 1. have the same line ID as one in the base, but // 2. have a different Lock (the hash of the text), which // indicates that the base text has changed. // First, get the list of IDs that are in both base and // translated, and then filter this list to any where the lock // values differ var outOfDateLockIDs = baseDictionary.Keys .Intersect(translatedDictionary.Keys) .Where(id => baseDictionary[id].Lock != translatedDictionary[id].Lock); // Now loop over all of these, and update our translated // dictionary to include a note that it needs attention foreach (var id in outOfDateLockIDs) { // Get the translated entry as it currently exists var entry = translatedDictionary[id]; // Include a note that this entry is out of date entry.Text = $"(NEEDS UPDATE) {entry.Text}"; // Update the lock to match the new one entry.Lock = baseDictionary[id].Lock; // Put this modified entry back in the table translatedDictionary[id] = entry; modificationsNeeded = true; } // We're all done! if (modificationsNeeded == false) { // No changes needed to be done to the translated string // table entries. Stop here. return(false); } // We need to produce a replacement CSV file for the translated // entries. var outputStringEntries = translatedDictionary.Values .OrderBy(entry => entry.File) .ThenBy(entry => int.Parse(entry.LineNumber)); var outputCSV = StringTableEntry.CreateCSV(outputStringEntries); // Write out the replacement text to this existing file, // replacing its existing contents var outputFile = AssetDatabase.GetAssetPath(destinationLocalizationAsset); File.WriteAllText(outputFile, outputCSV, System.Text.Encoding.UTF8); // Tell the asset database that the file needs to be reimported AssetDatabase.ImportAsset(outputFile); // Signal that the file was changed return(true); }
private void ImportYarn(AssetImportContext ctx) { var sourceText = File.ReadAllText(ctx.assetPath); string fileName = System.IO.Path.GetFileNameWithoutExtension(ctx.assetPath); Yarn.Program compiledProgram = null; IDictionary <string, Yarn.Compiler.StringInfo> stringTable = null; compilationErrorMessage = null; try { // Compile the source code into a compiled Yarn program (or // generate a parse error) compilationStatus = Yarn.Compiler.Compiler.CompileString(sourceText, fileName, out compiledProgram, out stringTable); isSuccesfullyCompiled = true; compilationErrorMessage = string.Empty; } catch (Yarn.Compiler.ParseException e) { isSuccesfullyCompiled = false; compilationErrorMessage = e.Message; ctx.LogImportError(e.Message); } // Create a container for storing the bytes if (programContainer == null) { programContainer = ScriptableObject.CreateInstance <YarnProgram>(); } byte[] compiledBytes = null; if (compiledProgram != null) { using (var memoryStream = new MemoryStream()) using (var outputStream = new Google.Protobuf.CodedOutputStream(memoryStream)) { // Serialize the compiled program to memory compiledProgram.WriteTo(outputStream); outputStream.Flush(); compiledBytes = memoryStream.ToArray(); } } programContainer.compiledProgram = compiledBytes; // 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); if (stringTable?.Count > 0) { var lines = stringTable.Select(x => new StringTableEntry { ID = x.Key, Language = baseLanguageID, Text = x.Value.text, File = x.Value.fileName, Node = x.Value.nodeName, LineNumber = x.Value.lineNumber.ToString(), Lock = GetHashString(x.Value.text, 8), }).OrderBy(entry => int.Parse(entry.LineNumber)); var stringTableCSV = StringTableEntry.CreateCSV(lines); var textAsset = new TextAsset(stringTableCSV); textAsset.name = $"{fileName} ({baseLanguageID})"; ctx.AddObjectToAsset("Strings", textAsset); programContainer.baseLocalizationId = baseLanguageID; baseLanguage = textAsset; programContainer.localizations = localizations.Append(new YarnProgram.YarnTranslation(baseLanguageID, textAsset)).ToArray(); programContainer.baseLocalizationId = baseLanguageID; stringIDs = lines.Select(l => l.ID).ToArray(); } }