Dialogue.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. /*
  2. The MIT License (MIT)
  3. Copyright (c) 2015-2017 Secret Lab Pty. Ltd. and Yarn Spinner contributors.
  4. Permission is hereby granted, free of charge, to any person obtaining a copy
  5. of this software and associated documentation files (the "Software"), to deal
  6. in the Software without restriction, including without limitation the rights
  7. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in all
  11. copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  17. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  18. SOFTWARE.
  19. */
  20. using System;
  21. using System.Collections;
  22. using System.Collections.Generic;
  23. namespace Yarn {
  24. /// Represents things that can go wrong while loading or running a dialogue.
  25. public class YarnException : Exception {
  26. public YarnException(string message) : base(message) {}
  27. }
  28. // Delegates, which are used by the client.
  29. /// OptionChoosers let the client tell the Dialogue about what
  30. /// response option the user selected.
  31. public delegate void OptionChooser (int selectedOptionIndex);
  32. /// Loggers let the client send output to a console, for both debugging
  33. /// and error logging.
  34. public delegate void Logger(string message);
  35. /// Information about stuff that the client should handle.
  36. /** (Currently this just wraps a single field, but doing it like this
  37. * gives us the option to add more stuff later without breaking the API.)
  38. */
  39. public struct Line { public string text; }
  40. public struct Options { public IList<string> options; }
  41. public struct Command { public string text; }
  42. /// Where we turn to for storing and loading variable data.
  43. public interface VariableStorage {
  44. [Obsolete] void SetNumber(string variableName, float number);
  45. [Obsolete] float GetNumber(string variableName);
  46. void SetValue(string variableName, Value value);
  47. Value GetValue(string variableName);
  48. void Clear();
  49. }
  50. public abstract class BaseVariableStorage : VariableStorage {
  51. [Obsolete]
  52. public void SetNumber(string variableName, float number) {
  53. this.SetValue(variableName, new Value(number));
  54. }
  55. [Obsolete]
  56. public float GetNumber(string variableName) {
  57. return this.GetValue(variableName).AsNumber;
  58. }
  59. public abstract void SetValue(string variableName, Value value);
  60. public abstract Value GetValue(string variableName);
  61. public abstract void Clear();
  62. }
  63. /// A line, localised into the current locale.
  64. /** LocalisedLines are used in both lines, options, and shortcut options - basically,
  65. * anything user-facing.
  66. */
  67. public class LocalisedLine
  68. {
  69. public string LineCode { get; set; }
  70. public string LineText { get; set; }
  71. public string Comment { get; set; }
  72. }
  73. /// Very simple continuity class that keeps all variables in memory
  74. public class MemoryVariableStore : Yarn.BaseVariableStorage
  75. {
  76. Dictionary<string, Value> variables = new Dictionary<string, Value>();
  77. public override void SetValue(string variableName, Value value)
  78. {
  79. variables[variableName] = value;
  80. }
  81. public override Value GetValue(string variableName)
  82. {
  83. Value value = Value.NULL;
  84. if (variables.ContainsKey(variableName))
  85. {
  86. value = variables[variableName];
  87. }
  88. return value;
  89. }
  90. public override void Clear()
  91. {
  92. variables.Clear();
  93. }
  94. }
  95. /// The Dialogue class is the main thing that clients will use.
  96. public class Dialogue {
  97. internal VariableStorage continuity;
  98. /// We'll ask this object for the state of variables
  99. public bool experimentalMode = false;
  100. // currently this is just the ANTLR compiler but it could change over time
  101. // used to determine if the dialogue should use the experimental features
  102. /// Represents something for the end user ("client") of the Dialogue class to do.
  103. public abstract class RunnerResult { }
  104. /// The client should run a line of dialogue.
  105. public class LineResult : RunnerResult {
  106. public Line line;
  107. public LineResult (string text) {
  108. var line = new Line();
  109. line.text = text;
  110. this.line = line;
  111. }
  112. }
  113. /// The client should run a command (it's up to them to parse the string)
  114. public class CommandResult: RunnerResult {
  115. public Command command;
  116. public CommandResult (string text) {
  117. var command = new Command();
  118. command.text = text;
  119. this.command = command;
  120. }
  121. }
  122. /// The client should show a list of options, and call
  123. /// setSelectedOptionDelegate before asking for the
  124. /// next line. It's an error if you don't.
  125. public class OptionSetResult : RunnerResult {
  126. public Options options;
  127. public OptionChooser setSelectedOptionDelegate;
  128. public OptionSetResult (IList<string> optionStrings, OptionChooser setSelectedOption) {
  129. var options = new Options();
  130. options.options = optionStrings;
  131. this.options = options;
  132. this.setSelectedOptionDelegate = setSelectedOption;
  133. }
  134. }
  135. /// We've reached the end of this node.
  136. public class NodeCompleteResult: RunnerResult {
  137. public string nextNode;
  138. public NodeCompleteResult (string nextNode) {
  139. this.nextNode = nextNode;
  140. }
  141. }
  142. /// Delegates used for logging.
  143. public Logger LogDebugMessage;
  144. public Logger LogErrorMessage;
  145. /// The node we start from.
  146. public const string DEFAULT_START = "Start";
  147. /// The loader contains all of the nodes we're going to run.
  148. internal Loader loader;
  149. /// The Program is the compiled Yarn program.
  150. internal Program program;
  151. /// The library contains all of the functions and operators we know about.
  152. public Library library;
  153. /// The collection of nodes that we've seen.
  154. public Dictionary<String, int> visitedNodeCount = new Dictionary<string, int>();
  155. /// A function exposed to Yarn that returns the number of times a node has been run.
  156. /** If no parameters are supplied, returns the number of time the current node
  157. * has been run.
  158. */
  159. object YarnFunctionNodeVisitCount (Value[] parameters)
  160. {
  161. // Determine the node we're checking
  162. string nodeName;
  163. if (parameters.Length == 0) {
  164. // No parameters? Check the current node
  165. nodeName = vm.currentNodeName;
  166. } else if (parameters.Length == 1) {
  167. // A parameter? Check the named node
  168. nodeName = parameters [0].AsString;
  169. // Ensure this node exists
  170. if (NodeExists (nodeName) == false) {
  171. var errorMessage = string.Format ("The node {0} does not " + "exist.", nodeName);
  172. LogErrorMessage (errorMessage);
  173. return 0;
  174. }
  175. } else {
  176. // We got too many parameters
  177. var errorMessage = string.Format ("Incorrect number of parameters to " + "visitCount (expected 0 or 1, got {0})", parameters.Length);
  178. LogErrorMessage (errorMessage);
  179. return 0;
  180. }
  181. // Figure out how many times this node was run
  182. int visitCount = 0;
  183. visitedNodeCount.TryGetValue (nodeName, out visitCount);
  184. return visitCount;
  185. }
  186. /// A Yarn function that returns true if the named node, or the current node
  187. /// if no parameters were provided, has been visited at least once.
  188. object YarnFunctionIsNodeVisited (Value[] parameters)
  189. {
  190. return (int)YarnFunctionNodeVisitCount(parameters) > 0;
  191. }
  192. public Dialogue(Yarn.VariableStorage continuity) {
  193. this.continuity = continuity;
  194. loader = new Loader (this);
  195. library = new Library ();
  196. library.ImportLibrary (new StandardLibrary ());
  197. // Register the "visited" function, which returns true if we've visited
  198. // a node previously (nodes are marked as visited when we leave them)
  199. library.RegisterFunction ("visited", -1, (ReturningFunction)YarnFunctionIsNodeVisited);
  200. // Register the "visitCount" function, which returns the number of times
  201. // a node has been run (which increments when a node ends). If called with
  202. // no parameters, check the CURRENT node.
  203. library.RegisterFunction ("visitCount", -1, (ReturningFunction)YarnFunctionNodeVisitCount);
  204. }
  205. /// Load a file from disk.
  206. public void LoadFile(string fileName, bool showTokens = false, bool showParseTree = false, string onlyConsiderNode=null) {
  207. // Is this a compiled program file?
  208. if (fileName.EndsWith(".yarn.bytes")) {
  209. var bytes = System.IO.File.ReadAllBytes(fileName);
  210. LoadCompiledProgram(bytes, fileName);
  211. return;
  212. } else {
  213. // It's source code, either a single node in text form or a JSON file
  214. string inputString;
  215. using (System.IO.StreamReader reader = new System.IO.StreamReader(fileName))
  216. {
  217. inputString = reader.ReadToEnd();
  218. }
  219. LoadString(inputString, fileName, showTokens, showParseTree, onlyConsiderNode);
  220. }
  221. }
  222. public void LoadCompiledProgram(byte[] bytes, string fileName, CompiledFormat format = LATEST_FORMAT)
  223. {
  224. if (LogDebugMessage == null)
  225. {
  226. throw new YarnException("LogDebugMessage must be set before loading");
  227. }
  228. if (LogErrorMessage == null)
  229. {
  230. throw new YarnException("LogErrorMessage must be set before loading");
  231. }
  232. switch (format)
  233. {
  234. case CompiledFormat.V1:
  235. LoadCompiledProgramV1(bytes);
  236. break;
  237. default:
  238. throw new ArgumentOutOfRangeException();
  239. }
  240. }
  241. private void LoadCompiledProgramV1(byte[] bytes)
  242. {
  243. using (var stream = new System.IO.MemoryStream(bytes))
  244. {
  245. using (var reader = new Newtonsoft.Json.Bson.BsonReader(stream))
  246. {
  247. var serializer = new Newtonsoft.Json.JsonSerializer();
  248. try
  249. {
  250. // Load the stored program
  251. var newProgram = serializer.Deserialize<Program>(reader);
  252. // Merge it with our existing one, if present
  253. if (program != null)
  254. {
  255. program.Include(newProgram);
  256. }
  257. else {
  258. program = newProgram;
  259. }
  260. }
  261. catch (Newtonsoft.Json.JsonReaderException e)
  262. {
  263. LogErrorMessage(string.Format("Cannot load compiled program: {0}", e.Message));
  264. }
  265. }
  266. }
  267. }
  268. /// Ask the loader to parse a string.
  269. /** Returns the number of nodes that were loaded.
  270. */
  271. public void LoadString(string text, string fileName="<input>", bool showTokens=false, bool showParseTree=false, string onlyConsiderNode=null) {
  272. if (LogDebugMessage == null) {
  273. throw new YarnException ("LogDebugMessage must be set before loading");
  274. }
  275. if (LogErrorMessage == null) {
  276. throw new YarnException ("LogErrorMessage must be set before loading");
  277. }
  278. // Try to infer the type
  279. NodeFormat format;
  280. if (text.StartsWith("[", StringComparison.Ordinal)) {
  281. // starts with a {? this is probably a JSON array
  282. format = NodeFormat.JSON;
  283. } else if (text.Contains("---")) {
  284. // contains a --- delimiter? probably multi node text
  285. format = NodeFormat.Text;
  286. } else {
  287. // fall back to the single node format
  288. format = NodeFormat.SingleNodeText;
  289. }
  290. program = loader.Load(text, library, fileName, program, showTokens, showParseTree, onlyConsiderNode, format, this.experimentalMode);
  291. }
  292. private VirtualMachine vm;
  293. // Executes a node.
  294. /** Use this in a for-each construct; each time you iterate over it,
  295. * you'll get a line, command, or set of options.
  296. */
  297. public IEnumerable<Yarn.Dialogue.RunnerResult> Run(string startNode = DEFAULT_START) {
  298. if (LogDebugMessage == null) {
  299. throw new YarnException ("LogDebugMessage must be set before running");
  300. }
  301. if (LogErrorMessage == null) {
  302. throw new YarnException ("LogErrorMessage must be set before running");
  303. }
  304. if (program == null) {
  305. LogErrorMessage ("Dialogue.Run was called, but no program was loaded. Stopping.");
  306. yield break;
  307. }
  308. vm = new VirtualMachine (this, program);
  309. RunnerResult latestResult;
  310. vm.lineHandler = delegate(LineResult result) {
  311. latestResult = result;
  312. };
  313. vm.commandHandler = delegate(CommandResult result) {
  314. // Is it the special custom command "<<stop>>"?
  315. if (result is CommandResult && (result as CommandResult).command.text == "stop") {
  316. vm.Stop();
  317. }
  318. latestResult = result;
  319. };
  320. vm.nodeCompleteHandler = delegate(NodeCompleteResult result) {
  321. // get the count if it's there, otherwise it defaults to 0
  322. int count = 0;
  323. visitedNodeCount.TryGetValue(vm.currentNodeName, out count);
  324. visitedNodeCount[vm.currentNodeName] = count + 1;
  325. latestResult = result;
  326. };
  327. vm.optionsHandler = delegate(OptionSetResult result) {
  328. latestResult = result;
  329. };
  330. if (vm.SetNode (startNode) == false) {
  331. yield break;
  332. }
  333. // Run until the program stops, pausing to yield important
  334. // results
  335. do {
  336. latestResult = null;
  337. vm.RunNext ();
  338. if (latestResult != null)
  339. yield return latestResult;
  340. } while (vm.executionState != VirtualMachine.ExecutionState.Stopped);
  341. }
  342. public void Stop() {
  343. if (vm != null)
  344. vm.Stop();
  345. }
  346. public IEnumerable<string> visitedNodes {
  347. get {
  348. return visitedNodeCount.Keys;
  349. }
  350. set {
  351. visitedNodeCount = new Dictionary<string, int>();
  352. foreach (var entry in value) {
  353. visitedNodeCount[entry] = 1;
  354. }
  355. }
  356. }
  357. public IEnumerable<string> allNodes {
  358. get {
  359. return program.nodes.Keys;
  360. }
  361. }
  362. public string currentNode {
  363. get {
  364. if (vm == null) {
  365. return null;
  366. } else {
  367. return vm.currentNodeName;
  368. }
  369. }
  370. }
  371. public Dictionary<string, string> GetTextForAllNodes() {
  372. var d = new Dictionary<string,string>();
  373. foreach (var node in program.nodes) {
  374. var text = program.GetTextForNode(node.Key);
  375. if (text == null)
  376. continue;
  377. d [node.Key] = text;
  378. }
  379. return d;
  380. }
  381. /// Returns the source code for the node 'nodeName', if that node was tagged with rawText.
  382. public string GetTextForNode(string nodeName) {
  383. if (program.nodes.Count == 0) {
  384. LogErrorMessage ("No nodes are loaded!");
  385. return null;
  386. } else if (program.nodes.ContainsKey(nodeName)) {
  387. return program.GetTextForNode (nodeName);
  388. } else {
  389. LogErrorMessage ("No node named " + nodeName);
  390. return null;
  391. }
  392. }
  393. public void AddStringTable(Dictionary<string, string> stringTable)
  394. {
  395. program.LoadStrings(stringTable);
  396. }
  397. public Dictionary<string,string> GetStringTable() {
  398. return program.strings;
  399. }
  400. internal Dictionary<string,LineInfo> GetStringInfoTable() {
  401. return program.lineInfo;
  402. }
  403. public enum CompiledFormat
  404. {
  405. V1
  406. }
  407. public const CompiledFormat LATEST_FORMAT = CompiledFormat.V1;
  408. public byte[] GetCompiledProgram(CompiledFormat format = LATEST_FORMAT)
  409. {
  410. switch (format)
  411. {
  412. case CompiledFormat.V1:
  413. return GetCompiledProgramV1();
  414. default:
  415. throw new ArgumentOutOfRangeException();
  416. }
  417. }
  418. private byte[] GetCompiledProgramV1()
  419. {
  420. using (var outputStream = new System.IO.MemoryStream())
  421. {
  422. using (var bsonWriter = new Newtonsoft.Json.Bson.BsonWriter(outputStream))
  423. {
  424. var s = new Newtonsoft.Json.JsonSerializer();
  425. s.Serialize(bsonWriter, this.program);
  426. }
  427. return outputStream.ToArray();
  428. }
  429. }
  430. /// Unloads ALL nodes.
  431. public void UnloadAll(bool clearVisitedNodes = true) {
  432. if (clearVisitedNodes)
  433. visitedNodeCount.Clear();
  434. program = null;
  435. }
  436. [Obsolete("Calling Compile() is no longer necessary.")]
  437. public String Compile() {
  438. return program.DumpCode (library);
  439. }
  440. public String GetByteCode() {
  441. return program.DumpCode (library);
  442. }
  443. public bool NodeExists(string nodeName) {
  444. if (program == null) {
  445. if (program.nodes.Count > 0) {
  446. LogErrorMessage ("Internal consistency error: Called NodeExists, and " +
  447. "there are nodes loaded, but the program hasn't " +
  448. "been compiled yet, somehow?");
  449. return false;
  450. } else {
  451. LogErrorMessage ("Tried to call NodeExists, but no nodes " +
  452. "have been compiled!");
  453. return false;
  454. }
  455. }
  456. if (program.nodes == null || program.nodes.Count == 0) {
  457. LogDebugMessage ("Called NodeExists, but there are zero nodes. " +
  458. "This may be an error.");
  459. return false;
  460. }
  461. return program.nodes.ContainsKey(nodeName);
  462. }
  463. public void Analyse(Analysis.Context context) {
  464. context.AddProgramToAnalysis (this.program);
  465. }
  466. /// The standard, built-in library of functions and operators.
  467. private class StandardLibrary : Library {
  468. public StandardLibrary() {
  469. #region Operators
  470. this.RegisterFunction(TokenType.Add.ToString(), 2, delegate(Value[] parameters) {
  471. return parameters[0] + parameters[1];
  472. });
  473. this.RegisterFunction(TokenType.Minus.ToString(), 2, delegate(Value[] parameters) {
  474. return parameters[0] - parameters[1];
  475. });
  476. this.RegisterFunction(TokenType.UnaryMinus.ToString(), 1, delegate(Value[] parameters) {
  477. return -parameters[0];
  478. });
  479. this.RegisterFunction(TokenType.Divide.ToString(), 2, delegate(Value[] parameters) {
  480. return parameters[0] / parameters[1];
  481. });
  482. this.RegisterFunction(TokenType.Multiply.ToString(), 2, delegate(Value[] parameters) {
  483. return parameters[0] * parameters[1];
  484. });
  485. this.RegisterFunction(TokenType.Modulo.ToString(), 2, delegate(Value[] parameters) {
  486. return parameters[0] % parameters[1];
  487. });
  488. this.RegisterFunction(TokenType.EqualTo.ToString(), 2, delegate(Value[] parameters) {
  489. return parameters[0].Equals( parameters[1] );
  490. });
  491. this.RegisterFunction(TokenType.NotEqualTo.ToString(), 2, delegate(Value[] parameters) {
  492. // Return the logical negative of the == operator's result
  493. var equalTo = this.GetFunction(TokenType.EqualTo.ToString());
  494. return !equalTo.Invoke(parameters).AsBool;
  495. });
  496. this.RegisterFunction(TokenType.GreaterThan.ToString(), 2, delegate(Value[] parameters) {
  497. return parameters[0] > parameters[1];
  498. });
  499. this.RegisterFunction(TokenType.GreaterThanOrEqualTo.ToString(), 2, delegate(Value[] parameters) {
  500. return parameters[0] >= parameters[1];
  501. });
  502. this.RegisterFunction(TokenType.LessThan.ToString(), 2, delegate(Value[] parameters) {
  503. return parameters[0] < parameters[1];
  504. });
  505. this.RegisterFunction(TokenType.LessThanOrEqualTo.ToString(), 2, delegate(Value[] parameters) {
  506. return parameters[0] <= parameters[1];
  507. });
  508. this.RegisterFunction(TokenType.And.ToString(), 2, delegate(Value[] parameters) {
  509. return parameters[0].AsBool && parameters[1].AsBool;
  510. });
  511. this.RegisterFunction(TokenType.Or.ToString(), 2, delegate(Value[] parameters) {
  512. return parameters[0].AsBool || parameters[1].AsBool;
  513. });
  514. this.RegisterFunction(TokenType.Xor.ToString(), 2, delegate(Value[] parameters) {
  515. return parameters[0].AsBool ^ parameters[1].AsBool;
  516. });
  517. this.RegisterFunction(TokenType.Not.ToString(), 1, delegate(Value[] parameters) {
  518. return !parameters[0].AsBool;
  519. });
  520. #endregion Operators
  521. }
  522. }
  523. }
  524. }