Newer
Older
TheVengeance-Project-IADE-Unity2D / Assets / Ink / InkLibs / InkRuntime / StoryState.cs
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Diagnostics;
  5. using System.IO;
  6. namespace Ink.Runtime
  7. {
  8. /// <summary>
  9. /// All story state information is included in the StoryState class,
  10. /// including global variables, read counts, the pointer to the current
  11. /// point in the story, the call stack (for tunnels, functions, etc),
  12. /// and a few other smaller bits and pieces. You can save the current
  13. /// state using the json serialisation functions ToJson and LoadJson.
  14. /// </summary>
  15. public class StoryState
  16. {
  17. /// <summary>
  18. /// The current version of the state save file JSON-based format.
  19. /// </summary>
  20. //
  21. // Backward compatible changes since v8:
  22. // v10: dynamic tags
  23. // v9: multi-flows
  24. public const int kInkSaveStateVersion = 10;
  25. const int kMinCompatibleLoadVersion = 8;
  26. /// <summary>
  27. /// Callback for when a state is loaded
  28. /// </summary>
  29. public event Action onDidLoadState;
  30. /// <summary>
  31. /// Exports the current state to json format, in order to save the game.
  32. /// </summary>
  33. /// <returns>The save state in json format.</returns>
  34. public string ToJson() {
  35. var writer = new SimpleJson.Writer();
  36. WriteJson(writer);
  37. return writer.ToString();
  38. }
  39. /// <summary>
  40. /// Exports the current state to json format, in order to save the game.
  41. /// For this overload you can pass in a custom stream, such as a FileStream.
  42. /// </summary>
  43. public void ToJson(Stream stream) {
  44. var writer = new SimpleJson.Writer(stream);
  45. WriteJson(writer);
  46. }
  47. /// <summary>
  48. /// Loads a previously saved state in JSON format.
  49. /// </summary>
  50. /// <param name="json">The JSON string to load.</param>
  51. public void LoadJson(string json)
  52. {
  53. var jObject = SimpleJson.TextToDictionary (json);
  54. LoadJsonObj(jObject);
  55. if(onDidLoadState != null) onDidLoadState();
  56. }
  57. /// <summary>
  58. /// Gets the visit/read count of a particular Container at the given path.
  59. /// For a knot or stitch, that path string will be in the form:
  60. ///
  61. /// knot
  62. /// knot.stitch
  63. ///
  64. /// </summary>
  65. /// <returns>The number of times the specific knot or stitch has
  66. /// been enountered by the ink engine.</returns>
  67. /// <param name="pathString">The dot-separated path string of
  68. /// the specific knot or stitch.</param>
  69. public int VisitCountAtPathString(string pathString)
  70. {
  71. int visitCountOut;
  72. if ( _patch != null ) {
  73. var container = story.ContentAtPath(new Path(pathString)).container;
  74. if (container == null)
  75. throw new Exception("Content at path not found: " + pathString);
  76. if( _patch.TryGetVisitCount(container, out visitCountOut) )
  77. return visitCountOut;
  78. }
  79. if (_visitCounts.TryGetValue(pathString, out visitCountOut))
  80. return visitCountOut;
  81. return 0;
  82. }
  83. public int VisitCountForContainer(Container container)
  84. {
  85. if (!container.visitsShouldBeCounted)
  86. {
  87. story.Error("Read count for target (" + container.name + " - on " + container.debugMetadata + ") unknown.");
  88. return 0;
  89. }
  90. int count = 0;
  91. if (_patch != null && _patch.TryGetVisitCount(container, out count))
  92. return count;
  93. var containerPathStr = container.path.ToString();
  94. _visitCounts.TryGetValue(containerPathStr, out count);
  95. return count;
  96. }
  97. public void IncrementVisitCountForContainer(Container container)
  98. {
  99. if( _patch != null ) {
  100. var currCount = VisitCountForContainer(container);
  101. currCount++;
  102. _patch.SetVisitCount(container, currCount);
  103. return;
  104. }
  105. int count = 0;
  106. var containerPathStr = container.path.ToString();
  107. _visitCounts.TryGetValue(containerPathStr, out count);
  108. count++;
  109. _visitCounts[containerPathStr] = count;
  110. }
  111. public void RecordTurnIndexVisitToContainer(Container container)
  112. {
  113. if( _patch != null ) {
  114. _patch.SetTurnIndex(container, currentTurnIndex);
  115. return;
  116. }
  117. var containerPathStr = container.path.ToString();
  118. _turnIndices[containerPathStr] = currentTurnIndex;
  119. }
  120. public int TurnsSinceForContainer(Container container)
  121. {
  122. if (!container.turnIndexShouldBeCounted)
  123. {
  124. story.Error("TURNS_SINCE() for target (" + container.name + " - on " + container.debugMetadata + ") unknown.");
  125. }
  126. int index = 0;
  127. if ( _patch != null && _patch.TryGetTurnIndex(container, out index) ) {
  128. return currentTurnIndex - index;
  129. }
  130. var containerPathStr = container.path.ToString();
  131. if (_turnIndices.TryGetValue(containerPathStr, out index))
  132. {
  133. return currentTurnIndex - index;
  134. }
  135. else
  136. {
  137. return -1;
  138. }
  139. }
  140. public int callstackDepth {
  141. get {
  142. return callStack.depth;
  143. }
  144. }
  145. // REMEMBER! REMEMBER! REMEMBER!
  146. // When adding state, update the Copy method, and serialisation.
  147. // REMEMBER! REMEMBER! REMEMBER!
  148. public List<Runtime.Object> outputStream {
  149. get {
  150. return _currentFlow.outputStream;
  151. }
  152. }
  153. public List<Choice> currentChoices {
  154. get {
  155. // If we can continue generating text content rather than choices,
  156. // then we reflect the choice list as being empty, since choices
  157. // should always come at the end.
  158. if( canContinue ) return new List<Choice>();
  159. return _currentFlow.currentChoices;
  160. }
  161. }
  162. public List<Choice> generatedChoices {
  163. get {
  164. return _currentFlow.currentChoices;
  165. }
  166. }
  167. // TODO: Consider removing currentErrors / currentWarnings altogether
  168. // and relying on client error handler code immediately handling StoryExceptions etc
  169. // Or is there a specific reason we need to collect potentially multiple
  170. // errors before throwing/exiting?
  171. public List<string> currentErrors { get; private set; }
  172. public List<string> currentWarnings { get; private set; }
  173. public VariablesState variablesState { get; private set; }
  174. public CallStack callStack {
  175. get {
  176. return _currentFlow.callStack;
  177. }
  178. // set {
  179. // _currentFlow.callStack = value;
  180. // }
  181. }
  182. public List<Runtime.Object> evaluationStack { get; private set; }
  183. public Pointer divertedPointer { get; set; }
  184. public int currentTurnIndex { get; private set; }
  185. public int storySeed { get; set; }
  186. public int previousRandom { get; set; }
  187. public bool didSafeExit { get; set; }
  188. public Story story { get; set; }
  189. /// <summary>
  190. /// String representation of the location where the story currently is.
  191. /// </summary>
  192. public string currentPathString {
  193. get {
  194. var pointer = currentPointer;
  195. if (pointer.isNull)
  196. return null;
  197. else
  198. return pointer.path.ToString();
  199. }
  200. }
  201. public Runtime.Pointer currentPointer {
  202. get {
  203. return callStack.currentElement.currentPointer;
  204. }
  205. set {
  206. callStack.currentElement.currentPointer = value;
  207. }
  208. }
  209. public Pointer previousPointer {
  210. get {
  211. return callStack.currentThread.previousPointer;
  212. }
  213. set {
  214. callStack.currentThread.previousPointer = value;
  215. }
  216. }
  217. public bool canContinue {
  218. get {
  219. return !currentPointer.isNull && !hasError;
  220. }
  221. }
  222. public bool hasError
  223. {
  224. get {
  225. return currentErrors != null && currentErrors.Count > 0;
  226. }
  227. }
  228. public bool hasWarning {
  229. get {
  230. return currentWarnings != null && currentWarnings.Count > 0;
  231. }
  232. }
  233. public string currentText
  234. {
  235. get
  236. {
  237. if( _outputStreamTextDirty ) {
  238. var sb = new StringBuilder ();
  239. bool inTag = false;
  240. foreach (var outputObj in outputStream) {
  241. var textContent = outputObj as StringValue;
  242. if (!inTag && textContent != null) {
  243. sb.Append(textContent.value);
  244. } else {
  245. var controlCommand = outputObj as ControlCommand;
  246. if( controlCommand != null ) {
  247. if( controlCommand.commandType == ControlCommand.CommandType.BeginTag ) {
  248. inTag = true;
  249. } else if( controlCommand.commandType == ControlCommand.CommandType.EndTag ) {
  250. inTag = false;
  251. }
  252. }
  253. }
  254. }
  255. _currentText = CleanOutputWhitespace (sb.ToString ());
  256. _outputStreamTextDirty = false;
  257. }
  258. return _currentText;
  259. }
  260. }
  261. string _currentText;
  262. // Cleans inline whitespace in the following way:
  263. // - Removes all whitespace from the start and end of line (including just before a \n)
  264. // - Turns all consecutive space and tab runs into single spaces (HTML style)
  265. public string CleanOutputWhitespace(string str)
  266. {
  267. var sb = new StringBuilder(str.Length);
  268. int currentWhitespaceStart = -1;
  269. int startOfLine = 0;
  270. for (int i = 0; i < str.Length; i++) {
  271. var c = str[i];
  272. bool isInlineWhitespace = c == ' ' || c == '\t';
  273. if (isInlineWhitespace && currentWhitespaceStart == -1)
  274. currentWhitespaceStart = i;
  275. if (!isInlineWhitespace) {
  276. if (c != '\n' && currentWhitespaceStart > 0 && currentWhitespaceStart != startOfLine) {
  277. sb.Append(' ');
  278. }
  279. currentWhitespaceStart = -1;
  280. }
  281. if (c == '\n')
  282. startOfLine = i + 1;
  283. if (!isInlineWhitespace)
  284. sb.Append(c);
  285. }
  286. return sb.ToString();
  287. }
  288. public List<string> currentTags
  289. {
  290. get
  291. {
  292. if( _outputStreamTagsDirty ) {
  293. _currentTags = new List<string>();
  294. bool inTag = false;
  295. var sb = new StringBuilder ();
  296. foreach (var outputObj in outputStream) {
  297. var controlCommand = outputObj as ControlCommand;
  298. if( controlCommand != null ) {
  299. if( controlCommand.commandType == ControlCommand.CommandType.BeginTag ) {
  300. if( inTag && sb.Length > 0 ) {
  301. var txt = CleanOutputWhitespace(sb.ToString());
  302. _currentTags.Add(txt);
  303. sb.Clear();
  304. }
  305. inTag = true;
  306. }
  307. else if( controlCommand.commandType == ControlCommand.CommandType.EndTag ) {
  308. if( sb.Length > 0 ) {
  309. var txt = CleanOutputWhitespace(sb.ToString());
  310. _currentTags.Add(txt);
  311. sb.Clear();
  312. }
  313. inTag = false;
  314. }
  315. }
  316. else if( inTag ) {
  317. var strVal = outputObj as StringValue;
  318. if( strVal != null ) {
  319. sb.Append(strVal.value);
  320. }
  321. }
  322. else {
  323. var tag = outputObj as Tag;
  324. if (tag != null && tag.text != null && tag.text.Length > 0) {
  325. _currentTags.Add (tag.text); // tag.text has whitespae already cleaned
  326. }
  327. }
  328. }
  329. if( sb.Length > 0 ) {
  330. var txt = CleanOutputWhitespace(sb.ToString());
  331. _currentTags.Add(txt);
  332. sb.Clear();
  333. }
  334. _outputStreamTagsDirty = false;
  335. }
  336. return _currentTags;
  337. }
  338. }
  339. List<string> _currentTags;
  340. public string currentFlowName {
  341. get {
  342. return _currentFlow.name;
  343. }
  344. }
  345. public bool currentFlowIsDefaultFlow {
  346. get {
  347. return _currentFlow.name == kDefaultFlowName;
  348. }
  349. }
  350. public List<string> aliveFlowNames {
  351. get {
  352. if( _aliveFlowNamesDirty ) {
  353. _aliveFlowNames = new List<string>();
  354. if (_namedFlows != null)
  355. {
  356. foreach (string flowName in _namedFlows.Keys) {
  357. if (flowName != kDefaultFlowName) {
  358. _aliveFlowNames.Add(flowName);
  359. }
  360. }
  361. }
  362. _aliveFlowNamesDirty = false;
  363. }
  364. return _aliveFlowNames;
  365. }
  366. }
  367. List<string> _aliveFlowNames;
  368. public bool inExpressionEvaluation {
  369. get {
  370. return callStack.currentElement.inExpressionEvaluation;
  371. }
  372. set {
  373. callStack.currentElement.inExpressionEvaluation = value;
  374. }
  375. }
  376. public StoryState (Story story)
  377. {
  378. this.story = story;
  379. _currentFlow = new Flow(kDefaultFlowName, story);
  380. OutputStreamDirty();
  381. _aliveFlowNamesDirty = true;
  382. evaluationStack = new List<Runtime.Object> ();
  383. variablesState = new VariablesState (callStack, story.listDefinitions);
  384. _visitCounts = new Dictionary<string, int> ();
  385. _turnIndices = new Dictionary<string, int> ();
  386. currentTurnIndex = -1;
  387. // Seed the shuffle random numbers
  388. int timeSeed = DateTime.Now.Millisecond;
  389. storySeed = (new Random (timeSeed)).Next () % 100;
  390. previousRandom = 0;
  391. GoToStart();
  392. }
  393. public void GoToStart()
  394. {
  395. callStack.currentElement.currentPointer = Pointer.StartOf (story.mainContentContainer);
  396. }
  397. internal void SwitchFlow_Internal(string flowName)
  398. {
  399. if(flowName == null) throw new System.Exception("Must pass a non-null string to Story.SwitchFlow");
  400. if( _namedFlows == null ) {
  401. _namedFlows = new Dictionary<string, Flow>();
  402. _namedFlows[kDefaultFlowName] = _currentFlow;
  403. }
  404. if( flowName == _currentFlow.name ) {
  405. return;
  406. }
  407. Flow flow;
  408. if( !_namedFlows.TryGetValue(flowName, out flow) ) {
  409. flow = new Flow(flowName, story);
  410. _namedFlows[flowName] = flow;
  411. _aliveFlowNamesDirty = true;
  412. }
  413. _currentFlow = flow;
  414. variablesState.callStack = _currentFlow.callStack;
  415. // Cause text to be regenerated from output stream if necessary
  416. OutputStreamDirty();
  417. }
  418. internal void SwitchToDefaultFlow_Internal()
  419. {
  420. if( _namedFlows == null ) return;
  421. SwitchFlow_Internal(kDefaultFlowName);
  422. }
  423. internal void RemoveFlow_Internal(string flowName)
  424. {
  425. if(flowName == null) throw new System.Exception("Must pass a non-null string to Story.DestroyFlow");
  426. if(flowName == kDefaultFlowName) throw new System.Exception("Cannot destroy default flow");
  427. // If we're currently in the flow that's being removed, switch back to default
  428. if( _currentFlow.name == flowName ) {
  429. SwitchToDefaultFlow_Internal();
  430. }
  431. _namedFlows.Remove(flowName);
  432. _aliveFlowNamesDirty = true;
  433. }
  434. // Warning: Any Runtime.Object content referenced within the StoryState will
  435. // be re-referenced rather than cloned. This is generally okay though since
  436. // Runtime.Objects are treated as immutable after they've been set up.
  437. // (e.g. we don't edit a Runtime.StringValue after it's been created an added.)
  438. // I wonder if there's a sensible way to enforce that..??
  439. public StoryState CopyAndStartPatching()
  440. {
  441. var copy = new StoryState(story);
  442. copy._patch = new StatePatch(_patch);
  443. // Hijack the new default flow to become a copy of our current one
  444. // If the patch is applied, then this new flow will replace the old one in _namedFlows
  445. copy._currentFlow.name = _currentFlow.name;
  446. copy._currentFlow.callStack = new CallStack (_currentFlow.callStack);
  447. copy._currentFlow.currentChoices.AddRange(_currentFlow.currentChoices);
  448. copy._currentFlow.outputStream.AddRange(_currentFlow.outputStream);
  449. copy.OutputStreamDirty();
  450. // The copy of the state has its own copy of the named flows dictionary,
  451. // except with the current flow replaced with the copy above
  452. // (Assuming we're in multi-flow mode at all. If we're not then
  453. // the above copy is simply the default flow copy and we're done)
  454. if( _namedFlows != null ) {
  455. copy._namedFlows = new Dictionary<string, Flow>();
  456. foreach(var namedFlow in _namedFlows)
  457. copy._namedFlows[namedFlow.Key] = namedFlow.Value;
  458. copy._namedFlows[_currentFlow.name] = copy._currentFlow;
  459. copy._aliveFlowNamesDirty = true;
  460. }
  461. if (hasError) {
  462. copy.currentErrors = new List<string> ();
  463. copy.currentErrors.AddRange (currentErrors);
  464. }
  465. if (hasWarning) {
  466. copy.currentWarnings = new List<string> ();
  467. copy.currentWarnings.AddRange (currentWarnings);
  468. }
  469. // ref copy - exactly the same variables state!
  470. // we're expecting not to read it only while in patch mode
  471. // (though the callstack will be modified)
  472. copy.variablesState = variablesState;
  473. copy.variablesState.callStack = copy.callStack;
  474. copy.variablesState.patch = copy._patch;
  475. copy.evaluationStack.AddRange (evaluationStack);
  476. if (!divertedPointer.isNull)
  477. copy.divertedPointer = divertedPointer;
  478. copy.previousPointer = previousPointer;
  479. // visit counts and turn indicies will be read only, not modified
  480. // while in patch mode
  481. copy._visitCounts = _visitCounts;
  482. copy._turnIndices = _turnIndices;
  483. copy.currentTurnIndex = currentTurnIndex;
  484. copy.storySeed = storySeed;
  485. copy.previousRandom = previousRandom;
  486. copy.didSafeExit = didSafeExit;
  487. return copy;
  488. }
  489. public void RestoreAfterPatch()
  490. {
  491. // VariablesState was being borrowed by the patched
  492. // state, so restore it with our own callstack.
  493. // _patch will be null normally, but if you're in the
  494. // middle of a save, it may contain a _patch for save purpsoes.
  495. variablesState.callStack = callStack;
  496. variablesState.patch = _patch; // usually null
  497. }
  498. public void ApplyAnyPatch()
  499. {
  500. if (_patch == null) return;
  501. variablesState.ApplyPatch();
  502. foreach(var pathToCount in _patch.visitCounts)
  503. ApplyCountChanges(pathToCount.Key, pathToCount.Value, isVisit:true);
  504. foreach (var pathToIndex in _patch.turnIndices)
  505. ApplyCountChanges(pathToIndex.Key, pathToIndex.Value, isVisit:false);
  506. _patch = null;
  507. }
  508. void ApplyCountChanges(Container container, int newCount, bool isVisit)
  509. {
  510. var counts = isVisit ? _visitCounts : _turnIndices;
  511. counts[container.path.ToString()] = newCount;
  512. }
  513. void WriteJson(SimpleJson.Writer writer)
  514. {
  515. writer.WriteObjectStart();
  516. // Flows
  517. writer.WritePropertyStart("flows");
  518. writer.WriteObjectStart();
  519. // Multi-flow
  520. if( _namedFlows != null ) {
  521. foreach(var namedFlow in _namedFlows) {
  522. writer.WriteProperty(namedFlow.Key, namedFlow.Value.WriteJson);
  523. }
  524. }
  525. // Single flow
  526. else {
  527. writer.WriteProperty(_currentFlow.name, _currentFlow.WriteJson);
  528. }
  529. writer.WriteObjectEnd();
  530. writer.WritePropertyEnd(); // end of flows
  531. writer.WriteProperty("currentFlowName", _currentFlow.name);
  532. writer.WriteProperty("variablesState", variablesState.WriteJson);
  533. writer.WriteProperty("evalStack", w => Json.WriteListRuntimeObjs(w, evaluationStack));
  534. if (!divertedPointer.isNull)
  535. writer.WriteProperty("currentDivertTarget", divertedPointer.path.componentsString);
  536. writer.WriteProperty("visitCounts", w => Json.WriteIntDictionary(w, _visitCounts));
  537. writer.WriteProperty("turnIndices", w => Json.WriteIntDictionary(w, _turnIndices));
  538. writer.WriteProperty("turnIdx", currentTurnIndex);
  539. writer.WriteProperty("storySeed", storySeed);
  540. writer.WriteProperty("previousRandom", previousRandom);
  541. writer.WriteProperty("inkSaveVersion", kInkSaveStateVersion);
  542. // Not using this right now, but could do in future.
  543. writer.WriteProperty("inkFormatVersion", Story.inkVersionCurrent);
  544. writer.WriteObjectEnd();
  545. }
  546. void LoadJsonObj(Dictionary<string, object> jObject)
  547. {
  548. object jSaveVersion = null;
  549. if (!jObject.TryGetValue("inkSaveVersion", out jSaveVersion)) {
  550. throw new Exception ("ink save format incorrect, can't load.");
  551. }
  552. else if ((int)jSaveVersion < kMinCompatibleLoadVersion) {
  553. throw new Exception("Ink save format isn't compatible with the current version (saw '"+jSaveVersion+"', but minimum is "+kMinCompatibleLoadVersion+"), so can't load.");
  554. }
  555. // Flows: Always exists in latest format (even if there's just one default)
  556. // but this dictionary doesn't exist in prev format
  557. object flowsObj = null;
  558. if (jObject.TryGetValue("flows", out flowsObj)) {
  559. var flowsObjDict = (Dictionary<string, object>)flowsObj;
  560. // Single default flow
  561. if( flowsObjDict.Count == 1 )
  562. _namedFlows = null;
  563. // Multi-flow, need to create flows dict
  564. else if( _namedFlows == null )
  565. _namedFlows = new Dictionary<string, Flow>();
  566. // Multi-flow, already have a flows dict
  567. else
  568. _namedFlows.Clear();
  569. // Load up each flow (there may only be one)
  570. foreach(var namedFlowObj in flowsObjDict) {
  571. var name = namedFlowObj.Key;
  572. var flowObj = (Dictionary<string, object>)namedFlowObj.Value;
  573. // Load up this flow using JSON data
  574. var flow = new Flow(name, story, flowObj);
  575. if( flowsObjDict.Count == 1 ) {
  576. _currentFlow = new Flow(name, story, flowObj);
  577. } else {
  578. _namedFlows[name] = flow;
  579. }
  580. }
  581. if( _namedFlows != null && _namedFlows.Count > 1 ) {
  582. var currFlowName = (string)jObject["currentFlowName"];
  583. _currentFlow = _namedFlows[currFlowName];
  584. }
  585. }
  586. // Old format: individually load up callstack, output stream, choices in current/default flow
  587. else {
  588. _namedFlows = null;
  589. _currentFlow.name = kDefaultFlowName;
  590. _currentFlow.callStack.SetJsonToken ((Dictionary < string, object > )jObject ["callstackThreads"], story);
  591. _currentFlow.outputStream = Json.JArrayToRuntimeObjList ((List<object>)jObject ["outputStream"]);
  592. _currentFlow.currentChoices = Json.JArrayToRuntimeObjList<Choice>((List<object>)jObject ["currentChoices"]);
  593. object jChoiceThreadsObj = null;
  594. jObject.TryGetValue("choiceThreads", out jChoiceThreadsObj);
  595. _currentFlow.LoadFlowChoiceThreads((Dictionary<string, object>)jChoiceThreadsObj, story);
  596. }
  597. OutputStreamDirty();
  598. _aliveFlowNamesDirty = true;
  599. variablesState.SetJsonToken((Dictionary < string, object> )jObject["variablesState"]);
  600. variablesState.callStack = _currentFlow.callStack;
  601. evaluationStack = Json.JArrayToRuntimeObjList ((List<object>)jObject ["evalStack"]);
  602. object currentDivertTargetPath;
  603. if (jObject.TryGetValue("currentDivertTarget", out currentDivertTargetPath)) {
  604. var divertPath = new Path (currentDivertTargetPath.ToString ());
  605. divertedPointer = story.PointerAtPath (divertPath);
  606. }
  607. _visitCounts = Json.JObjectToIntDictionary((Dictionary<string, object>)jObject["visitCounts"]);
  608. _turnIndices = Json.JObjectToIntDictionary((Dictionary<string, object>)jObject["turnIndices"]);
  609. currentTurnIndex = (int)jObject ["turnIdx"];
  610. storySeed = (int)jObject ["storySeed"];
  611. // Not optional, but bug in inkjs means it's actually missing in inkjs saves
  612. object previousRandomObj = null;
  613. if( jObject.TryGetValue("previousRandom", out previousRandomObj) ) {
  614. previousRandom = (int)previousRandomObj;
  615. } else {
  616. previousRandom = 0;
  617. }
  618. }
  619. public void ResetErrors()
  620. {
  621. currentErrors = null;
  622. currentWarnings = null;
  623. }
  624. public void ResetOutput(List<Runtime.Object> objs = null)
  625. {
  626. outputStream.Clear ();
  627. if( objs != null ) outputStream.AddRange (objs);
  628. OutputStreamDirty();
  629. }
  630. // Push to output stream, but split out newlines in text for consistency
  631. // in dealing with them later.
  632. public void PushToOutputStream(Runtime.Object obj)
  633. {
  634. var text = obj as StringValue;
  635. if (text) {
  636. var listText = TrySplittingHeadTailWhitespace (text);
  637. if (listText != null) {
  638. foreach (var textObj in listText) {
  639. PushToOutputStreamIndividual (textObj);
  640. }
  641. OutputStreamDirty();
  642. return;
  643. }
  644. }
  645. PushToOutputStreamIndividual (obj);
  646. OutputStreamDirty();
  647. }
  648. public void PopFromOutputStream (int count)
  649. {
  650. outputStream.RemoveRange (outputStream.Count - count, count);
  651. OutputStreamDirty ();
  652. }
  653. // At both the start and the end of the string, split out the new lines like so:
  654. //
  655. // " \n \n \n the string \n is awesome \n \n "
  656. // ^-----------^ ^-------^
  657. //
  658. // Excess newlines are converted into single newlines, and spaces discarded.
  659. // Outside spaces are significant and retained. "Interior" newlines within
  660. // the main string are ignored, since this is for the purpose of gluing only.
  661. //
  662. // - If no splitting is necessary, null is returned.
  663. // - A newline on its own is returned in a list for consistency.
  664. List<Runtime.StringValue> TrySplittingHeadTailWhitespace(Runtime.StringValue single)
  665. {
  666. string str = single.value;
  667. int headFirstNewlineIdx = -1;
  668. int headLastNewlineIdx = -1;
  669. for (int i = 0; i < str.Length; i++) {
  670. char c = str [i];
  671. if (c == '\n') {
  672. if (headFirstNewlineIdx == -1)
  673. headFirstNewlineIdx = i;
  674. headLastNewlineIdx = i;
  675. }
  676. else if (c == ' ' || c == '\t')
  677. continue;
  678. else
  679. break;
  680. }
  681. int tailLastNewlineIdx = -1;
  682. int tailFirstNewlineIdx = -1;
  683. for (int i = str.Length-1; i >= 0; i--) {
  684. char c = str [i];
  685. if (c == '\n') {
  686. if (tailLastNewlineIdx == -1)
  687. tailLastNewlineIdx = i;
  688. tailFirstNewlineIdx = i;
  689. }
  690. else if (c == ' ' || c == '\t')
  691. continue;
  692. else
  693. break;
  694. }
  695. // No splitting to be done?
  696. if (headFirstNewlineIdx == -1 && tailLastNewlineIdx == -1)
  697. return null;
  698. var listTexts = new List<Runtime.StringValue> ();
  699. int innerStrStart = 0;
  700. int innerStrEnd = str.Length;
  701. if (headFirstNewlineIdx != -1) {
  702. if (headFirstNewlineIdx > 0) {
  703. var leadingSpaces = new StringValue (str.Substring (0, headFirstNewlineIdx));
  704. listTexts.Add(leadingSpaces);
  705. }
  706. listTexts.Add (new StringValue ("\n"));
  707. innerStrStart = headLastNewlineIdx + 1;
  708. }
  709. if (tailLastNewlineIdx != -1) {
  710. innerStrEnd = tailFirstNewlineIdx;
  711. }
  712. if (innerStrEnd > innerStrStart) {
  713. var innerStrText = str.Substring (innerStrStart, innerStrEnd - innerStrStart);
  714. listTexts.Add (new StringValue (innerStrText));
  715. }
  716. if (tailLastNewlineIdx != -1 && tailFirstNewlineIdx > headLastNewlineIdx) {
  717. listTexts.Add (new StringValue ("\n"));
  718. if (tailLastNewlineIdx < str.Length - 1) {
  719. int numSpaces = (str.Length - tailLastNewlineIdx) - 1;
  720. var trailingSpaces = new StringValue (str.Substring (tailLastNewlineIdx + 1, numSpaces));
  721. listTexts.Add(trailingSpaces);
  722. }
  723. }
  724. return listTexts;
  725. }
  726. void PushToOutputStreamIndividual(Runtime.Object obj)
  727. {
  728. var glue = obj as Runtime.Glue;
  729. var text = obj as Runtime.StringValue;
  730. bool includeInOutput = true;
  731. // New glue, so chomp away any whitespace from the end of the stream
  732. if (glue) {
  733. TrimNewlinesFromOutputStream();
  734. includeInOutput = true;
  735. }
  736. // New text: do we really want to append it, if it's whitespace?
  737. // Two different reasons for whitespace to be thrown away:
  738. // - Function start/end trimming
  739. // - User defined glue: <>
  740. // We also need to know when to stop trimming, when there's non-whitespace.
  741. else if( text ) {
  742. // Where does the current function call begin?
  743. var functionTrimIndex = -1;
  744. var currEl = callStack.currentElement;
  745. if (currEl.type == PushPopType.Function) {
  746. functionTrimIndex = currEl.functionStartInOuputStream;
  747. }
  748. // Do 2 things:
  749. // - Find latest glue
  750. // - Check whether we're in the middle of string evaluation
  751. // If we're in string eval within the current function, we
  752. // don't want to trim back further than the length of the current string.
  753. int glueTrimIndex = -1;
  754. for (int i = outputStream.Count - 1; i >= 0; i--) {
  755. var o = outputStream [i];
  756. var c = o as ControlCommand;
  757. var g = o as Glue;
  758. // Find latest glue
  759. if (g) {
  760. glueTrimIndex = i;
  761. break;
  762. }
  763. // Don't function-trim past the start of a string evaluation section
  764. else if (c && c.commandType == ControlCommand.CommandType.BeginString) {
  765. if (i >= functionTrimIndex) {
  766. functionTrimIndex = -1;
  767. }
  768. break;
  769. }
  770. }
  771. // Where is the most agressive (earliest) trim point?
  772. var trimIndex = -1;
  773. if (glueTrimIndex != -1 && functionTrimIndex != -1)
  774. trimIndex = Math.Min (functionTrimIndex, glueTrimIndex);
  775. else if (glueTrimIndex != -1)
  776. trimIndex = glueTrimIndex;
  777. else
  778. trimIndex = functionTrimIndex;
  779. // So, are we trimming then?
  780. if (trimIndex != -1) {
  781. // While trimming, we want to throw all newlines away,
  782. // whether due to glue or the start of a function
  783. if (text.isNewline) {
  784. includeInOutput = false;
  785. }
  786. // Able to completely reset when normal text is pushed
  787. else if (text.isNonWhitespace) {
  788. if( glueTrimIndex > -1 )
  789. RemoveExistingGlue ();
  790. // Tell all functions in callstack that we have seen proper text,
  791. // so trimming whitespace at the start is done.
  792. if (functionTrimIndex > -1) {
  793. var callstackElements = callStack.elements;
  794. for (int i = callstackElements.Count - 1; i >= 0; i--) {
  795. var el = callstackElements [i];
  796. if (el.type == PushPopType.Function) {
  797. el.functionStartInOuputStream = -1;
  798. } else {
  799. break;
  800. }
  801. }
  802. }
  803. }
  804. }
  805. // De-duplicate newlines, and don't ever lead with a newline
  806. else if (text.isNewline) {
  807. if (outputStreamEndsInNewline || !outputStreamContainsContent)
  808. includeInOutput = false;
  809. }
  810. }
  811. if (includeInOutput) {
  812. outputStream.Add (obj);
  813. OutputStreamDirty();
  814. }
  815. }
  816. void TrimNewlinesFromOutputStream()
  817. {
  818. int removeWhitespaceFrom = -1;
  819. // Work back from the end, and try to find the point where
  820. // we need to start removing content.
  821. // - Simply work backwards to find the first newline in a string of whitespace
  822. // e.g. This is the content \n \n\n
  823. // ^---------^ whitespace to remove
  824. // ^--- first while loop stops here
  825. int i = outputStream.Count-1;
  826. while (i >= 0) {
  827. var obj = outputStream [i];
  828. var cmd = obj as ControlCommand;
  829. var txt = obj as StringValue;
  830. if (cmd || (txt && txt.isNonWhitespace)) {
  831. break;
  832. }
  833. else if (txt && txt.isNewline) {
  834. removeWhitespaceFrom = i;
  835. }
  836. i--;
  837. }
  838. // Remove the whitespace
  839. if (removeWhitespaceFrom >= 0) {
  840. i=removeWhitespaceFrom;
  841. while(i < outputStream.Count) {
  842. var text = outputStream [i] as StringValue;
  843. if (text) {
  844. outputStream.RemoveAt (i);
  845. } else {
  846. i++;
  847. }
  848. }
  849. }
  850. OutputStreamDirty();
  851. }
  852. // Only called when non-whitespace is appended
  853. void RemoveExistingGlue()
  854. {
  855. for (int i = outputStream.Count - 1; i >= 0; i--) {
  856. var c = outputStream [i];
  857. if (c is Glue) {
  858. outputStream.RemoveAt (i);
  859. } else if( c is ControlCommand ) { // e.g. BeginString
  860. break;
  861. }
  862. }
  863. OutputStreamDirty();
  864. }
  865. public bool outputStreamEndsInNewline {
  866. get {
  867. if (outputStream.Count > 0) {
  868. for (int i = outputStream.Count - 1; i >= 0; i--) {
  869. var obj = outputStream [i];
  870. if (obj is ControlCommand) // e.g. BeginString
  871. break;
  872. var text = outputStream [i] as StringValue;
  873. if (text) {
  874. if (text.isNewline)
  875. return true;
  876. else if (text.isNonWhitespace)
  877. break;
  878. }
  879. }
  880. }
  881. return false;
  882. }
  883. }
  884. public bool outputStreamContainsContent {
  885. get {
  886. foreach (var content in outputStream) {
  887. if (content is StringValue)
  888. return true;
  889. }
  890. return false;
  891. }
  892. }
  893. public bool inStringEvaluation {
  894. get {
  895. for (int i = outputStream.Count - 1; i >= 0; i--) {
  896. var cmd = outputStream [i] as ControlCommand;
  897. if (cmd && cmd.commandType == ControlCommand.CommandType.BeginString) {
  898. return true;
  899. }
  900. }
  901. return false;
  902. }
  903. }
  904. public void PushEvaluationStack(Runtime.Object obj)
  905. {
  906. // Include metadata about the origin List for list values when
  907. // they're used, so that lower level functions can make use
  908. // of the origin list to get related items, or make comparisons
  909. // with the integer values etc.
  910. var listValue = obj as ListValue;
  911. if (listValue) {
  912. // Update origin when list is has something to indicate the list origin
  913. var rawList = listValue.value;
  914. if (rawList.originNames != null) {
  915. if( rawList.origins == null ) rawList.origins = new List<ListDefinition>();
  916. rawList.origins.Clear();
  917. foreach (var n in rawList.originNames) {
  918. ListDefinition def = null;
  919. story.listDefinitions.TryListGetDefinition (n, out def);
  920. if( !rawList.origins.Contains(def) )
  921. rawList.origins.Add (def);
  922. }
  923. }
  924. }
  925. evaluationStack.Add(obj);
  926. }
  927. public Runtime.Object PopEvaluationStack()
  928. {
  929. var obj = evaluationStack [evaluationStack.Count - 1];
  930. evaluationStack.RemoveAt (evaluationStack.Count - 1);
  931. return obj;
  932. }
  933. public Runtime.Object PeekEvaluationStack()
  934. {
  935. return evaluationStack [evaluationStack.Count - 1];
  936. }
  937. public List<Runtime.Object> PopEvaluationStack(int numberOfObjects)
  938. {
  939. if(numberOfObjects > evaluationStack.Count) {
  940. throw new System.Exception ("trying to pop too many objects");
  941. }
  942. var popped = evaluationStack.GetRange (evaluationStack.Count - numberOfObjects, numberOfObjects);
  943. evaluationStack.RemoveRange (evaluationStack.Count - numberOfObjects, numberOfObjects);
  944. return popped;
  945. }
  946. /// <summary>
  947. /// Ends the current ink flow, unwrapping the callstack but without
  948. /// affecting any variables. Useful if the ink is (say) in the middle
  949. /// a nested tunnel, and you want it to reset so that you can divert
  950. /// elsewhere using ChoosePathString(). Otherwise, after finishing
  951. /// the content you diverted to, it would continue where it left off.
  952. /// Calling this is equivalent to calling -> END in ink.
  953. /// </summary>
  954. public void ForceEnd()
  955. {
  956. callStack.Reset();
  957. _currentFlow.currentChoices.Clear();
  958. currentPointer = Pointer.Null;
  959. previousPointer = Pointer.Null;
  960. didSafeExit = true;
  961. }
  962. // Add the end of a function call, trim any whitespace from the end.
  963. // We always trim the start and end of the text that a function produces.
  964. // The start whitespace is discard as it is generated, and the end
  965. // whitespace is trimmed in one go here when we pop the function.
  966. void TrimWhitespaceFromFunctionEnd ()
  967. {
  968. Debug.Assert (callStack.currentElement.type == PushPopType.Function);
  969. var functionStartPoint = callStack.currentElement.functionStartInOuputStream;
  970. // If the start point has become -1, it means that some non-whitespace
  971. // text has been pushed, so it's safe to go as far back as we're able.
  972. if (functionStartPoint == -1) {
  973. functionStartPoint = 0;
  974. }
  975. // Trim whitespace from END of function call
  976. for (int i = outputStream.Count - 1; i >= functionStartPoint; i--) {
  977. var obj = outputStream [i];
  978. var txt = obj as StringValue;
  979. var cmd = obj as ControlCommand;
  980. if (!txt) continue;
  981. if (cmd) break;
  982. if (txt.isNewline || txt.isInlineWhitespace) {
  983. outputStream.RemoveAt (i);
  984. OutputStreamDirty ();
  985. } else {
  986. break;
  987. }
  988. }
  989. }
  990. public void PopCallstack (PushPopType? popType = null)
  991. {
  992. // Add the end of a function call, trim any whitespace from the end.
  993. if (callStack.currentElement.type == PushPopType.Function)
  994. TrimWhitespaceFromFunctionEnd ();
  995. callStack.Pop (popType);
  996. }
  997. // Don't make public since the method need to be wrapped in Story for visit counting
  998. public void SetChosenPath(Path path, bool incrementingTurnIndex)
  999. {
  1000. // Changing direction, assume we need to clear current set of choices
  1001. _currentFlow.currentChoices.Clear ();
  1002. var newPointer = story.PointerAtPath (path);
  1003. if (!newPointer.isNull && newPointer.index == -1)
  1004. newPointer.index = 0;
  1005. currentPointer = newPointer;
  1006. if( incrementingTurnIndex )
  1007. currentTurnIndex++;
  1008. }
  1009. public void StartFunctionEvaluationFromGame (Container funcContainer, params object[] arguments)
  1010. {
  1011. callStack.Push (PushPopType.FunctionEvaluationFromGame, evaluationStack.Count);
  1012. callStack.currentElement.currentPointer = Pointer.StartOf (funcContainer);
  1013. PassArgumentsToEvaluationStack (arguments);
  1014. }
  1015. public void PassArgumentsToEvaluationStack (params object [] arguments)
  1016. {
  1017. // Pass arguments onto the evaluation stack
  1018. if (arguments != null) {
  1019. for (int i = 0; i < arguments.Length; i++) {
  1020. if (!(arguments [i] is int || arguments [i] is float || arguments [i] is string || arguments [i] is bool || arguments [i] is InkList)) {
  1021. throw new System.ArgumentException ("ink arguments when calling EvaluateFunction / ChoosePathStringWithParameters must be int, float, string, bool or InkList. Argument was "+(arguments [i] == null ? "null" : arguments [i].GetType().Name));
  1022. }
  1023. PushEvaluationStack (Runtime.Value.Create (arguments [i]));
  1024. }
  1025. }
  1026. }
  1027. public bool TryExitFunctionEvaluationFromGame ()
  1028. {
  1029. if( callStack.currentElement.type == PushPopType.FunctionEvaluationFromGame ) {
  1030. currentPointer = Pointer.Null;
  1031. didSafeExit = true;
  1032. return true;
  1033. }
  1034. return false;
  1035. }
  1036. public object CompleteFunctionEvaluationFromGame ()
  1037. {
  1038. if (callStack.currentElement.type != PushPopType.FunctionEvaluationFromGame) {
  1039. throw new Exception ("Expected external function evaluation to be complete. Stack trace: "+callStack.callStackTrace);
  1040. }
  1041. int originalEvaluationStackHeight = callStack.currentElement.evaluationStackHeightWhenPushed;
  1042. // Do we have a returned value?
  1043. // Potentially pop multiple values off the stack, in case we need
  1044. // to clean up after ourselves (e.g. caller of EvaluateFunction may
  1045. // have passed too many arguments, and we currently have no way to check for that)
  1046. Runtime.Object returnedObj = null;
  1047. while (evaluationStack.Count > originalEvaluationStackHeight) {
  1048. var poppedObj = PopEvaluationStack ();
  1049. if (returnedObj == null)
  1050. returnedObj = poppedObj;
  1051. }
  1052. // Finally, pop the external function evaluation
  1053. PopCallstack (PushPopType.FunctionEvaluationFromGame);
  1054. // What did we get back?
  1055. if (returnedObj) {
  1056. if (returnedObj is Runtime.Void)
  1057. return null;
  1058. // Some kind of value, if not void
  1059. var returnVal = returnedObj as Runtime.Value;
  1060. // DivertTargets get returned as the string of components
  1061. // (rather than a Path, which isn't public)
  1062. if (returnVal.valueType == ValueType.DivertTarget) {
  1063. return returnVal.valueObject.ToString ();
  1064. }
  1065. // Other types can just have their exact object type:
  1066. // int, float, string. VariablePointers get returned as strings.
  1067. return returnVal.valueObject;
  1068. }
  1069. return null;
  1070. }
  1071. public void AddError(string message, bool isWarning)
  1072. {
  1073. if (!isWarning) {
  1074. if (currentErrors == null) currentErrors = new List<string> ();
  1075. currentErrors.Add (message);
  1076. } else {
  1077. if (currentWarnings == null) currentWarnings = new List<string> ();
  1078. currentWarnings.Add (message);
  1079. }
  1080. }
  1081. void OutputStreamDirty()
  1082. {
  1083. _outputStreamTextDirty = true;
  1084. _outputStreamTagsDirty = true;
  1085. }
  1086. // REMEMBER! REMEMBER! REMEMBER!
  1087. // When adding state, update the Copy method and serialisation
  1088. // REMEMBER! REMEMBER! REMEMBER!
  1089. Dictionary<string, int> _visitCounts;
  1090. Dictionary<string, int> _turnIndices;
  1091. bool _outputStreamTextDirty = true;
  1092. bool _outputStreamTagsDirty = true;
  1093. StatePatch _patch;
  1094. Flow _currentFlow;
  1095. Dictionary<string, Flow> _namedFlows;
  1096. const string kDefaultFlowName = "DEFAULT_FLOW";
  1097. bool _aliveFlowNamesDirty = true;
  1098. }
  1099. }