private void UpdateDestinationProject() { destinationYarnProject = (target as YarnImporter).DestinationProject; if (destinationYarnProject != null) { destinationYarnProjectImporter = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(destinationYarnProject)) as YarnProjectImporter; } }
/// <summary> /// Updates every localization .CSV file associated with this /// .yarnproject file. /// </summary> /// <remarks> /// This method updates each localization file by performing the /// following operations: /// /// - Inserts new entries if they're present in the base /// localization and not in the translated localization /// /// - Removes entries if they're present in the translated /// localization and not in the base localization /// /// - Detects if a line in the base localization has changed its /// Lock value from when the translated localization was created, /// and update its Comment /// </remarks> /// <param name="serializedObject">A serialized object that /// represents a <see cref="YarnProjectImporter"/>.</param> internal static void UpdateLocalizationCSVs(YarnProjectImporter yarnProjectImporter) { if (yarnProjectImporter.CanGenerateStringsTable == false) { Debug.LogError($"Can't update localization CSVs for Yarn Project \"{yarnProjectImporter.name}\" because not every line has a tag."); return; } var baseLocalizationStrings = yarnProjectImporter.GenerateStringsTable(); var localizations = yarnProjectImporter.languagesToSourceAssets; var modifiedFiles = new List <TextAsset>(); try { AssetDatabase.StartAssetEditing(); foreach (var loc in localizations) { if (loc.stringsFile == null) { Debug.LogWarning($"Can't update localization for {loc.languageID} because it doesn't have a strings file.", yarnProjectImporter); continue; } var fileWasChanged = UpdateLocalizationFile(baseLocalizationStrings, loc.languageID, loc.stringsFile); if (fileWasChanged) { modifiedFiles.Add(loc.stringsFile); } } if (modifiedFiles.Count > 0) { Debug.Log($"Updated the following files: {string.Join(", ", modifiedFiles.Select(f => AssetDatabase.GetAssetPath(f)))}"); } else { Debug.Log($"No files needed updating."); } } finally { AssetDatabase.StopAssetEditing(); } }
internal static void UpdateAssetAddresses(YarnProjectImporter importer) { #if USE_ADDRESSABLES var lineIDs = importer.GenerateStringsTable().Select(s => s.ID); // Get a map of language IDs to (lineID, asset path) pairs var languageToAssets = importer // Get the languages-to-source-assets map .languagesToSourceAssets // Get the asset folder for them .Select(l => new { l.languageID, l.assetsFolder }) // Only consider those that have an asset folder .Where(f => f.assetsFolder != null) // Get the path for the asset folder .Select(f => new { f.languageID, path = AssetDatabase.GetAssetPath(f.assetsFolder) }) // Use that to get the assets inside these folders .Select(f => new { f.languageID, assetPaths = FindAssetPathsForLineIDs(lineIDs, f.path) }); var addressableAssetSettings = AddressableAssetSettingsDefaultObject.Settings; foreach (var languageToAsset in languageToAssets) { var assets = languageToAsset.assetPaths .Select(pair => new { LineID = pair.Key, GUID = AssetDatabase.AssetPathToGUID(pair.Value) }); foreach (var asset in assets) { // Find the existing entry for this asset, if it has // one. AddressableAssetEntry entry = addressableAssetSettings.FindAssetEntry(asset.GUID); if (entry == null) { // This asset didn't have an entry. Create one in // the default group. entry = addressableAssetSettings.CreateOrMoveEntry(asset.GUID, addressableAssetSettings.DefaultGroup); } // Update the entry's address. entry.SetAddress(Localization.GetAddressForLine(asset.LineID, languageToAsset.languageID)); } } #else throw new System.NotSupportedException($"A method that requires the Addressable Assets package was called, but USE_ADDRESSABLES was not defined. Please either install Addressable Assets, or if you have already, add it to this project's compiler definitions."); #endif }
/// <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); }
internal static void ConvertImplicitVariableDeclarationsToExplicit(YarnProjectImporter yarnProjectImporter) { var allFilePaths = yarnProjectImporter.sourceScripts.Select(textAsset => AssetDatabase.GetAssetPath(textAsset)); var library = new Library(); ActionManager.RegisterFunctions(library); var explicitDeclarationsCompilerJob = Compiler.CompilationJob.CreateFromFiles(AssetDatabase.GetAssetPath(yarnProjectImporter)); Compiler.CompilationResult explicitResult; try { explicitResult = Compiler.Compiler.Compile(explicitDeclarationsCompilerJob); } catch (System.Exception e) { Debug.LogError($"Compile error: {e}"); return; } var implicitDeclarationsCompilerJob = Compiler.CompilationJob.CreateFromFiles(allFilePaths, library); implicitDeclarationsCompilerJob.CompilationType = Compiler.CompilationJob.Type.DeclarationsOnly; implicitDeclarationsCompilerJob.VariableDeclarations = explicitResult.Declarations; Compiler.CompilationResult implicitResult; try { implicitResult = Compiler.Compiler.Compile(implicitDeclarationsCompilerJob); } catch (System.Exception e) { Debug.LogError($"Compile error: {e}"); return; } var implicitDeclarations = implicitResult.Declarations.Where(d => !(d.Type is Yarn.FunctionType) && d.IsImplicit); var output = Yarn.Compiler.Utility.GenerateYarnFileWithDeclarations(explicitResult.Declarations.Concat(implicitDeclarations), "Program"); File.WriteAllText(yarnProjectImporter.assetPath, output, System.Text.Encoding.UTF8); AssetDatabase.ImportAsset(yarnProjectImporter.assetPath); }
internal static void AddLineTagsToFilesInYarnProject(YarnProjectImporter importer) { // First, gather all existing line tags across ALL yarn // projects, so that we don't accidentally overwrite an // existing one. Do this by finding all yarn scripts in all // yarn projects, and get the string tags inside them. var allYarnFiles = // get all yarn projects across the entire project AssetDatabase.FindAssets($"t:{nameof(YarnProject)}") // Get the path for each asset's GUID .Select(guid => AssetDatabase.GUIDToAssetPath(guid)) // Get the importer for each asset at this path .Select(path => AssetImporter.GetAtPath(path)) // Ensure it's a YarnProjectImporter .OfType <YarnProjectImporter>() // Get all of their source scripts, as a single sequence .SelectMany(i => i.sourceScripts) // Get the path for each asset .Select(sourceAsset => AssetDatabase.GetAssetPath(sourceAsset)) // get each asset importer for that path .Select(path => AssetImporter.GetAtPath(path)) // ensure that it's a YarnImporter .OfType <YarnImporter>() // get the path for each importer's asset (the compiler // will use this) .Select(i => AssetDatabase.GetAssetPath(i)) // remove any nulls, in case any are found .Where(path => path != null); #if YARNSPINNER_DEBUG var stopwatch = System.Diagnostics.Stopwatch.StartNew(); #endif // Compile all of these, and get whatever existing string tags // they had. Do each in isolation so that we can continue even // if a file contains a parse error. var allExistingTags = allYarnFiles.SelectMany(path => { // Compile this script in strings-only mode to get // string entries var compilationJob = Yarn.Compiler.CompilationJob.CreateFromFiles(path); compilationJob.CompilationType = Yarn.Compiler.CompilationJob.Type.StringsOnly; var result = Yarn.Compiler.Compiler.Compile(compilationJob); bool containsErrors = result.Diagnostics .Any(d => d.Severity == Compiler.Diagnostic.DiagnosticSeverity.Error); if (containsErrors) { Debug.LogWarning($"Can't check for existing line tags in {path} because it contains errors."); return(new string[] { }); } return(result.StringTable.Where(i => i.Value.isImplicitTag == false).Select(i => i.Key)); }).ToList(); // immediately execute this query so we can determine timing information #if YARNSPINNER_DEBUG stopwatch.Stop(); Debug.Log($"Checked {allYarnFiles.Count()} yarn files for line tags in {stopwatch.ElapsedMilliseconds}ms"); #endif var modifiedFiles = new List <string>(); try { AssetDatabase.StartAssetEditing(); foreach (var script in importer.sourceScripts) { var assetPath = AssetDatabase.GetAssetPath(script); var contents = File.ReadAllText(assetPath); // Produce a version of this file that contains line // tags added where they're needed. var taggedVersion = Yarn.Compiler.Utility.AddTagsToLines(contents, allExistingTags); // If this produced a modified version of the file, // write it out and re-import it. if (contents != taggedVersion) { modifiedFiles.Add(Path.GetFileNameWithoutExtension(assetPath)); File.WriteAllText(assetPath, taggedVersion, System.Text.Encoding.UTF8); AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.Default); } } } catch (System.Exception e) { Debug.LogError($"Encountered an error when updating scripts: {e}"); return; } finally { AssetDatabase.StopAssetEditing(); } // Report on the work we did. if (modifiedFiles.Count > 0) { Debug.Log($"Updated the following files: {string.Join(", ", modifiedFiles)}"); } else { Debug.Log("No files needed updating."); } }
/// <summary> /// Assign a .yarn <see cref="TextAsset"/> file <paramref /// name="newSourceScript"/> to the <see /// cref="YarnProjectImporter"/> <paramref /// name="projectImporter"/>. /// </summary> /// <remarks>If <paramref name="projectImporter"/> is <see /// langword="null"/>, this script will be removed from its current /// project (if any) and not added to another.</remarks> /// <param name="newSourceScript">The script that should be /// assigned to the Yarn Project.</param> /// <param name="scriptImporter">The importer for <paramref /// name="newSourceScript"/>.</param> /// <param name="projectImporter">The importer for the project that /// newSourceScript should be made a part of, or null.</param> internal static void AssignScriptToProject(TextAsset newSourceScript, YarnImporter scriptImporter, YarnProjectImporter projectImporter) { if (scriptImporter.DestinationProject != null) { var existingProjectImporter = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(scriptImporter.DestinationProject)) as YarnProjectImporter; existingProjectImporter.sourceScripts.Remove(newSourceScript); EditorUtility.SetDirty(existingProjectImporter); existingProjectImporter.SaveAndReimport(); } if (projectImporter != null) { projectImporter.sourceScripts.Add(newSourceScript); EditorUtility.SetDirty(projectImporter); // Reimport the program to make it generate its default string // table, if needed projectImporter.SaveAndReimport(); } }
public override void OnInspectorGUI() { serializedObject.Update(); YarnProjectImporter yarnProjectImporter = serializedObject.targetObject as YarnProjectImporter; EditorGUILayout.Space(); if (sourceScriptsProperty.arraySize == 0) { EditorGUILayout.HelpBox("This Yarn Project has no content. Add Yarn Scripts to it.", MessageType.Warning); } EditorGUILayout.PropertyField(sourceScriptsProperty, true); EditorGUILayout.Space(); bool hasCompileError = compileErrorsProperty.arraySize > 0; if (hasCompileError) { foreach (SerializedProperty compileError in compileErrorsProperty) { EditorGUILayout.HelpBox(compileError.stringValue, MessageType.Error); } } serializedDeclarationsList.DrawLayout(); // The 'Convert Implicit Declarations' feature has been // temporarily removed in v2.0.0-beta5. #if false // If any of the serialized declarations are implicit, add a // button that lets you generate explicit declarations for them var anyImplicitDeclarations = false; foreach (SerializedProperty declProp in serializedDeclarationsProperty) { anyImplicitDeclarations |= declProp.FindPropertyRelative("isImplicit").boolValue; } if (hasCompileError == false && anyImplicitDeclarations) { if (GUILayout.Button("Convert Implicit Declarations")) { // add explicit variable declarations to the file YarnProjectUtility.ConvertImplicitVariableDeclarationsToExplicit(yarnProjectImporter); // Return here becuase this method call will cause the // YarnProgram contents to change, which confuses the // SerializedObject when we're in the middle of a GUI // draw call. So, stop here, and let Unity re-draw the // Inspector (which it will do on the next editor tick // because the item we're inspecting got re-imported.) return; } } #endif EditorGUILayout.PropertyField(defaultLanguageProperty, new GUIContent("Base Language")); CurrentProjectDefaultLanguageProperty = defaultLanguageProperty; EditorGUILayout.PropertyField(languagesToSourceAssetsProperty, new GUIContent("Localisations")); CurrentProjectDefaultLanguageProperty = null; // Ask the project importer if it can generate a strings table. // This involves querying several assets, which means various // exceptions might get thrown, which we'll catch and log (if // we're in debug mode). bool canGenerateStringsTable; try { canGenerateStringsTable = yarnProjectImporter.CanGenerateStringsTable; } catch (System.Exception e) { #if YARNSPINNER_DEBUG Debug.LogWarning($"Encountered in error when checking to see if Yarn Project Importer could generate a strings table: {e}", this); #else // Ignore the 'variable e is unused' warning var _ = e; #endif canGenerateStringsTable = false; } // The following controls only do something useful if all of // the lines in the project have tags, which means the project // can generate a string table. using (new EditorGUI.DisabledScope(canGenerateStringsTable == false)) { #if USE_ADDRESSABLES // If the addressable assets package is available, show a // checkbox for using it. var hasAnySourceAssetFolders = yarnProjectImporter.languagesToSourceAssets.Any(l => l.assetsFolder != null); if (hasAnySourceAssetFolders == false) { // Disable this checkbox if there are no assets // available. using (new EditorGUI.DisabledScope(true)) { EditorGUILayout.Toggle(useAddressableAssetsProperty.displayName, false); } } else { EditorGUILayout.PropertyField(useAddressableAssetsProperty); // Show a warning if we've requested addressables but // haven't set it up. if (useAddressableAssetsProperty.boolValue && AddressableAssetSettingsDefaultObject.SettingsExists == false) { EditorGUILayout.HelpBox("Please set up Addressable Assets in this project.", MessageType.Warning); } } // Add a button for updating asset addresses, if any asset // source folders exist if (useAddressableAssetsProperty.boolValue && AddressableAssetSettingsDefaultObject.SettingsExists) { using (new EditorGUI.DisabledScope(hasAnySourceAssetFolders == false)) { if (GUILayout.Button($"Update Asset Addresses")) { YarnProjectUtility.UpdateAssetAddresses(yarnProjectImporter); } } } #endif } EditorGUILayout.Space(); EditorGUILayout.LabelField("Commands and Functions", EditorStyles.boldLabel); var searchAllAssembliesLabel = new GUIContent("Search All Assemblies", "Search all assembly definitions for commands and functions, as well as code that's not in a folder with an assembly definition"); EditorGUILayout.PropertyField(searchAllAssembliesProperty, searchAllAssembliesLabel); if (searchAllAssembliesProperty.boolValue == false) { EditorGUI.indentLevel += 1; EditorGUILayout.PropertyField(assembliesToSearchProperty); EditorGUI.indentLevel -= 1; } using (new EditorGUI.DisabledGroupScope(canGenerateStringsTable == false)) { if (GUILayout.Button("Export Strings as CSV")) { var currentPath = AssetDatabase.GetAssetPath(serializedObject.targetObject); var currentFileName = Path.GetFileNameWithoutExtension(currentPath); var currentDirectory = Path.GetDirectoryName(currentPath); var destinationPath = EditorUtility.SaveFilePanel("Export Strings CSV", currentDirectory, $"{currentFileName}.csv", "csv"); if (string.IsNullOrEmpty(destinationPath) == false) { // Generate the file on disk YarnProjectUtility.WriteStringsFile(destinationPath, yarnProjectImporter); // destinationPath may have been inside our Assets // directory, so refresh the asset database AssetDatabase.Refresh(); } } if (yarnProjectImporter.languagesToSourceAssets.Count > 0) { if (GUILayout.Button("Update Existing Strings Files")) { YarnProjectUtility.UpdateLocalizationCSVs(yarnProjectImporter); } } } // Does this project's source scripts list contain any actual // assets? (It can have a count of >0 and still have no assets // when, for example, you've just clicked the + button but // haven't dragged an asset in yet.) var hasAnyTextAssets = yarnProjectImporter.sourceScripts.Where(s => s != null).Count() > 0; // Disable this button if 1. all lines already have tags or 2. // no actual source files exist using (new EditorGUI.DisabledScope(canGenerateStringsTable == true || hasAnyTextAssets == false)) { if (GUILayout.Button("Add Line Tags to Scripts")) { YarnProjectUtility.AddLineTagsToFilesInYarnProject(yarnProjectImporter); } } var hadChanges = serializedObject.ApplyModifiedProperties(); #if UNITY_2018 // Unity 2018's ApplyRevertGUI is buggy, and doesn't // automatically detect changes to the importer's // serializedObject. This means that we'd need to track the // state of the importer, and don't have a way to present a // Revert button. // // Rather than offer a broken experience, on Unity 2018 we // immediately reimport the changes. This is slow (we're // serializing and writing the asset to disk on every property // change!) but ensures that the writes are done. if (hadChanges) { // Manually perform the same tasks as the 'Apply' button // would ApplyAndImport(); } #endif #if UNITY_2019_1_OR_NEWER // On Unity 2019 and newer, we can use an ApplyRevertGUI that // works identically to the built-in importer inspectors. ApplyRevertGUI(); #endif }