Loader.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  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. // Comment out to not catch exceptions
  21. #define CATCH_EXCEPTIONS
  22. using System;
  23. using System.Collections.Generic;
  24. using Newtonsoft.Json;
  25. using Antlr4.Runtime;
  26. using Antlr4.Runtime.Misc;
  27. using Antlr4.Runtime.Tree;
  28. using System.Text;
  29. using System.IO;
  30. using System.Linq;
  31. namespace Yarn {
  32. public enum NodeFormat
  33. {
  34. Unknown, // an unknown type
  35. SingleNodeText, // a plain text file containing a single node with no metadata
  36. JSON, // a JSON file containing multiple nodes with metadata
  37. Text, // a text file containing multiple nodes with metadata
  38. }
  39. internal class Loader {
  40. private Dialogue dialogue;
  41. public Program program { get; private set; }
  42. // Prints out the list of tokens that the tokeniser found for this node
  43. void PrintTokenList(IEnumerable<Token> tokenList) {
  44. // Sum up the result
  45. var sb = new System.Text.StringBuilder();
  46. foreach (var t in tokenList) {
  47. sb.AppendLine (string.Format("{0} ({1} line {2})", t.ToString (), t.context, t.lineNumber));
  48. }
  49. // Let's see what we got
  50. dialogue.LogDebugMessage("Tokens:");
  51. dialogue.LogDebugMessage(sb.ToString());
  52. }
  53. // Prints the parse tree for the node
  54. void PrintParseTree(Yarn.Parser.ParseNode rootNode) {
  55. dialogue.LogDebugMessage("Parse Tree:");
  56. dialogue.LogDebugMessage(rootNode.PrintTree(0));
  57. }
  58. // Prepares a loader. 'implementation' is used for logging.
  59. public Loader(Dialogue dialogue) {
  60. if (dialogue == null)
  61. throw new ArgumentNullException ("dialogue");
  62. this.dialogue = dialogue;
  63. }
  64. // the preprocessor that cleans up things to make it easier on ANTLR
  65. // replaces \r\n with \n
  66. // adds in INDENTS and DEDENTS where necessary
  67. // replaces \t with four spaces
  68. // takes in a string of yarn and returns a string the compiler can then use
  69. private struct EmissionTuple
  70. {
  71. public int depth;
  72. public bool emitted;
  73. public EmissionTuple(int depth, bool emitted)
  74. {
  75. this.depth = depth;
  76. this.emitted = emitted;
  77. }
  78. }
  79. private string preprocessor(string nodeText)
  80. {
  81. string processed = null;
  82. using (StringReader reader = new StringReader(nodeText))
  83. {
  84. // a list to hold outputLines once they have been cleaned up
  85. List<string> outputLines = new List<string>();
  86. // a stack to keep track of how far indented we are
  87. // made up of ints and bools
  88. // ints track the depth, bool tracks if we emitted an indent token
  89. // starts with 0 and false so we can never fall off the end of the stack
  90. Stack<EmissionTuple> indents = new Stack<EmissionTuple>();
  91. indents.Push(new EmissionTuple(0, false));
  92. // a bool to determine if we are in a mode where we need to track indents
  93. bool shouldTrackNextIndentation = false;
  94. char INDENT = '\a';
  95. char DEDENT = '\v';
  96. //string INDENT = "{";
  97. //string DEDENT = "}";
  98. string OPTION = "->";
  99. string line;
  100. while ((line = reader.ReadLine()) != null)
  101. {
  102. // replacing \t with 4 spaces
  103. string tweakedLine = line.Replace("\t", " ");
  104. // stripping of any trailing newlines, will add them back in later
  105. tweakedLine = tweakedLine.TrimEnd('\r', '\n');
  106. // getting the number of indents on this line
  107. int lineIndent = tweakedLine.TakeWhile(Char.IsWhiteSpace).Count();
  108. // working out if it is an option (ie does it start with ->)
  109. bool isOption = tweakedLine.TrimStart(' ').StartsWith(OPTION);
  110. // are we in a state where we need to track indents?
  111. var previous = indents.Peek();
  112. if (shouldTrackNextIndentation && (lineIndent > previous.depth))
  113. {
  114. indents.Push(new EmissionTuple(lineIndent, true));
  115. // adding an indent to the stream
  116. // tries to add it to the end of the previous line where possible
  117. if (outputLines.Count == 0)
  118. {
  119. tweakedLine = INDENT + tweakedLine;
  120. }
  121. else
  122. {
  123. outputLines[outputLines.Count - 1] = outputLines[outputLines.Count - 1] + INDENT;
  124. }
  125. shouldTrackNextIndentation = false;
  126. }
  127. // have we finished with the current block of statements
  128. else if (lineIndent < previous.depth)
  129. {
  130. while (lineIndent < indents.Peek().depth)
  131. {
  132. var topLevel = indents.Pop();
  133. if (topLevel.emitted)
  134. {
  135. // adding dedents
  136. if (outputLines.Count == 0)
  137. {
  138. tweakedLine = DEDENT + tweakedLine;
  139. }
  140. else
  141. {
  142. outputLines[outputLines.Count - 1] = outputLines[outputLines.Count - 1] + DEDENT;
  143. }
  144. }
  145. }
  146. }
  147. else
  148. {
  149. shouldTrackNextIndentation = false;
  150. }
  151. // do we need to track the indents for the next statement?
  152. if (isOption)
  153. {
  154. shouldTrackNextIndentation = true;
  155. if (indents.Peek().depth < lineIndent)
  156. {
  157. indents.Push(new EmissionTuple(lineIndent, false));
  158. }
  159. }
  160. outputLines.Add(tweakedLine);
  161. }
  162. // mash it all back together now
  163. StringBuilder builder = new StringBuilder();
  164. foreach (string outLine in outputLines)
  165. {
  166. builder.Append(outLine);
  167. builder.Append("\n");
  168. }
  169. processed = builder.ToString();
  170. }
  171. return processed;
  172. }
  173. // Given a bunch of raw text, load all nodes that were inside it.
  174. // You can call this multiple times to append to the collection of nodes,
  175. // but note that new nodes will replace older ones with the same name.
  176. // Returns the number of nodes that were loaded.
  177. public Program Load(string text, Library library, string fileName, Program includeProgram, bool showTokens, bool showParseTree, string onlyConsiderNode, NodeFormat format, bool experimentalMode = false)
  178. {
  179. if (format == NodeFormat.Unknown)
  180. {
  181. format = GetFormatFromFileName(fileName);
  182. }
  183. // currently experimental node can only be used on yarn.txt yarn files and single nodes
  184. if (experimentalMode && (format == NodeFormat.Text || format == NodeFormat.SingleNodeText))
  185. {
  186. // this isn't the greatest...
  187. if (format == NodeFormat.SingleNodeText)
  188. {
  189. // it is just the body
  190. // need to add a dummy header and body delimiters
  191. StringBuilder builder = new StringBuilder();
  192. builder.Append("title:Start\n");
  193. builder.Append("---\n");
  194. builder.Append(text);
  195. builder.Append("\n===\n");
  196. text = builder.ToString();
  197. }
  198. string inputString = preprocessor(text);
  199. ICharStream input = CharStreams.fromstring(inputString);
  200. YarnSpinnerLexer lexer = new YarnSpinnerLexer(input);
  201. CommonTokenStream tokens = new CommonTokenStream(lexer);
  202. YarnSpinnerParser parser = new YarnSpinnerParser(tokens);
  203. // turning off the normal error listener and using ours
  204. parser.RemoveErrorListeners();
  205. parser.AddErrorListener(ErrorListener.Instance);
  206. IParseTree tree = parser.dialogue();
  207. AntlrCompiler antlrcompiler = new AntlrCompiler(library);
  208. antlrcompiler.Compile(tree);
  209. // merging in the other program if requested
  210. if (includeProgram != null)
  211. {
  212. antlrcompiler.program.Include(includeProgram);
  213. }
  214. return antlrcompiler.program;
  215. }
  216. else
  217. {
  218. // The final parsed nodes that were in the file we were given
  219. Dictionary<string, Yarn.Parser.Node> nodes = new Dictionary<string, Parser.Node>();
  220. // Load the raw data and get the array of node title-text pairs
  221. var nodeInfos = GetNodesFromText(text, format);
  222. int nodesLoaded = 0;
  223. foreach (NodeInfo nodeInfo in nodeInfos)
  224. {
  225. if (onlyConsiderNode != null && nodeInfo.title != onlyConsiderNode)
  226. continue;
  227. // Attempt to parse every node; log if we encounter any errors
  228. #if CATCH_EXCEPTIONS
  229. try
  230. {
  231. #endif
  232. if (nodeInfo.title == null)
  233. {
  234. throw new InvalidOperationException("Tried to load a node with no title.");
  235. }
  236. if (nodes.ContainsKey(nodeInfo.title))
  237. {
  238. throw new InvalidOperationException("Attempted to load a node called " +
  239. nodeInfo.title + ", but a node with that name has already been loaded!");
  240. }
  241. var lexer = new Lexer();
  242. var tokens = lexer.Tokenise(nodeInfo.title, nodeInfo.body);
  243. if (showTokens)
  244. PrintTokenList(tokens);
  245. var node = new Parser(tokens, library).Parse();
  246. // If this node is tagged "rawText", then preserve its source
  247. if (string.IsNullOrEmpty(nodeInfo.tags) == false &&
  248. nodeInfo.tags.Contains("rawText"))
  249. {
  250. node.source = nodeInfo.body;
  251. }
  252. node.name = nodeInfo.title;
  253. node.nodeTags = nodeInfo.tagsList;
  254. if (showParseTree)
  255. PrintParseTree(node);
  256. nodes[nodeInfo.title] = node;
  257. nodesLoaded++;
  258. #if CATCH_EXCEPTIONS
  259. }
  260. catch (Yarn.TokeniserException t)
  261. {
  262. // Add file information
  263. var message = string.Format("In file {0}: Error reading node {1}: {2}", fileName, nodeInfo.title, t.Message);
  264. throw new Yarn.TokeniserException(message);
  265. }
  266. catch (Yarn.ParseException p)
  267. {
  268. var message = string.Format("In file {0}: Error parsing node {1}: {2}", fileName, nodeInfo.title, p.Message);
  269. throw new Yarn.ParseException(message);
  270. }
  271. catch (InvalidOperationException e)
  272. {
  273. var message = string.Format("In file {0}: Error reading node {1}: {2}", fileName, nodeInfo.title, e.Message);
  274. throw new InvalidOperationException(message);
  275. }
  276. #endif
  277. }
  278. var compiler = new Yarn.Compiler(fileName);
  279. foreach (var node in nodes)
  280. {
  281. compiler.CompileNode(node.Value);
  282. }
  283. if (includeProgram != null)
  284. {
  285. compiler.program.Include(includeProgram);
  286. }
  287. return compiler.program;
  288. }
  289. }
  290. // The raw text of the Yarn node, plus metadata
  291. // All properties are serialised except tagsList, which is a derived property
  292. [JsonObject(MemberSerialization.OptOut)]
  293. public struct NodeInfo {
  294. public struct Position {
  295. public int x { get; set; }
  296. public int y { get; set; }
  297. }
  298. public string title { get; set; }
  299. public string body { get; set; }
  300. // The raw "tags" field, containing space-separated tags. This is written
  301. // to the file.
  302. public string tags { get; set; }
  303. public int colorID { get; set; }
  304. public Position position { get; set; }
  305. // The tags for this node, as a list of individual strings.
  306. [JsonIgnore]
  307. public List<string> tagsList
  308. {
  309. get
  310. {
  311. // If we have no tags list, or it's empty, return the empty list
  312. if (tags == null || tags.Length == 0) {
  313. return new List<string>();
  314. }
  315. return new List<string>(tags.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
  316. }
  317. }
  318. }
  319. internal static NodeFormat GetFormatFromFileName(string fileName)
  320. {
  321. NodeFormat format;
  322. if (fileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
  323. {
  324. format = NodeFormat.JSON;
  325. }
  326. else if (fileName.EndsWith(".yarn.txt", StringComparison.OrdinalIgnoreCase))
  327. {
  328. format = NodeFormat.Text;
  329. }
  330. else if (fileName.EndsWith(".node", StringComparison.OrdinalIgnoreCase))
  331. {
  332. format = NodeFormat.SingleNodeText;
  333. }
  334. else {
  335. throw new FormatException(string.Format("Unknown file format for file '{0}'", fileName));
  336. }
  337. return format;
  338. }
  339. // Given either Twine, JSON or XML input, return an array
  340. // containing info about the nodes in that file
  341. internal NodeInfo[] GetNodesFromText(string text, NodeFormat format)
  342. {
  343. // All the nodes we found in this file
  344. var nodes = new List<NodeInfo> ();
  345. switch (format)
  346. {
  347. case NodeFormat.SingleNodeText:
  348. // If it starts with a comment, treat it as a single-node file
  349. var nodeInfo = new NodeInfo();
  350. nodeInfo.title = "Start";
  351. nodeInfo.body = text;
  352. nodes.Add(nodeInfo);
  353. break;
  354. case NodeFormat.JSON:
  355. // Parse it as JSON
  356. try
  357. {
  358. nodes = JsonConvert.DeserializeObject<List<NodeInfo>>(text);
  359. }
  360. catch (JsonReaderException e)
  361. {
  362. dialogue.LogErrorMessage("Error parsing Yarn input: " + e.Message);
  363. }
  364. break;
  365. case NodeFormat.Text:
  366. // check for the existence of at least one "---"+newline sentinel, which divides
  367. // the headers from the body
  368. // we use a regex to match either \r\n or \n line endings
  369. if (System.Text.RegularExpressions.Regex.IsMatch(text, "---.?\n") == false) {
  370. dialogue.LogErrorMessage("Error parsing input: text appears corrupt (no header sentinel)");
  371. break;
  372. }
  373. var headerRegex = new System.Text.RegularExpressions.Regex("(?<field>.*): *(?<value>.*)");
  374. var nodeProperties = typeof(NodeInfo).GetProperties();
  375. int lineNumber = 0;
  376. using (var reader = new System.IO.StringReader(text))
  377. {
  378. string line;
  379. while ((line = reader.ReadLine()) != null)
  380. {
  381. // Create a new node
  382. NodeInfo node = new NodeInfo();
  383. // Read header lines
  384. do
  385. {
  386. lineNumber++;
  387. // skip empty lines
  388. if (line == null || line.Length == 0)
  389. {
  390. continue;
  391. }
  392. // Attempt to parse the header
  393. var headerMatches = headerRegex.Match(line);
  394. if (headerMatches == null)
  395. {
  396. dialogue.LogErrorMessage(string.Format("Line {0}: Can't parse header '{1}'", lineNumber, line));
  397. continue;
  398. }
  399. var field = headerMatches.Groups["field"].Value;
  400. var value = headerMatches.Groups["value"].Value;
  401. // Attempt to set the appropriate property using this field
  402. foreach (var property in nodeProperties)
  403. {
  404. if (property.Name != field) {
  405. continue;
  406. }
  407. // skip properties that can't be written to
  408. if (property.CanWrite == false)
  409. {
  410. continue;
  411. }
  412. try
  413. {
  414. var propertyType = property.PropertyType;
  415. object convertedValue;
  416. if (propertyType.IsAssignableFrom(typeof(string)))
  417. {
  418. convertedValue = value;
  419. }
  420. else if (propertyType.IsAssignableFrom(typeof(int)))
  421. {
  422. convertedValue = int.Parse(value);
  423. }
  424. else if (propertyType.IsAssignableFrom(typeof(NodeInfo.Position)))
  425. {
  426. var components = value.Split(',');
  427. // we expect 2 components: x and y
  428. if (components.Length != 2)
  429. {
  430. throw new FormatException();
  431. }
  432. var position = new NodeInfo.Position();
  433. position.x = int.Parse(components[0]);
  434. position.y = int.Parse(components[1]);
  435. convertedValue = position;
  436. }
  437. else {
  438. throw new NotSupportedException();
  439. }
  440. // we need to box this because structs are value types,
  441. // so calling SetValue using 'node' would just modify a copy of 'node'
  442. object box = node;
  443. property.SetValue(box, convertedValue, null);
  444. node = (NodeInfo)box;
  445. break;
  446. }
  447. catch (FormatException)
  448. {
  449. dialogue.LogErrorMessage(string.Format("{0}: Error setting '{1}': invalid value '{2}'", lineNumber, field, value));
  450. }
  451. catch (NotSupportedException)
  452. {
  453. dialogue.LogErrorMessage(string.Format("{0}: Error setting '{1}': This property cannot be set", lineNumber, field));
  454. }
  455. }
  456. } while ((line = reader.ReadLine()) != "---");
  457. lineNumber++;
  458. // We're past the header; read the body
  459. var lines = new List<string>();
  460. // Read header lines until we hit the end of node sentinel or the end of the file
  461. while ((line = reader.ReadLine()) != "===" && line != null)
  462. {
  463. lineNumber++;
  464. lines.Add(line);
  465. }
  466. // We're done reading the lines! Zip 'em up into a string and
  467. // store it in the body
  468. node.body = string.Join("\n", lines.ToArray());
  469. // And add this node to the list
  470. nodes.Add(node);
  471. // And now we're ready to move on to the next line!
  472. }
  473. }
  474. break;
  475. default:
  476. throw new InvalidOperationException("Unknown format " + format.ToString());
  477. }
  478. // hooray we're done
  479. return nodes.ToArray();
  480. }
  481. }
  482. }