private void UpdateDestinationProject()
        {
            destinationYarnProject = (target as YarnImporter).DestinationProject;

            if (destinationYarnProject != null)
            {
                destinationYarnProjectImporter = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(destinationYarnProject)) as YarnProjectImporter;
            }
        }
Beispiel #2
0
        /// <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();
            }
        }
Beispiel #3
0
        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
        }
Beispiel #4
0
        /// <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);
        }
Beispiel #5
0
        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);
        }
Beispiel #6
0
        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.");
            }
        }
Beispiel #7
0
        /// <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
        }