Esempio n. 1
0
        public void TestPrepareForLine()
        {
            var path = Path.Combine(TestDataPath, "TaggedLines.yarn");

            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            Assert.Empty(result.Diagnostics);

            stringTable = result.StringTable;

            bool prepareForLinesWasCalled = false;

            dialogue.PrepareForLinesHandler = (lines) => {
                // When the Dialogue realises it's about to run the Start
                // node, it will tell us that it's about to run these two
                // line IDs
                Assert.Equal(2, lines.Count());
                Assert.Contains("line:test1", lines);
                Assert.Contains("line:test2", lines);

                // Ensure that these asserts were actually called
                prepareForLinesWasCalled = true;
            };

            dialogue.SetProgram(result.Program);
            dialogue.SetNode("Start");

            Assert.True(prepareForLinesWasCalled);
        }
Esempio n. 2
0
        public void TestInvalidCharactersInNodeTitle()
        {
            var path = Path.Combine(TestDataPath, "InvalidNodeTitle.yarn");

            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            Assert.NotEmpty(result.Diagnostics);
        }
Esempio n. 3
0
        public void TestInvalidCharactersInNodeTitle()
        {
            var path = Path.Combine(TestDataPath, "InvalidNodeTitle.yarn");

            Assert.Throws <Yarn.Compiler.ParseException>(() => {
                Compiler.Compile(CompilationJob.CreateFromFiles(path));
            });
        }
Esempio n. 4
0
        public void TestDumpingCode()
        {
            var path   = Path.Combine(TestDataPath, "Example.yarn");
            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            dialogue.SetProgram(result.Program);

            var byteCode = dialogue.GetByteCode();

            Assert.NotNull(byteCode);
        }
Esempio n. 5
0
        public void TestMissingNode()
        {
            var path = Path.Combine(TestDataPath, "TestCases", "Smileys.yarn");

            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            dialogue.SetProgram(result.Program);

            errorsCauseFailures = false;

            Assert.Throws <DialogueException>(() => dialogue.SetNode("THIS NODE DOES NOT EXIST"));
        }
Esempio n. 6
0
        public void TestAnalysis()
        {
            ICollection <Yarn.Analysis.Diagnosis> diagnoses;

            Yarn.Analysis.Context context;

            // this script has the following variables:
            // $foo is read from and written to
            // $bar is written to but never read
            // this means that there should be one diagnosis result
            context = new Yarn.Analysis.Context(typeof(Yarn.Analysis.UnusedVariableChecker));

            var path = Path.Combine(TestDataPath, "AnalysisTest.yarn");

            CompilationJob compilationJob = CompilationJob.CreateFromFiles(path);

            compilationJob.Library = dialogue.Library;

            var result = Compiler.Compile(compilationJob);

            Assert.Empty(result.Diagnostics);

            stringTable = result.StringTable;

            dialogue.SetProgram(result.Program);
            dialogue.Analyse(context);
            diagnoses = new List <Yarn.Analysis.Diagnosis>(context.FinishAnalysis());

            Assert.Equal(1, diagnoses.Count);
            Assert.Contains("Variable $bar is assigned, but never read from", diagnoses.First().message);

            dialogue.UnloadAll();

            context = new Yarn.Analysis.Context(typeof(Yarn.Analysis.UnusedVariableChecker));

            result = Compiler.Compile(CompilationJob.CreateFromFiles(new[] {
                Path.Combine(SpaceDemoScriptsPath, "Ship.yarn"),
                Path.Combine(SpaceDemoScriptsPath, "Sally.yarn"),
            }, dialogue.Library));

            Assert.Empty(result.Diagnostics);

            dialogue.SetProgram(result.Program);

            dialogue.Analyse(context);
            diagnoses = new List <Yarn.Analysis.Diagnosis>(context.FinishAnalysis());

            // This script should contain no unused variables
            Assert.Empty(diagnoses);
        }
Esempio n. 7
0
        public void TestExampleScript()
        {
            errorsCauseFailures = false;
            var path     = Path.Combine(TestDataPath, "Example.yarn");
            var testPath = Path.ChangeExtension(path, ".testplan");

            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            dialogue.SetProgram(result.Program);
            stringTable = result.StringTable;

            this.LoadTestPlan(testPath);

            RunStandardTestcase();
        }
Esempio n. 8
0
        public void TestGettingTags()
        {
            var path = Path.Combine(TestDataPath, "Example.yarn");

            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            dialogue.SetProgram(result.Program);

            var source = dialogue.GetTagsForNode("LearnMore");

            Assert.NotNull(source);

            Assert.NotEmpty(source);

            Assert.Equal("rawText", source.First());
        }
Esempio n. 9
0
        public void TestEndOfNotesWithOptionsNotAdded()
        {
            var path = Path.Combine(TestDataPath, "SkippedOptions.yarn");

            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            dialogue.SetProgram(result.Program);
            stringTable = result.StringTable;

            dialogue.OptionsHandler = delegate(OptionSet optionSets) {
                Assert.False(true, "Options should not be shown to the user in this test.");
            };

            dialogue.SetNode();
            dialogue.Continue();
        }
Esempio n. 10
0
        public void TestGettingRawSource()
        {
            var path = Path.Combine(TestDataPath, "Example.yarn");

            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            dialogue.SetProgram(result.Program);

            stringTable = result.StringTable;

            var sourceID = dialogue.GetStringIDForNode("LearnMore");
            var source   = stringTable[sourceID].text;

            Assert.NotNull(source);

            Assert.Equal("A: HAHAHA\n", source);
        }
Esempio n. 11
0
        public void TestLoadingNodes()
        {
            var path = Path.Combine(TestDataPath, "Projects", "Basic", "Test.yarn");

            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            dialogue.SetProgram(result.Program);
            stringTable = result.StringTable;

            // high-level test: load the file, verify it has the nodes we want,
            // and run one

            Assert.Equal(3, dialogue.NodeNames.Count());

            Assert.True(dialogue.NodeExists("TestNode"));
            Assert.True(dialogue.NodeExists("AnotherTestNode"));
            Assert.True(dialogue.NodeExists("ThirdNode"));
        }
Esempio n. 12
0
        public void TestNodeHeaders()
        {
            var path   = Path.Combine(TestDataPath, "Headers.yarn");
            var result = Compiler.Compile(CompilationJob.CreateFromFiles(path));

            Assert.Equal(4, result.Program.Nodes.Count);

            foreach (var tag in new[] { "one", "two", "three" })
            {
                Assert.Contains(tag, result.Program.Nodes["Tags"].Tags);
            }

            // Assert.Contains("version:2", result.FileTags);
            Assert.Contains(path, result.FileTags.Keys);
            Assert.Equal(1, result.FileTags.Count);
            Assert.Equal(1, result.FileTags[path].Count());
            Assert.Contains("file_header", result.FileTags[path]);
        }
Esempio n. 13
0
        public void TestNodeExists()
        {
            var path = Path.Combine(SpaceDemoScriptsPath, "Sally.yarn");

            CompilationJob compilationJob = CompilationJob.CreateFromFiles(path);

            compilationJob.Library = dialogue.Library;

            var result = Compiler.Compile(compilationJob);

            dialogue.SetProgram(result.Program);

            Assert.True(dialogue.NodeExists("Sally"));

            // Test clearing everything
            dialogue.UnloadAll();

            Assert.False(dialogue.NodeExists("Sally"));
        }
Esempio n. 14
0
        /// <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);
        }
Esempio n. 15
0
        public void TestMergingNodes()
        {
            var sallyPath = Path.Combine(SpaceDemoScriptsPath, "Sally.yarn");
            var shipPath  = Path.Combine(SpaceDemoScriptsPath, "Ship.yarn");

            CompilationJob compilationJobSally        = CompilationJob.CreateFromFiles(sallyPath);
            CompilationJob compilationJobSallyAndShip = CompilationJob.CreateFromFiles(sallyPath, shipPath);

            compilationJobSally.Library        = dialogue.Library;
            compilationJobSallyAndShip.Library = dialogue.Library;

            var resultSally        = Compiler.Compile(compilationJobSally);
            var resultSallyAndShip = Compiler.Compile(compilationJobSallyAndShip);

            // Loading code with the same contents should throw
            Assert.Throws <InvalidOperationException>(delegate()
            {
                var combinedNotWorking = Program.Combine(resultSally.Program, resultSallyAndShip.Program);
            });
        }
Esempio n. 16
0
        public void TestGettingCurrentNodeName()
        {
            string path = Path.Combine(SpaceDemoScriptsPath, "Sally.yarn");

            CompilationJob compilationJob = CompilationJob.CreateFromFiles(path);

            compilationJob.Library = dialogue.Library;

            var result = Compiler.Compile(compilationJob);

            dialogue.SetProgram(result.Program);

            // dialogue should not be running yet
            Assert.Null(dialogue.CurrentNode);

            dialogue.SetNode("Sally");
            Assert.Equal("Sally", dialogue.CurrentNode);

            dialogue.Stop();
            // Current node should now be null
            Assert.Null(dialogue.CurrentNode);
        }
Esempio n. 17
0
        public void TestUpgradingV1toV2(string directory)
        {
            Console.ForegroundColor = ConsoleColor.Blue;
            Console.WriteLine($"INFO: Loading file {directory}");

            storage.Clear();

            directory = Path.Combine(TestBase.TestDataPath, directory);

            var allInputYarnFiles = Directory.EnumerateFiles(directory)
                                    .Where(path => path.EndsWith(".yarn"))
                                    .Where(path => path.Contains(".upgraded.") == false);

            var expectedOutputFiles = Directory.EnumerateFiles(directory)
                                      .Where(path => path.Contains(".upgraded."));

            var testPlanPath = Directory.EnumerateFiles(directory)
                               .Where(path => path.EndsWith(".testplan"))
                               .FirstOrDefault();

            var upgradeJob = new UpgradeJob(
                UpgradeType.Version1to2,
                allInputYarnFiles.Select(path => new CompilationJob.File {
                FileName = path,
                Source   = File.ReadAllText(path)
            }));

            var upgradeResult = LanguageUpgrader.Upgrade(upgradeJob);

            // The upgrade result should produce as many files as there are
            // expected output files
            Assert.Equal(expectedOutputFiles.Count(), upgradeResult.Files.Count());

            // For each file produced by the upgrade job, its content
            // should match that of the corresponding expected output
            foreach (var outputFile in upgradeResult.Files)
            {
                string extension = Path.GetExtension(outputFile.Path);
                var    expectedOutputFilePath = Path.ChangeExtension(outputFile.Path, ".upgraded" + extension);

                if (expectedOutputFiles.Contains(expectedOutputFilePath) == false)
                {
                    // This test case doesn't expect this output (perhaps
                    // it's a test case that isn't expected to succeed.) Ignore it.
                    continue;
                }

                Assert.True(File.Exists(expectedOutputFilePath), $"Expected file {expectedOutputFilePath} to exist");

                var expectedOutputFileContents = File.ReadAllText(expectedOutputFilePath);

                Assert.Equal(expectedOutputFileContents, outputFile.UpgradedSource);
            }

            // If the test case doesn't contain a test plan file, it's not
            // expected to compile successfully, so don't do it. Instead,
            // we'll rely on the fact that the upgraded contents are what
            // we expected.
            if (testPlanPath == null)
            {
                // Don't compile; just succeed here.
                return;
            }

            // While we're here, correctness-check the upgraded source. (To
            // be strictly correct, we're using the files on disk, not the
            // generated source, but we just demonstrated that they're
            // identical, so that's fine! Saves us having to write them to
            // a temporary location.)

            var result = Compiler.Compile(CompilationJob.CreateFromFiles(expectedOutputFiles));

            Assert.Empty(result.Diagnostics);

            stringTable = result.StringTable;

            // Execute the program and verify thats output matches the test
            // plan
            dialogue.SetProgram(result.Program);

            // Load the test plan
            LoadTestPlan(testPlanPath);

            // If this file contains a Start node, run the test case
            // (otherwise, we're just testing its parsability, which we did
            // in the last line)
            if (dialogue.NodeExists("Start"))
            {
                RunStandardTestcase();
            }
        }
Esempio n. 18
0
        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
        }