Newer
Older
TheVengeance-Project-IADE-Unity2D / Assets / Ink / InkLibs / InkRuntime / Story.cs
@Rackday Rackday on 29 Oct 123 KB Major Update
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.IO;
  6. using System.Diagnostics;
  7. namespace Ink.Runtime
  8. {
  9. /// <summary>
  10. /// A Story is the core class that represents a complete Ink narrative, and
  11. /// manages the evaluation and state of it.
  12. /// </summary>
  13. public class Story : Runtime.Object
  14. {
  15. /// <summary>
  16. /// The current version of the ink story file format.
  17. /// </summary>
  18. public const int inkVersionCurrent = 21;
  19. // Version numbers are for engine itself and story file, rather
  20. // than the story state save format
  21. // -- old engine, new format: always fail
  22. // -- new engine, old format: possibly cope, based on this number
  23. // When incrementing the version number above, the question you
  24. // should ask yourself is:
  25. // -- Will the engine be able to load an old story file from
  26. // before I made these changes to the engine?
  27. // If possible, you should support it, though it's not as
  28. // critical as loading old save games, since it's an
  29. // in-development problem only.
  30. /// <summary>
  31. /// The minimum legacy version of ink that can be loaded by the current version of the code.
  32. /// </summary>
  33. const int inkVersionMinimumCompatible = 18;
  34. /// <summary>
  35. /// The list of Choice objects available at the current point in
  36. /// the Story. This list will be populated as the Story is stepped
  37. /// through with the Continue() method. Once canContinue becomes
  38. /// false, this list will be populated, and is usually
  39. /// (but not always) on the final Continue() step.
  40. /// </summary>
  41. public List<Choice> currentChoices
  42. {
  43. get
  44. {
  45. // Don't include invisible choices for external usage.
  46. var choices = new List<Choice>();
  47. foreach (var c in _state.currentChoices) {
  48. if (!c.isInvisibleDefault) {
  49. c.index = choices.Count;
  50. choices.Add (c);
  51. }
  52. }
  53. return choices;
  54. }
  55. }
  56. /// <summary>
  57. /// The latest line of text to be generated from a Continue() call.
  58. /// </summary>
  59. public string currentText {
  60. get {
  61. IfAsyncWeCant ("call currentText since it's a work in progress");
  62. return state.currentText;
  63. }
  64. }
  65. /// <summary>
  66. /// Gets a list of tags as defined with '#' in source that were seen
  67. /// during the latest Continue() call.
  68. /// </summary>
  69. public List<string> currentTags {
  70. get {
  71. IfAsyncWeCant ("call currentTags since it's a work in progress");
  72. return state.currentTags;
  73. }
  74. }
  75. /// <summary>
  76. /// Any errors generated during evaluation of the Story.
  77. /// </summary>
  78. public List<string> currentErrors { get { return state.currentErrors; } }
  79. /// <summary>
  80. /// Any warnings generated during evaluation of the Story.
  81. /// </summary>
  82. public List<string> currentWarnings { get { return state.currentWarnings; } }
  83. /// <summary>
  84. /// The current flow name if using multi-flow functionality - see SwitchFlow
  85. /// </summary>
  86. public string currentFlowName => state.currentFlowName;
  87. /// <summary>
  88. /// Is the default flow currently active? By definition, will also return true if not using multi-flow functionality - see SwitchFlow
  89. /// </summary>
  90. public bool currentFlowIsDefaultFlow { get { return state.currentFlowIsDefaultFlow; } }
  91. /// <summary>
  92. /// Names of currently alive flows (not including the default flow)
  93. /// </summary>
  94. public List<string> aliveFlowNames { get { return state.aliveFlowNames; } }
  95. /// <summary>
  96. /// Whether the currentErrors list contains any errors.
  97. /// THIS MAY BE REMOVED - you should be setting an error handler directly
  98. /// using Story.onError.
  99. /// </summary>
  100. public bool hasError { get { return state.hasError; } }
  101. /// <summary>
  102. /// Whether the currentWarnings list contains any warnings.
  103. /// </summary>
  104. public bool hasWarning { get { return state.hasWarning; } }
  105. /// <summary>
  106. /// The VariablesState object contains all the global variables in the story.
  107. /// However, note that there's more to the state of a Story than just the
  108. /// global variables. This is a convenience accessor to the full state object.
  109. /// </summary>
  110. public VariablesState variablesState{ get { return state.variablesState; } }
  111. public ListDefinitionsOrigin listDefinitions {
  112. get {
  113. return _listDefinitions;
  114. }
  115. }
  116. /// <summary>
  117. /// The entire current state of the story including (but not limited to):
  118. ///
  119. /// * Global variables
  120. /// * Temporary variables
  121. /// * Read/visit and turn counts
  122. /// * The callstack and evaluation stacks
  123. /// * The current threads
  124. ///
  125. /// </summary>
  126. public StoryState state { get { return _state; } }
  127. /// <summary>
  128. /// Error handler for all runtime errors in ink - i.e. problems
  129. /// with the source ink itself that are only discovered when playing
  130. /// the story.
  131. /// It's strongly recommended that you assign an error handler to your
  132. /// story instance to avoid getting exceptions for ink errors.
  133. /// </summary>
  134. public event Ink.ErrorHandler onError;
  135. /// <summary>
  136. /// Callback for when ContinueInternal is complete
  137. /// </summary>
  138. public event Action onDidContinue;
  139. /// <summary>
  140. /// Callback for when a choice is about to be executed
  141. /// </summary>
  142. public event Action<Choice> onMakeChoice;
  143. /// <summary>
  144. /// Callback for when a function is about to be evaluated
  145. /// </summary>
  146. public event Action<string, object[]> onEvaluateFunction;
  147. /// <summary>
  148. /// Callback for when a function has been evaluated
  149. /// This is necessary because evaluating a function can cause continuing
  150. /// </summary>
  151. public event Action<string, object[], string, object> onCompleteEvaluateFunction;
  152. /// <summary>
  153. /// Callback for when a path string is chosen
  154. /// </summary>
  155. public event Action<string, object[]> onChoosePathString;
  156. /// <summary>
  157. /// Start recording ink profiling information during calls to Continue on Story.
  158. /// Return a Profiler instance that you can request a report from when you're finished.
  159. /// </summary>
  160. public Profiler StartProfiling() {
  161. IfAsyncWeCant ("start profiling");
  162. _profiler = new Profiler();
  163. return _profiler;
  164. }
  165. /// <summary>
  166. /// Stop recording ink profiling information during calls to Continue on Story.
  167. /// To generate a report from the profiler, call
  168. /// </summary>
  169. public void EndProfiling() {
  170. _profiler = null;
  171. }
  172. // Warning: When creating a Story using this constructor, you need to
  173. // call ResetState on it before use. Intended for compiler use only.
  174. // For normal use, use the constructor that takes a json string.
  175. public Story (Container contentContainer, List<Runtime.ListDefinition> lists = null)
  176. {
  177. _mainContentContainer = contentContainer;
  178. if (lists != null)
  179. _listDefinitions = new ListDefinitionsOrigin (lists);
  180. _externals = new Dictionary<string, ExternalFunctionDef> ();
  181. }
  182. /// <summary>
  183. /// Construct a Story object using a JSON string compiled through inklecate.
  184. /// </summary>
  185. public Story(string jsonString) : this((Container)null)
  186. {
  187. Dictionary<string, object> rootObject = SimpleJson.TextToDictionary (jsonString);
  188. object versionObj = rootObject ["inkVersion"];
  189. if (versionObj == null)
  190. throw new System.Exception ("ink version number not found. Are you sure it's a valid .ink.json file?");
  191. int formatFromFile = (int)versionObj;
  192. if (formatFromFile > inkVersionCurrent) {
  193. throw new System.Exception ("Version of ink used to build story was newer than the current version of the engine");
  194. } else if (formatFromFile < inkVersionMinimumCompatible) {
  195. throw new System.Exception ("Version of ink used to build story is too old to be loaded by this version of the engine");
  196. } else if (formatFromFile != inkVersionCurrent) {
  197. System.Diagnostics.Debug.WriteLine ("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising.");
  198. }
  199. var rootToken = rootObject ["root"];
  200. if (rootToken == null)
  201. throw new System.Exception ("Root node for ink not found. Are you sure it's a valid .ink.json file?");
  202. object listDefsObj;
  203. if (rootObject.TryGetValue ("listDefs", out listDefsObj)) {
  204. _listDefinitions = Json.JTokenToListDefinitions (listDefsObj);
  205. }
  206. _mainContentContainer = Json.JTokenToRuntimeObject (rootToken) as Container;
  207. ResetState ();
  208. }
  209. /// <summary>
  210. /// The Story itself in JSON representation.
  211. /// </summary>
  212. public string ToJson()
  213. {
  214. //return ToJsonOld();
  215. var writer = new SimpleJson.Writer();
  216. ToJson(writer);
  217. return writer.ToString();
  218. }
  219. /// <summary>
  220. /// The Story itself in JSON representation.
  221. /// </summary>
  222. public void ToJson(Stream stream)
  223. {
  224. var writer = new SimpleJson.Writer(stream);
  225. ToJson(writer);
  226. }
  227. void ToJson(SimpleJson.Writer writer)
  228. {
  229. writer.WriteObjectStart();
  230. writer.WriteProperty("inkVersion", inkVersionCurrent);
  231. // Main container content
  232. writer.WriteProperty("root", w => Json.WriteRuntimeContainer(w, _mainContentContainer));
  233. // List definitions
  234. if (_listDefinitions != null) {
  235. writer.WritePropertyStart("listDefs");
  236. writer.WriteObjectStart();
  237. foreach (ListDefinition def in _listDefinitions.lists)
  238. {
  239. writer.WritePropertyStart(def.name);
  240. writer.WriteObjectStart();
  241. foreach (var itemToVal in def.items)
  242. {
  243. InkListItem item = itemToVal.Key;
  244. int val = itemToVal.Value;
  245. writer.WriteProperty(item.itemName, val);
  246. }
  247. writer.WriteObjectEnd();
  248. writer.WritePropertyEnd();
  249. }
  250. writer.WriteObjectEnd();
  251. writer.WritePropertyEnd();
  252. }
  253. writer.WriteObjectEnd();
  254. }
  255. /// <summary>
  256. /// Reset the Story back to its initial state as it was when it was
  257. /// first constructed.
  258. /// </summary>
  259. public void ResetState()
  260. {
  261. // TODO: Could make this possible
  262. IfAsyncWeCant ("ResetState");
  263. _state = new StoryState (this);
  264. _state.variablesState.variableChangedEvent += VariableStateDidChangeEvent;
  265. ResetGlobals ();
  266. }
  267. void ResetErrors()
  268. {
  269. _state.ResetErrors ();
  270. }
  271. /// <summary>
  272. /// Unwinds the callstack. Useful to reset the Story's evaluation
  273. /// without actually changing any meaningful state, for example if
  274. /// you want to exit a section of story prematurely and tell it to
  275. /// go elsewhere with a call to ChoosePathString(...).
  276. /// Doing so without calling ResetCallstack() could cause unexpected
  277. /// issues if, for example, the Story was in a tunnel already.
  278. /// </summary>
  279. public void ResetCallstack()
  280. {
  281. IfAsyncWeCant ("ResetCallstack");
  282. _state.ForceEnd ();
  283. }
  284. void ResetGlobals()
  285. {
  286. if (_mainContentContainer.namedContent.ContainsKey ("global decl")) {
  287. var originalPointer = state.currentPointer;
  288. ChoosePath (new Path ("global decl"), incrementingTurnIndex: false);
  289. // Continue, but without validating external bindings,
  290. // since we may be doing this reset at initialisation time.
  291. ContinueInternal ();
  292. state.currentPointer = originalPointer;
  293. }
  294. state.variablesState.SnapshotDefaultGlobals ();
  295. }
  296. public void SwitchFlow(string flowName)
  297. {
  298. IfAsyncWeCant("switch flow");
  299. if (_asyncSaving) throw new System.Exception("Story is already in background saving mode, can't switch flow to "+flowName);
  300. state.SwitchFlow_Internal(flowName);
  301. }
  302. public void RemoveFlow(string flowName)
  303. {
  304. state.RemoveFlow_Internal(flowName);
  305. }
  306. public void SwitchToDefaultFlow()
  307. {
  308. state.SwitchToDefaultFlow_Internal();
  309. }
  310. /// <summary>
  311. /// Continue the story for one line of content, if possible.
  312. /// If you're not sure if there's more content available, for example if you
  313. /// want to check whether you're at a choice point or at the end of the story,
  314. /// you should call <c>canContinue</c> before calling this function.
  315. /// </summary>
  316. /// <returns>The line of text content.</returns>
  317. public string Continue()
  318. {
  319. ContinueAsync(0);
  320. return currentText;
  321. }
  322. /// <summary>
  323. /// Check whether more content is available if you were to call <c>Continue()</c> - i.e.
  324. /// are we mid story rather than at a choice point or at the end.
  325. /// </summary>
  326. /// <value><c>true</c> if it's possible to call <c>Continue()</c>.</value>
  327. public bool canContinue {
  328. get {
  329. return state.canContinue;
  330. }
  331. }
  332. /// <summary>
  333. /// If ContinueAsync was called (with milliseconds limit > 0) then this property
  334. /// will return false if the ink evaluation isn't yet finished, and you need to call
  335. /// it again in order for the Continue to fully complete.
  336. /// </summary>
  337. public bool asyncContinueComplete {
  338. get {
  339. return !_asyncContinueActive;
  340. }
  341. }
  342. /// <summary>
  343. /// An "asnychronous" version of Continue that only partially evaluates the ink,
  344. /// with a budget of a certain time limit. It will exit ink evaluation early if
  345. /// the evaluation isn't complete within the time limit, with the
  346. /// asyncContinueComplete property being false.
  347. /// This is useful if ink evaluation takes a long time, and you want to distribute
  348. /// it over multiple game frames for smoother animation.
  349. /// If you pass a limit of zero, then it will fully evaluate the ink in the same
  350. /// way as calling Continue (and in fact, this exactly what Continue does internally).
  351. /// </summary>
  352. public void ContinueAsync (float millisecsLimitAsync)
  353. {
  354. if( !_hasValidatedExternals )
  355. ValidateExternalBindings ();
  356. ContinueInternal (millisecsLimitAsync);
  357. }
  358. void ContinueInternal (float millisecsLimitAsync = 0)
  359. {
  360. if( _profiler != null )
  361. _profiler.PreContinue();
  362. var isAsyncTimeLimited = millisecsLimitAsync > 0;
  363. _recursiveContinueCount++;
  364. // Doing either:
  365. // - full run through non-async (so not active and don't want to be)
  366. // - Starting async run-through
  367. if (!_asyncContinueActive) {
  368. _asyncContinueActive = isAsyncTimeLimited;
  369. if (!canContinue) {
  370. throw new Exception ("Can't continue - should check canContinue before calling Continue");
  371. }
  372. _state.didSafeExit = false;
  373. _state.ResetOutput ();
  374. // It's possible for ink to call game to call ink to call game etc
  375. // In this case, we only want to batch observe variable changes
  376. // for the outermost call.
  377. if (_recursiveContinueCount == 1)
  378. _state.variablesState.batchObservingVariableChanges = true;
  379. }
  380. // Start timing
  381. var durationStopwatch = new Stopwatch ();
  382. durationStopwatch.Start ();
  383. bool outputStreamEndsInNewline = false;
  384. _sawLookaheadUnsafeFunctionAfterNewline = false;
  385. do {
  386. try {
  387. outputStreamEndsInNewline = ContinueSingleStep ();
  388. } catch(StoryException e) {
  389. AddError (e.Message, useEndLineNumber:e.useEndLineNumber);
  390. break;
  391. }
  392. if (outputStreamEndsInNewline)
  393. break;
  394. // Run out of async time?
  395. if (_asyncContinueActive && durationStopwatch.ElapsedMilliseconds > millisecsLimitAsync) {
  396. break;
  397. }
  398. } while(canContinue);
  399. durationStopwatch.Stop ();
  400. // 4 outcomes:
  401. // - got newline (so finished this line of text)
  402. // - can't continue (e.g. choices or ending)
  403. // - ran out of time during evaluation
  404. // - error
  405. //
  406. // Successfully finished evaluation in time (or in error)
  407. if (outputStreamEndsInNewline || !canContinue) {
  408. // Need to rewind, due to evaluating further than we should?
  409. if( _stateSnapshotAtLastNewline != null ) {
  410. RestoreStateSnapshot ();
  411. }
  412. // Finished a section of content / reached a choice point?
  413. if( !canContinue ) {
  414. if (state.callStack.canPopThread)
  415. AddError ("Thread available to pop, threads should always be flat by the end of evaluation?");
  416. if (state.generatedChoices.Count == 0 && !state.didSafeExit && _temporaryEvaluationContainer == null) {
  417. if (state.callStack.CanPop (PushPopType.Tunnel))
  418. AddError ("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?");
  419. else if (state.callStack.CanPop (PushPopType.Function))
  420. AddError ("unexpectedly reached end of content. Do you need a '~ return'?");
  421. else if (!state.callStack.canPop)
  422. AddError ("ran out of content. Do you need a '-> DONE' or '-> END'?");
  423. else
  424. AddError ("unexpectedly reached end of content for unknown reason. Please debug compiler!");
  425. }
  426. }
  427. state.didSafeExit = false;
  428. _sawLookaheadUnsafeFunctionAfterNewline = false;
  429. if (_recursiveContinueCount == 1)
  430. _state.variablesState.batchObservingVariableChanges = false;
  431. _asyncContinueActive = false;
  432. if(onDidContinue != null) onDidContinue();
  433. }
  434. _recursiveContinueCount--;
  435. if( _profiler != null )
  436. _profiler.PostContinue();
  437. // Report any errors that occured during evaluation.
  438. // This may either have been StoryExceptions that were thrown
  439. // and caught during evaluation, or directly added with AddError.
  440. if( state.hasError || state.hasWarning ) {
  441. if( onError != null ) {
  442. if( state.hasError ) {
  443. foreach(var err in state.currentErrors) {
  444. onError(err, ErrorType.Error);
  445. }
  446. }
  447. if( state.hasWarning ) {
  448. foreach(var err in state.currentWarnings) {
  449. onError(err, ErrorType.Warning);
  450. }
  451. }
  452. ResetErrors();
  453. }
  454. // Throw an exception since there's no error handler
  455. else {
  456. var sb = new StringBuilder();
  457. sb.Append("Ink had ");
  458. if( state.hasError ) {
  459. sb.Append(state.currentErrors.Count);
  460. sb.Append(state.currentErrors.Count == 1 ? " error" : " errors");
  461. if( state.hasWarning ) sb.Append(" and ");
  462. }
  463. if( state.hasWarning ) {
  464. sb.Append(state.currentWarnings.Count);
  465. sb.Append(state.currentWarnings.Count == 1 ? " warning" : " warnings");
  466. }
  467. sb.Append(". It is strongly suggested that you assign an error handler to story.onError. The first issue was: ");
  468. sb.Append(state.hasError ? state.currentErrors[0] : state.currentWarnings[0]);
  469. // If you get this exception, please assign an error handler to your story.
  470. // If you're using Unity, you can do something like this when you create
  471. // your story:
  472. //
  473. // var story = new Ink.Runtime.Story(jsonTxt);
  474. // story.onError = (errorMessage, errorType) => {
  475. // if( errorType == ErrorType.Warning )
  476. // Debug.LogWarning(errorMessage);
  477. // else
  478. // Debug.LogError(errorMessage);
  479. // };
  480. //
  481. //
  482. throw new StoryException(sb.ToString());
  483. }
  484. }
  485. }
  486. bool ContinueSingleStep ()
  487. {
  488. if (_profiler != null)
  489. _profiler.PreStep ();
  490. // Run main step function (walks through content)
  491. Step ();
  492. if (_profiler != null)
  493. _profiler.PostStep ();
  494. // Run out of content and we have a default invisible choice that we can follow?
  495. if (!canContinue && !state.callStack.elementIsEvaluateFromGame) {
  496. TryFollowDefaultInvisibleChoice ();
  497. }
  498. if (_profiler != null)
  499. _profiler.PreSnapshot ();
  500. // Don't save/rewind during string evaluation, which is e.g. used for choices
  501. if (!state.inStringEvaluation) {
  502. // We previously found a newline, but were we just double checking that
  503. // it wouldn't immediately be removed by glue?
  504. if (_stateSnapshotAtLastNewline != null) {
  505. // Has proper text or a tag been added? Then we know that the newline
  506. // that was previously added is definitely the end of the line.
  507. var change = CalculateNewlineOutputStateChange (
  508. _stateSnapshotAtLastNewline.currentText, state.currentText,
  509. _stateSnapshotAtLastNewline.currentTags.Count, state.currentTags.Count
  510. );
  511. // The last time we saw a newline, it was definitely the end of the line, so we
  512. // want to rewind to that point.
  513. if (change == OutputStateChange.ExtendedBeyondNewline || _sawLookaheadUnsafeFunctionAfterNewline) {
  514. RestoreStateSnapshot ();
  515. // Hit a newline for sure, we're done
  516. return true;
  517. }
  518. // Newline that previously existed is no longer valid - e.g.
  519. // glue was encounted that caused it to be removed.
  520. else if (change == OutputStateChange.NewlineRemoved) {
  521. DiscardSnapshot();
  522. }
  523. }
  524. // Current content ends in a newline - approaching end of our evaluation
  525. if (state.outputStreamEndsInNewline) {
  526. // If we can continue evaluation for a bit:
  527. // Create a snapshot in case we need to rewind.
  528. // We're going to continue stepping in case we see glue or some
  529. // non-text content such as choices.
  530. if (canContinue) {
  531. // Don't bother to record the state beyond the current newline.
  532. // e.g.:
  533. // Hello world\n // record state at the end of here
  534. // ~ complexCalculation() // don't actually need this unless it generates text
  535. if (_stateSnapshotAtLastNewline == null)
  536. StateSnapshot ();
  537. }
  538. // Can't continue, so we're about to exit - make sure we
  539. // don't have an old state hanging around.
  540. else {
  541. DiscardSnapshot();
  542. }
  543. }
  544. }
  545. if (_profiler != null)
  546. _profiler.PostSnapshot ();
  547. // outputStreamEndsInNewline = false
  548. return false;
  549. }
  550. // Assumption: prevText is the snapshot where we saw a newline, and we're checking whether we're really done
  551. // with that line. Therefore prevText will definitely end in a newline.
  552. //
  553. // We take tags into account too, so that a tag following a content line:
  554. // Content
  555. // # tag
  556. // ... doesn't cause the tag to be wrongly associated with the content above.
  557. enum OutputStateChange
  558. {
  559. NoChange,
  560. ExtendedBeyondNewline,
  561. NewlineRemoved
  562. }
  563. OutputStateChange CalculateNewlineOutputStateChange (string prevText, string currText, int prevTagCount, int currTagCount)
  564. {
  565. // Simple case: nothing's changed, and we still have a newline
  566. // at the end of the current content
  567. var newlineStillExists = currText.Length >= prevText.Length && prevText.Length > 0 && currText [prevText.Length - 1] == '\n';
  568. if (prevTagCount == currTagCount && prevText.Length == currText.Length
  569. && newlineStillExists)
  570. return OutputStateChange.NoChange;
  571. // Old newline has been removed, it wasn't the end of the line after all
  572. if (!newlineStillExists) {
  573. return OutputStateChange.NewlineRemoved;
  574. }
  575. // Tag added - definitely the start of a new line
  576. if (currTagCount > prevTagCount)
  577. return OutputStateChange.ExtendedBeyondNewline;
  578. // There must be new content - check whether it's just whitespace
  579. for (int i = prevText.Length; i < currText.Length; i++) {
  580. var c = currText [i];
  581. if (c != ' ' && c != '\t') {
  582. return OutputStateChange.ExtendedBeyondNewline;
  583. }
  584. }
  585. // There's new text but it's just spaces and tabs, so there's still the potential
  586. // for glue to kill the newline.
  587. return OutputStateChange.NoChange;
  588. }
  589. /// <summary>
  590. /// Continue the story until the next choice point or until it runs out of content.
  591. /// This is as opposed to the Continue() method which only evaluates one line of
  592. /// output at a time.
  593. /// </summary>
  594. /// <returns>The resulting text evaluated by the ink engine, concatenated together.</returns>
  595. public string ContinueMaximally()
  596. {
  597. IfAsyncWeCant ("ContinueMaximally");
  598. var sb = new StringBuilder ();
  599. while (canContinue) {
  600. sb.Append (Continue ());
  601. }
  602. return sb.ToString ();
  603. }
  604. public SearchResult ContentAtPath(Path path)
  605. {
  606. return mainContentContainer.ContentAtPath (path);
  607. }
  608. public Runtime.Container KnotContainerWithName (string name)
  609. {
  610. INamedContent namedContainer;
  611. if (mainContentContainer.namedContent.TryGetValue (name, out namedContainer))
  612. return namedContainer as Container;
  613. else
  614. return null;
  615. }
  616. public Pointer PointerAtPath (Path path)
  617. {
  618. if (path.length == 0)
  619. return Pointer.Null;
  620. var p = new Pointer ();
  621. int pathLengthToUse = path.length;
  622. SearchResult result;
  623. if( path.lastComponent.isIndex ) {
  624. pathLengthToUse = path.length - 1;
  625. result = mainContentContainer.ContentAtPath (path, partialPathLength:pathLengthToUse);
  626. p.container = result.container;
  627. p.index = path.lastComponent.index;
  628. } else {
  629. result = mainContentContainer.ContentAtPath (path);
  630. p.container = result.container;
  631. p.index = -1;
  632. }
  633. if (result.obj == null || result.obj == mainContentContainer && pathLengthToUse > 0)
  634. Error ("Failed to find content at path '" + path + "', and no approximation of it was possible.");
  635. else if (result.approximate)
  636. Warning ("Failed to find content at path '" + path + "', so it was approximated to: '"+result.obj.path+"'.");
  637. return p;
  638. }
  639. // Maximum snapshot stack:
  640. // - stateSnapshotDuringSave -- not retained, but returned to game code
  641. // - _stateSnapshotAtLastNewline (has older patch)
  642. // - _state (current, being patched)
  643. void StateSnapshot()
  644. {
  645. _stateSnapshotAtLastNewline = _state;
  646. _state = _state.CopyAndStartPatching();
  647. }
  648. void RestoreStateSnapshot()
  649. {
  650. // Patched state had temporarily hijacked our
  651. // VariablesState and set its own callstack on it,
  652. // so we need to restore that.
  653. // If we're in the middle of saving, we may also
  654. // need to give the VariablesState the old patch.
  655. _stateSnapshotAtLastNewline.RestoreAfterPatch();
  656. _state = _stateSnapshotAtLastNewline;
  657. _stateSnapshotAtLastNewline = null;
  658. // If save completed while the above snapshot was
  659. // active, we need to apply any changes made since
  660. // the save was started but before the snapshot was made.
  661. if( !_asyncSaving ) {
  662. _state.ApplyAnyPatch();
  663. }
  664. }
  665. void DiscardSnapshot()
  666. {
  667. // Normally we want to integrate the patch
  668. // into the main global/counts dictionaries.
  669. // However, if we're in the middle of async
  670. // saving, we simply stay in a "patching" state,
  671. // albeit with the newer cloned patch.
  672. if( !_asyncSaving )
  673. _state.ApplyAnyPatch();
  674. // No longer need the snapshot.
  675. _stateSnapshotAtLastNewline = null;
  676. }
  677. /// <summary>
  678. /// Advanced usage!
  679. /// If you have a large story, and saving state to JSON takes too long for your
  680. /// framerate, you can temporarily freeze a copy of the state for saving on
  681. /// a separate thread. Internally, the engine maintains a "diff patch".
  682. /// When you've finished saving your state, call BackgroundSaveComplete()
  683. /// and that diff patch will be applied, allowing the story to continue
  684. /// in its usual mode.
  685. /// </summary>
  686. /// <returns>The state for background thread save.</returns>
  687. public StoryState CopyStateForBackgroundThreadSave()
  688. {
  689. IfAsyncWeCant("start saving on a background thread");
  690. if (_asyncSaving) throw new System.Exception("Story is already in background saving mode, can't call CopyStateForBackgroundThreadSave again!");
  691. var stateToSave = _state;
  692. _state = _state.CopyAndStartPatching();
  693. _asyncSaving = true;
  694. return stateToSave;
  695. }
  696. /// <summary>
  697. /// See CopyStateForBackgroundThreadSave. This method releases the
  698. /// "frozen" save state, applying its patch that it was using internally.
  699. /// </summary>
  700. public void BackgroundSaveComplete()
  701. {
  702. // CopyStateForBackgroundThreadSave must be called outside
  703. // of any async ink evaluation, since otherwise you'd be saving
  704. // during an intermediate state.
  705. // However, it's possible to *complete* the save in the middle of
  706. // a glue-lookahead when there's a state stored in _stateSnapshotAtLastNewline.
  707. // This state will have its own patch that is newer than the save patch.
  708. // We hold off on the final apply until the glue-lookahead is finished.
  709. // In that case, the apply is always done, it's just that it may
  710. // apply the looked-ahead changes OR it may simply apply the changes
  711. // made during the save process to the old _stateSnapshotAtLastNewline state.
  712. if ( _stateSnapshotAtLastNewline == null ) {
  713. _state.ApplyAnyPatch();
  714. }
  715. _asyncSaving = false;
  716. }
  717. void Step ()
  718. {
  719. bool shouldAddToStream = true;
  720. // Get current content
  721. var pointer = state.currentPointer;
  722. if (pointer.isNull) {
  723. return;
  724. }
  725. // Step directly to the first element of content in a container (if necessary)
  726. Container containerToEnter = pointer.Resolve () as Container;
  727. while(containerToEnter) {
  728. // Mark container as being entered
  729. VisitContainer (containerToEnter, atStart:true);
  730. // No content? the most we can do is step past it
  731. if (containerToEnter.content.Count == 0)
  732. break;
  733. pointer = Pointer.StartOf (containerToEnter);
  734. containerToEnter = pointer.Resolve() as Container;
  735. }
  736. state.currentPointer = pointer;
  737. if( _profiler != null ) {
  738. _profiler.Step(state.callStack);
  739. }
  740. // Is the current content object:
  741. // - Normal content
  742. // - Or a logic/flow statement - if so, do it
  743. // Stop flow if we hit a stack pop when we're unable to pop (e.g. return/done statement in knot
  744. // that was diverted to rather than called as a function)
  745. var currentContentObj = pointer.Resolve ();
  746. bool isLogicOrFlowControl = PerformLogicAndFlowControl (currentContentObj);
  747. // Has flow been forced to end by flow control above?
  748. if (state.currentPointer.isNull) {
  749. return;
  750. }
  751. if (isLogicOrFlowControl) {
  752. shouldAddToStream = false;
  753. }
  754. // Choice with condition?
  755. var choicePoint = currentContentObj as ChoicePoint;
  756. if (choicePoint) {
  757. var choice = ProcessChoice (choicePoint);
  758. if (choice) {
  759. state.generatedChoices.Add (choice);
  760. }
  761. currentContentObj = null;
  762. shouldAddToStream = false;
  763. }
  764. // If the container has no content, then it will be
  765. // the "content" itself, but we skip over it.
  766. if (currentContentObj is Container) {
  767. shouldAddToStream = false;
  768. }
  769. // Content to add to evaluation stack or the output stream
  770. if (shouldAddToStream) {
  771. // If we're pushing a variable pointer onto the evaluation stack, ensure that it's specific
  772. // to our current (possibly temporary) context index. And make a copy of the pointer
  773. // so that we're not editing the original runtime object.
  774. var varPointer = currentContentObj as VariablePointerValue;
  775. if (varPointer && varPointer.contextIndex == -1) {
  776. // Create new object so we're not overwriting the story's own data
  777. var contextIdx = state.callStack.ContextForVariableNamed(varPointer.variableName);
  778. currentContentObj = new VariablePointerValue (varPointer.variableName, contextIdx);
  779. }
  780. // Expression evaluation content
  781. if (state.inExpressionEvaluation) {
  782. state.PushEvaluationStack (currentContentObj);
  783. }
  784. // Output stream content (i.e. not expression evaluation)
  785. else {
  786. state.PushToOutputStream (currentContentObj);
  787. }
  788. }
  789. // Increment the content pointer, following diverts if necessary
  790. NextContent ();
  791. // Starting a thread should be done after the increment to the content pointer,
  792. // so that when returning from the thread, it returns to the content after this instruction.
  793. var controlCmd = currentContentObj as ControlCommand;
  794. if (controlCmd && controlCmd.commandType == ControlCommand.CommandType.StartThread) {
  795. state.callStack.PushThread ();
  796. }
  797. }
  798. // Mark a container as having been visited
  799. void VisitContainer(Container container, bool atStart)
  800. {
  801. if ( !container.countingAtStartOnly || atStart ) {
  802. if( container.visitsShouldBeCounted )
  803. state.IncrementVisitCountForContainer (container);
  804. if (container.turnIndexShouldBeCounted)
  805. state.RecordTurnIndexVisitToContainer (container);
  806. }
  807. }
  808. List<Container> _prevContainers = new List<Container>();
  809. void VisitChangedContainersDueToDivert()
  810. {
  811. var previousPointer = state.previousPointer;
  812. var pointer = state.currentPointer;
  813. // Unless we're pointing *directly* at a piece of content, we don't do
  814. // counting here. Otherwise, the main stepping function will do the counting.
  815. if (pointer.isNull || pointer.index == -1)
  816. return;
  817. // First, find the previously open set of containers
  818. _prevContainers.Clear();
  819. if (!previousPointer.isNull) {
  820. Container prevAncestor = previousPointer.Resolve() as Container ?? previousPointer.container as Container;
  821. while (prevAncestor) {
  822. _prevContainers.Add (prevAncestor);
  823. prevAncestor = prevAncestor.parent as Container;
  824. }
  825. }
  826. // If the new object is a container itself, it will be visited automatically at the next actual
  827. // content step. However, we need to walk up the new ancestry to see if there are more new containers
  828. Runtime.Object currentChildOfContainer = pointer.Resolve();
  829. // Invalid pointer? May happen if attemptingto
  830. if (currentChildOfContainer == null) return;
  831. Container currentContainerAncestor = currentChildOfContainer.parent as Container;
  832. bool allChildrenEnteredAtStart = true;
  833. while (currentContainerAncestor && (!_prevContainers.Contains(currentContainerAncestor) || currentContainerAncestor.countingAtStartOnly)) {
  834. // Check whether this ancestor container is being entered at the start,
  835. // by checking whether the child object is the first.
  836. bool enteringAtStart = currentContainerAncestor.content.Count > 0
  837. && currentChildOfContainer == currentContainerAncestor.content [0]
  838. && allChildrenEnteredAtStart;
  839. // Don't count it as entering at start if we're entering random somewhere within
  840. // a container B that happens to be nested at index 0 of container A. It only counts
  841. // if we're diverting directly to the first leaf node.
  842. if (!enteringAtStart)
  843. allChildrenEnteredAtStart = false;
  844. // Mark a visit to this container
  845. VisitContainer (currentContainerAncestor, enteringAtStart);
  846. currentChildOfContainer = currentContainerAncestor;
  847. currentContainerAncestor = currentContainerAncestor.parent as Container;
  848. }
  849. }
  850. string PopChoiceStringAndTags(ref List<string> tags)
  851. {
  852. var choiceOnlyStrVal = (StringValue) state.PopEvaluationStack ();
  853. while( state.evaluationStack.Count > 0 && state.PeekEvaluationStack() is Tag ) {
  854. if( tags == null ) tags = new List<string>();
  855. var tag = (Tag)state.PopEvaluationStack ();
  856. tags.Insert(0, tag.text); // popped in reverse order
  857. }
  858. return choiceOnlyStrVal.value;
  859. }
  860. Choice ProcessChoice(ChoicePoint choicePoint)
  861. {
  862. bool showChoice = true;
  863. // Don't create choice if choice point doesn't pass conditional
  864. if (choicePoint.hasCondition) {
  865. var conditionValue = state.PopEvaluationStack ();
  866. if (!IsTruthy (conditionValue)) {
  867. showChoice = false;
  868. }
  869. }
  870. string startText = "";
  871. string choiceOnlyText = "";
  872. var tags = (List<string>)null;
  873. if (choicePoint.hasChoiceOnlyContent) {
  874. choiceOnlyText = PopChoiceStringAndTags(ref tags);
  875. }
  876. if (choicePoint.hasStartContent) {
  877. startText = PopChoiceStringAndTags(ref tags);
  878. }
  879. // Don't create choice if player has already read this content
  880. if (choicePoint.onceOnly) {
  881. var visitCount = state.VisitCountForContainer (choicePoint.choiceTarget);
  882. if (visitCount > 0) {
  883. showChoice = false;
  884. }
  885. }
  886. // We go through the full process of creating the choice above so
  887. // that we consume the content for it, since otherwise it'll
  888. // be shown on the output stream.
  889. if (!showChoice) {
  890. return null;
  891. }
  892. var choice = new Choice ();
  893. choice.targetPath = choicePoint.pathOnChoice;
  894. choice.sourcePath = choicePoint.path.ToString ();
  895. choice.isInvisibleDefault = choicePoint.isInvisibleDefault;
  896. choice.tags = tags;
  897. // We need to capture the state of the callstack at the point where
  898. // the choice was generated, since after the generation of this choice
  899. // we may go on to pop out from a tunnel (possible if the choice was
  900. // wrapped in a conditional), or we may pop out from a thread,
  901. // at which point that thread is discarded.
  902. // Fork clones the thread, gives it a new ID, but without affecting
  903. // the thread stack itself.
  904. choice.threadAtGeneration = state.callStack.ForkThread();
  905. // Set final text for the choice
  906. choice.text = (startText + choiceOnlyText).Trim(' ', '\t');
  907. return choice;
  908. }
  909. // Does the expression result represented by this object evaluate to true?
  910. // e.g. is it a Number that's not equal to 1?
  911. bool IsTruthy(Runtime.Object obj)
  912. {
  913. bool truthy = false;
  914. if (obj is Value) {
  915. var val = (Value)obj;
  916. if (val is DivertTargetValue) {
  917. var divTarget = (DivertTargetValue)val;
  918. Error ("Shouldn't use a divert target (to " + divTarget.targetPath + ") as a conditional value. Did you intend a function call 'likeThis()' or a read count check 'likeThis'? (no arrows)");
  919. return false;
  920. }
  921. return val.isTruthy;
  922. }
  923. return truthy;
  924. }
  925. /// <summary>
  926. /// Checks whether contentObj is a control or flow object rather than a piece of content,
  927. /// and performs the required command if necessary.
  928. /// </summary>
  929. /// <returns><c>true</c> if object was logic or flow control, <c>false</c> if it's normal content.</returns>
  930. /// <param name="contentObj">Content object.</param>
  931. bool PerformLogicAndFlowControl(Runtime.Object contentObj)
  932. {
  933. if( contentObj == null ) {
  934. return false;
  935. }
  936. // Divert
  937. if (contentObj is Divert) {
  938. Divert currentDivert = (Divert)contentObj;
  939. if (currentDivert.isConditional) {
  940. var conditionValue = state.PopEvaluationStack ();
  941. // False conditional? Cancel divert
  942. if (!IsTruthy (conditionValue))
  943. return true;
  944. }
  945. if (currentDivert.hasVariableTarget) {
  946. var varName = currentDivert.variableDivertName;
  947. var varContents = state.variablesState.GetVariableWithName (varName);
  948. if (varContents == null) {
  949. Error ("Tried to divert using a target from a variable that could not be found (" + varName + ")");
  950. }
  951. else if (!(varContents is DivertTargetValue)) {
  952. var intContent = varContents as IntValue;
  953. string errorMessage = "Tried to divert to a target from a variable, but the variable (" + varName + ") didn't contain a divert target, it ";
  954. if (intContent && intContent.value == 0) {
  955. errorMessage += "was empty/null (the value 0).";
  956. } else {
  957. errorMessage += "contained '" + varContents + "'.";
  958. }
  959. Error (errorMessage);
  960. }
  961. var target = (DivertTargetValue)varContents;
  962. state.divertedPointer = PointerAtPath(target.targetPath);
  963. } else if (currentDivert.isExternal) {
  964. CallExternalFunction (currentDivert.targetPathString, currentDivert.externalArgs);
  965. return true;
  966. } else {
  967. state.divertedPointer = currentDivert.targetPointer;
  968. }
  969. if (currentDivert.pushesToStack) {
  970. state.callStack.Push (
  971. currentDivert.stackPushType,
  972. outputStreamLengthWithPushed:state.outputStream.Count
  973. );
  974. }
  975. if (state.divertedPointer.isNull && !currentDivert.isExternal) {
  976. // Human readable name available - runtime divert is part of a hard-written divert that to missing content
  977. if (currentDivert && currentDivert.debugMetadata.sourceName != null) {
  978. Error ("Divert target doesn't exist: " + currentDivert.debugMetadata.sourceName);
  979. } else {
  980. Error ("Divert resolution failed: " + currentDivert);
  981. }
  982. }
  983. return true;
  984. }
  985. // Start/end an expression evaluation? Or print out the result?
  986. else if( contentObj is ControlCommand ) {
  987. var evalCommand = (ControlCommand) contentObj;
  988. switch (evalCommand.commandType) {
  989. case ControlCommand.CommandType.EvalStart:
  990. Assert (state.inExpressionEvaluation == false, "Already in expression evaluation?");
  991. state.inExpressionEvaluation = true;
  992. break;
  993. case ControlCommand.CommandType.EvalEnd:
  994. Assert (state.inExpressionEvaluation == true, "Not in expression evaluation mode");
  995. state.inExpressionEvaluation = false;
  996. break;
  997. case ControlCommand.CommandType.EvalOutput:
  998. // If the expression turned out to be empty, there may not be anything on the stack
  999. if (state.evaluationStack.Count > 0) {
  1000. var output = state.PopEvaluationStack ();
  1001. // Functions may evaluate to Void, in which case we skip output
  1002. if (!(output is Void)) {
  1003. // TODO: Should we really always blanket convert to string?
  1004. // It would be okay to have numbers in the output stream the
  1005. // only problem is when exporting text for viewing, it skips over numbers etc.
  1006. var text = new StringValue (output.ToString ());
  1007. state.PushToOutputStream (text);
  1008. }
  1009. }
  1010. break;
  1011. case ControlCommand.CommandType.NoOp:
  1012. break;
  1013. case ControlCommand.CommandType.Duplicate:
  1014. state.PushEvaluationStack (state.PeekEvaluationStack ());
  1015. break;
  1016. case ControlCommand.CommandType.PopEvaluatedValue:
  1017. state.PopEvaluationStack ();
  1018. break;
  1019. case ControlCommand.CommandType.PopFunction:
  1020. case ControlCommand.CommandType.PopTunnel:
  1021. var popType = evalCommand.commandType == ControlCommand.CommandType.PopFunction ?
  1022. PushPopType.Function : PushPopType.Tunnel;
  1023. // Tunnel onwards is allowed to specify an optional override
  1024. // divert to go to immediately after returning: ->-> target
  1025. DivertTargetValue overrideTunnelReturnTarget = null;
  1026. if (popType == PushPopType.Tunnel) {
  1027. var popped = state.PopEvaluationStack ();
  1028. overrideTunnelReturnTarget = popped as DivertTargetValue;
  1029. if (overrideTunnelReturnTarget == null) {
  1030. Assert (popped is Void, "Expected void if ->-> doesn't override target");
  1031. }
  1032. }
  1033. if (state.TryExitFunctionEvaluationFromGame ()) {
  1034. break;
  1035. }
  1036. else if (state.callStack.currentElement.type != popType || !state.callStack.canPop) {
  1037. var names = new Dictionary<PushPopType, string> ();
  1038. names [PushPopType.Function] = "function return statement (~ return)";
  1039. names [PushPopType.Tunnel] = "tunnel onwards statement (->->)";
  1040. string expected = names [state.callStack.currentElement.type];
  1041. if (!state.callStack.canPop) {
  1042. expected = "end of flow (-> END or choice)";
  1043. }
  1044. var errorMsg = string.Format ("Found {0}, when expected {1}", names [popType], expected);
  1045. Error (errorMsg);
  1046. }
  1047. else {
  1048. state.PopCallstack ();
  1049. // Does tunnel onwards override by diverting to a new ->-> target?
  1050. if( overrideTunnelReturnTarget )
  1051. state.divertedPointer = PointerAtPath (overrideTunnelReturnTarget.targetPath);
  1052. }
  1053. break;
  1054. case ControlCommand.CommandType.BeginString:
  1055. state.PushToOutputStream (evalCommand);
  1056. Assert (state.inExpressionEvaluation == true, "Expected to be in an expression when evaluating a string");
  1057. state.inExpressionEvaluation = false;
  1058. break;
  1059. // Leave it to story.currentText and story.currentTags to sort out the text from the tags
  1060. // This is mostly because we can't always rely on the existence of EndTag, and we don't want
  1061. // to try and flatten dynamic tags to strings every time \n is pushed to output
  1062. case ControlCommand.CommandType.BeginTag:
  1063. state.PushToOutputStream (evalCommand);
  1064. break;
  1065. case ControlCommand.CommandType.EndTag: {
  1066. // EndTag has 2 modes:
  1067. // - When in string evaluation (for choices)
  1068. // - Normal
  1069. //
  1070. // The only way you could have an EndTag in the middle of
  1071. // string evaluation is if we're currently generating text for a
  1072. // choice, such as:
  1073. //
  1074. // + choice # tag
  1075. //
  1076. // In the above case, the ink will be run twice:
  1077. // - First, to generate the choice text. String evaluation
  1078. // will be on, and the final string will be pushed to the
  1079. // evaluation stack, ready to be popped to make a Choice
  1080. // object.
  1081. // - Second, when ink generates text after choosing the choice.
  1082. // On this ocassion, it's not in string evaluation mode.
  1083. //
  1084. // On the writing side, we disallow manually putting tags within
  1085. // strings like this:
  1086. //
  1087. // {"hello # world"}
  1088. //
  1089. // So we know that the tag must be being generated as part of
  1090. // choice content. Therefore, when the tag has been generated,
  1091. // we push it onto the evaluation stack in the exact same way
  1092. // as the string for the choice content.
  1093. if( state.inStringEvaluation ) {
  1094. var contentStackForTag = new Stack<Runtime.Object> ();
  1095. int outputCountConsumed = 0;
  1096. for (int i = state.outputStream.Count - 1; i >= 0; --i) {
  1097. var obj = state.outputStream [i];
  1098. outputCountConsumed++;
  1099. var command = obj as ControlCommand;
  1100. if (command != null) {
  1101. if( command.commandType == ControlCommand.CommandType.BeginTag ) {
  1102. break;
  1103. } else {
  1104. Error("Unexpected ControlCommand while extracting tag from choice");
  1105. break;
  1106. }
  1107. }
  1108. if( obj is StringValue)
  1109. contentStackForTag.Push (obj);
  1110. }
  1111. // Consume the content that was produced for this string
  1112. state.PopFromOutputStream (outputCountConsumed);
  1113. var sb = new StringBuilder();
  1114. foreach(StringValue strVal in contentStackForTag) {
  1115. sb.Append(strVal.value);
  1116. }
  1117. var choiceTag = new Tag(state.CleanOutputWhitespace(sb.ToString()));
  1118. // Pushing to the evaluation stack means it gets picked up
  1119. // when a Choice is generated from the next Choice Point.
  1120. state.PushEvaluationStack(choiceTag);
  1121. }
  1122. // Otherwise! Simply push EndTag, so that in the output stream we
  1123. // have a structure of: [BeginTag, "the tag content", EndTag]
  1124. else {
  1125. state.PushToOutputStream (evalCommand);
  1126. }
  1127. break;
  1128. }
  1129. // Dynamic strings and tags are built in the same way
  1130. case ControlCommand.CommandType.EndString: {
  1131. // Since we're iterating backward through the content,
  1132. // build a stack so that when we build the string,
  1133. // it's in the right order
  1134. var contentStackForString = new Stack<Runtime.Object> ();
  1135. var contentToRetain = new Stack<Runtime.Object>();
  1136. int outputCountConsumed = 0;
  1137. for (int i = state.outputStream.Count - 1; i >= 0; --i) {
  1138. var obj = state.outputStream [i];
  1139. outputCountConsumed++;
  1140. var command = obj as ControlCommand;
  1141. if (command != null && command.commandType == ControlCommand.CommandType.BeginString) {
  1142. break;
  1143. }
  1144. if( obj is Tag )
  1145. contentToRetain.Push(obj);
  1146. if( obj is StringValue )
  1147. contentStackForString.Push (obj);
  1148. }
  1149. // Consume the content that was produced for this string
  1150. state.PopFromOutputStream (outputCountConsumed);
  1151. // Rescue the tags that we want actually to keep on the output stack
  1152. // rather than consume as part of the string we're building.
  1153. // At the time of writing, this only applies to Tag objects generated
  1154. // by choices, which are pushed to the stack during string generation.
  1155. foreach(var rescuedTag in contentToRetain)
  1156. state.PushToOutputStream(rescuedTag);
  1157. // Build string out of the content we collected
  1158. var sb = new StringBuilder ();
  1159. foreach (var c in contentStackForString) {
  1160. sb.Append (c.ToString ());
  1161. }
  1162. // Return to expression evaluation (from content mode)
  1163. state.inExpressionEvaluation = true;
  1164. state.PushEvaluationStack (new StringValue (sb.ToString ()));
  1165. break;
  1166. }
  1167. case ControlCommand.CommandType.ChoiceCount:
  1168. var choiceCount = state.generatedChoices.Count;
  1169. state.PushEvaluationStack (new Runtime.IntValue (choiceCount));
  1170. break;
  1171. case ControlCommand.CommandType.Turns:
  1172. state.PushEvaluationStack (new IntValue (state.currentTurnIndex+1));
  1173. break;
  1174. case ControlCommand.CommandType.TurnsSince:
  1175. case ControlCommand.CommandType.ReadCount:
  1176. var target = state.PopEvaluationStack();
  1177. if( !(target is DivertTargetValue) ) {
  1178. string extraNote = "";
  1179. if( target is IntValue )
  1180. extraNote = ". Did you accidentally pass a read count ('knot_name') instead of a target ('-> knot_name')?";
  1181. Error("TURNS_SINCE expected a divert target (knot, stitch, label name), but saw "+target+extraNote);
  1182. break;
  1183. }
  1184. var divertTarget = target as DivertTargetValue;
  1185. var container = ContentAtPath (divertTarget.targetPath).correctObj as Container;
  1186. int eitherCount;
  1187. if (container != null) {
  1188. if (evalCommand.commandType == ControlCommand.CommandType.TurnsSince)
  1189. eitherCount = state.TurnsSinceForContainer (container);
  1190. else
  1191. eitherCount = state.VisitCountForContainer (container);
  1192. } else {
  1193. if (evalCommand.commandType == ControlCommand.CommandType.TurnsSince)
  1194. eitherCount = -1; // turn count, default to never/unknown
  1195. else
  1196. eitherCount = 0; // visit count, assume 0 to default to allowing entry
  1197. Warning ("Failed to find container for " + evalCommand.ToString () + " lookup at " + divertTarget.targetPath.ToString ());
  1198. }
  1199. state.PushEvaluationStack (new IntValue (eitherCount));
  1200. break;
  1201. case ControlCommand.CommandType.Random: {
  1202. var maxInt = state.PopEvaluationStack () as IntValue;
  1203. var minInt = state.PopEvaluationStack () as IntValue;
  1204. if (minInt == null)
  1205. Error ("Invalid value for minimum parameter of RANDOM(min, max)");
  1206. if (maxInt == null)
  1207. Error ("Invalid value for maximum parameter of RANDOM(min, max)");
  1208. // +1 because it's inclusive of min and max, for e.g. RANDOM(1,6) for a dice roll.
  1209. int randomRange;
  1210. try {
  1211. randomRange = checked(maxInt.value - minInt.value + 1);
  1212. } catch (System.OverflowException) {
  1213. randomRange = int.MaxValue;
  1214. Error("RANDOM was called with a range that exceeds the size that ink numbers can use.");
  1215. }
  1216. if (randomRange <= 0)
  1217. Error ("RANDOM was called with minimum as " + minInt.value + " and maximum as " + maxInt.value + ". The maximum must be larger");
  1218. var resultSeed = state.storySeed + state.previousRandom;
  1219. var random = new Random (resultSeed);
  1220. var nextRandom = random.Next ();
  1221. var chosenValue = (nextRandom % randomRange) + minInt.value;
  1222. state.PushEvaluationStack (new IntValue (chosenValue));
  1223. // Next random number (rather than keeping the Random object around)
  1224. state.previousRandom = nextRandom;
  1225. break;
  1226. }
  1227. case ControlCommand.CommandType.SeedRandom:
  1228. var seed = state.PopEvaluationStack () as IntValue;
  1229. if (seed == null)
  1230. Error ("Invalid value passed to SEED_RANDOM");
  1231. // Story seed affects both RANDOM and shuffle behaviour
  1232. state.storySeed = seed.value;
  1233. state.previousRandom = 0;
  1234. // SEED_RANDOM returns nothing.
  1235. state.PushEvaluationStack (new Runtime.Void ());
  1236. break;
  1237. case ControlCommand.CommandType.VisitIndex:
  1238. var count = state.VisitCountForContainer(state.currentPointer.container) - 1; // index not count
  1239. state.PushEvaluationStack (new IntValue (count));
  1240. break;
  1241. case ControlCommand.CommandType.SequenceShuffleIndex:
  1242. var shuffleIndex = NextSequenceShuffleIndex ();
  1243. state.PushEvaluationStack (new IntValue (shuffleIndex));
  1244. break;
  1245. case ControlCommand.CommandType.StartThread:
  1246. // Handled in main step function
  1247. break;
  1248. case ControlCommand.CommandType.Done:
  1249. // We may exist in the context of the initial
  1250. // act of creating the thread, or in the context of
  1251. // evaluating the content.
  1252. if (state.callStack.canPopThread) {
  1253. state.callStack.PopThread ();
  1254. }
  1255. // In normal flow - allow safe exit without warning
  1256. else {
  1257. state.didSafeExit = true;
  1258. // Stop flow in current thread
  1259. state.currentPointer = Pointer.Null;
  1260. }
  1261. break;
  1262. // Force flow to end completely
  1263. case ControlCommand.CommandType.End:
  1264. state.ForceEnd ();
  1265. break;
  1266. case ControlCommand.CommandType.ListFromInt:
  1267. var intVal = state.PopEvaluationStack () as IntValue;
  1268. var listNameVal = state.PopEvaluationStack () as StringValue;
  1269. if (intVal == null) {
  1270. throw new StoryException ("Passed non-integer when creating a list element from a numerical value.");
  1271. }
  1272. ListValue generatedListValue = null;
  1273. ListDefinition foundListDef;
  1274. if (listDefinitions.TryListGetDefinition (listNameVal.value, out foundListDef)) {
  1275. InkListItem foundItem;
  1276. if (foundListDef.TryGetItemWithValue (intVal.value, out foundItem)) {
  1277. generatedListValue = new ListValue (foundItem, intVal.value);
  1278. }
  1279. } else {
  1280. throw new StoryException ("Failed to find LIST called " + listNameVal.value);
  1281. }
  1282. if (generatedListValue == null)
  1283. generatedListValue = new ListValue ();
  1284. state.PushEvaluationStack (generatedListValue);
  1285. break;
  1286. case ControlCommand.CommandType.ListRange: {
  1287. var max = state.PopEvaluationStack () as Value;
  1288. var min = state.PopEvaluationStack () as Value;
  1289. var targetList = state.PopEvaluationStack () as ListValue;
  1290. if (targetList == null || min == null || max == null)
  1291. throw new StoryException ("Expected list, minimum and maximum for LIST_RANGE");
  1292. var result = targetList.value.ListWithSubRange(min.valueObject, max.valueObject);
  1293. state.PushEvaluationStack (new ListValue(result));
  1294. break;
  1295. }
  1296. case ControlCommand.CommandType.ListRandom: {
  1297. var listVal = state.PopEvaluationStack () as ListValue;
  1298. if (listVal == null)
  1299. throw new StoryException ("Expected list for LIST_RANDOM");
  1300. var list = listVal.value;
  1301. InkList newList = null;
  1302. // List was empty: return empty list
  1303. if (list.Count == 0) {
  1304. newList = new InkList ();
  1305. }
  1306. // Non-empty source list
  1307. else {
  1308. // Generate a random index for the element to take
  1309. var resultSeed = state.storySeed + state.previousRandom;
  1310. var random = new Random (resultSeed);
  1311. var nextRandom = random.Next ();
  1312. var listItemIndex = nextRandom % list.Count;
  1313. // Iterate through to get the random element
  1314. var listEnumerator = list.GetEnumerator ();
  1315. for (int i = 0; i <= listItemIndex; i++) {
  1316. listEnumerator.MoveNext ();
  1317. }
  1318. var randomItem = listEnumerator.Current;
  1319. // Origin list is simply the origin of the one element
  1320. newList = new InkList (randomItem.Key.originName, this);
  1321. newList.Add (randomItem.Key, randomItem.Value);
  1322. state.previousRandom = nextRandom;
  1323. }
  1324. state.PushEvaluationStack (new ListValue(newList));
  1325. break;
  1326. }
  1327. default:
  1328. Error ("unhandled ControlCommand: " + evalCommand);
  1329. break;
  1330. }
  1331. return true;
  1332. }
  1333. // Variable assignment
  1334. else if( contentObj is VariableAssignment ) {
  1335. var varAss = (VariableAssignment) contentObj;
  1336. var assignedVal = state.PopEvaluationStack();
  1337. // When in temporary evaluation, don't create new variables purely within
  1338. // the temporary context, but attempt to create them globally
  1339. //var prioritiseHigherInCallStack = _temporaryEvaluationContainer != null;
  1340. state.variablesState.Assign (varAss, assignedVal);
  1341. return true;
  1342. }
  1343. // Variable reference
  1344. else if( contentObj is VariableReference ) {
  1345. var varRef = (VariableReference)contentObj;
  1346. Runtime.Object foundValue = null;
  1347. // Explicit read count value
  1348. if (varRef.pathForCount != null) {
  1349. var container = varRef.containerForCount;
  1350. int count = state.VisitCountForContainer (container);
  1351. foundValue = new IntValue (count);
  1352. }
  1353. // Normal variable reference
  1354. else {
  1355. foundValue = state.variablesState.GetVariableWithName (varRef.name);
  1356. if (foundValue == null) {
  1357. Warning ("Variable not found: '" + varRef.name + "'. Using default value of 0 (false). This can happen with temporary variables if the declaration hasn't yet been hit. Globals are always given a default value on load if a value doesn't exist in the save state.");
  1358. foundValue = new IntValue (0);
  1359. }
  1360. }
  1361. state.PushEvaluationStack (foundValue);
  1362. return true;
  1363. }
  1364. // Native function call
  1365. else if (contentObj is NativeFunctionCall) {
  1366. var func = (NativeFunctionCall)contentObj;
  1367. var funcParams = state.PopEvaluationStack (func.numberOfParameters);
  1368. var result = func.Call (funcParams);
  1369. state.PushEvaluationStack (result);
  1370. return true;
  1371. }
  1372. // No control content, must be ordinary content
  1373. return false;
  1374. }
  1375. /// <summary>
  1376. /// Change the current position of the story to the given path. From here you can
  1377. /// call Continue() to evaluate the next line.
  1378. ///
  1379. /// The path string is a dot-separated path as used internally by the engine.
  1380. /// These examples should work:
  1381. ///
  1382. /// myKnot
  1383. /// myKnot.myStitch
  1384. ///
  1385. /// Note however that this won't necessarily work:
  1386. ///
  1387. /// myKnot.myStitch.myLabelledChoice
  1388. ///
  1389. /// ...because of the way that content is nested within a weave structure.
  1390. ///
  1391. /// By default this will reset the callstack beforehand, which means that any
  1392. /// tunnels, threads or functions you were in at the time of calling will be
  1393. /// discarded. This is different from the behaviour of ChooseChoiceIndex, which
  1394. /// will always keep the callstack, since the choices are known to come from the
  1395. /// correct state, and known their source thread.
  1396. ///
  1397. /// You have the option of passing false to the resetCallstack parameter if you
  1398. /// don't want this behaviour, and will leave any active threads, tunnels or
  1399. /// function calls in-tact.
  1400. ///
  1401. /// This is potentially dangerous! If you're in the middle of a tunnel,
  1402. /// it'll redirect only the inner-most tunnel, meaning that when you tunnel-return
  1403. /// using '->->', it'll return to where you were before. This may be what you
  1404. /// want though. However, if you're in the middle of a function, ChoosePathString
  1405. /// will throw an exception.
  1406. ///
  1407. /// </summary>
  1408. /// <param name="path">A dot-separted path string, as specified above.</param>
  1409. /// <param name="resetCallstack">Whether to reset the callstack first (see summary description).</param>
  1410. /// <param name="arguments">Optional set of arguments to pass, if path is to a knot that takes them.</param>
  1411. public void ChoosePathString (string path, bool resetCallstack = true, params object [] arguments)
  1412. {
  1413. IfAsyncWeCant ("call ChoosePathString right now");
  1414. if(onChoosePathString != null) onChoosePathString(path, arguments);
  1415. if (resetCallstack) {
  1416. ResetCallstack ();
  1417. } else {
  1418. // ChoosePathString is potentially dangerous since you can call it when the stack is
  1419. // pretty much in any state. Let's catch one of the worst offenders.
  1420. if (state.callStack.currentElement.type == PushPopType.Function) {
  1421. string funcDetail = "";
  1422. var container = state.callStack.currentElement.currentPointer.container;
  1423. if (container != null) {
  1424. funcDetail = "("+container.path.ToString ()+") ";
  1425. }
  1426. throw new System.Exception ("Story was running a function "+funcDetail+"when you called ChoosePathString("+path+") - this is almost certainly not not what you want! Full stack trace: \n"+state.callStack.callStackTrace);
  1427. }
  1428. }
  1429. state.PassArgumentsToEvaluationStack (arguments);
  1430. ChoosePath (new Path (path));
  1431. }
  1432. void IfAsyncWeCant (string activityStr)
  1433. {
  1434. if (_asyncContinueActive)
  1435. throw new System.Exception ("Can't " + activityStr + ". Story is in the middle of a ContinueAsync(). Make more ContinueAsync() calls or a single Continue() call beforehand.");
  1436. }
  1437. public void ChoosePath(Path p, bool incrementingTurnIndex = true)
  1438. {
  1439. state.SetChosenPath (p, incrementingTurnIndex);
  1440. // Take a note of newly visited containers for read counts etc
  1441. VisitChangedContainersDueToDivert ();
  1442. }
  1443. /// <summary>
  1444. /// Chooses the Choice from the currentChoices list with the given
  1445. /// index. Internally, this sets the current content path to that
  1446. /// pointed to by the Choice, ready to continue story evaluation.
  1447. /// </summary>
  1448. public void ChooseChoiceIndex(int choiceIdx)
  1449. {
  1450. var choices = currentChoices;
  1451. Assert (choiceIdx >= 0 && choiceIdx < choices.Count, "choice out of range");
  1452. // Replace callstack with the one from the thread at the choosing point,
  1453. // so that we can jump into the right place in the flow.
  1454. // This is important in case the flow was forked by a new thread, which
  1455. // can create multiple leading edges for the story, each of
  1456. // which has its own context.
  1457. var choiceToChoose = choices [choiceIdx];
  1458. if(onMakeChoice != null) onMakeChoice(choiceToChoose);
  1459. state.callStack.currentThread = choiceToChoose.threadAtGeneration;
  1460. ChoosePath (choiceToChoose.targetPath);
  1461. }
  1462. /// <summary>
  1463. /// Checks if a function exists.
  1464. /// </summary>
  1465. /// <returns>True if the function exists, else false.</returns>
  1466. /// <param name="functionName">The name of the function as declared in ink.</param>
  1467. public bool HasFunction (string functionName)
  1468. {
  1469. try {
  1470. return KnotContainerWithName (functionName) != null;
  1471. } catch {
  1472. return false;
  1473. }
  1474. }
  1475. /// <summary>
  1476. /// Evaluates a function defined in ink.
  1477. /// </summary>
  1478. /// <returns>The return value as returned from the ink function with `~ return myValue`, or null if nothing is returned.</returns>
  1479. /// <param name="functionName">The name of the function as declared in ink.</param>
  1480. /// <param name="arguments">The arguments that the ink function takes, if any. Note that we don't (can't) do any validation on the number of arguments right now, so make sure you get it right!</param>
  1481. public object EvaluateFunction (string functionName, params object [] arguments)
  1482. {
  1483. string _;
  1484. return EvaluateFunction (functionName, out _, arguments);
  1485. }
  1486. /// <summary>
  1487. /// Evaluates a function defined in ink, and gathers the possibly multi-line text as generated by the function.
  1488. /// This text output is any text written as normal content within the function, as opposed to the return value, as returned with `~ return`.
  1489. /// </summary>
  1490. /// <returns>The return value as returned from the ink function with `~ return myValue`, or null if nothing is returned.</returns>
  1491. /// <param name="functionName">The name of the function as declared in ink.</param>
  1492. /// <param name="textOutput">The text content produced by the function via normal ink, if any.</param>
  1493. /// <param name="arguments">The arguments that the ink function takes, if any. Note that we don't (can't) do any validation on the number of arguments right now, so make sure you get it right!</param>
  1494. public object EvaluateFunction (string functionName, out string textOutput, params object [] arguments)
  1495. {
  1496. if(onEvaluateFunction != null) onEvaluateFunction(functionName, arguments);
  1497. IfAsyncWeCant ("evaluate a function");
  1498. if(functionName == null) {
  1499. throw new System.Exception ("Function is null");
  1500. } else if(functionName == string.Empty || functionName.Trim() == string.Empty) {
  1501. throw new System.Exception ("Function is empty or white space.");
  1502. }
  1503. // Get the content that we need to run
  1504. var funcContainer = KnotContainerWithName (functionName);
  1505. if( funcContainer == null )
  1506. throw new System.Exception ("Function doesn't exist: '" + functionName + "'");
  1507. // Snapshot the output stream
  1508. var outputStreamBefore = new List<Runtime.Object>(state.outputStream);
  1509. _state.ResetOutput ();
  1510. // State will temporarily replace the callstack in order to evaluate
  1511. state.StartFunctionEvaluationFromGame (funcContainer, arguments);
  1512. // Evaluate the function, and collect the string output
  1513. var stringOutput = new StringBuilder ();
  1514. while (canContinue) {
  1515. stringOutput.Append (Continue ());
  1516. }
  1517. textOutput = stringOutput.ToString ();
  1518. // Restore the output stream in case this was called
  1519. // during main story evaluation.
  1520. _state.ResetOutput (outputStreamBefore);
  1521. // Finish evaluation, and see whether anything was produced
  1522. var result = state.CompleteFunctionEvaluationFromGame ();
  1523. if(onCompleteEvaluateFunction != null) onCompleteEvaluateFunction(functionName, arguments, textOutput, result);
  1524. return result;
  1525. }
  1526. // Evaluate a "hot compiled" piece of ink content, as used by the REPL-like
  1527. // CommandLinePlayer.
  1528. public Runtime.Object EvaluateExpression(Runtime.Container exprContainer)
  1529. {
  1530. int startCallStackHeight = state.callStack.elements.Count;
  1531. state.callStack.Push (PushPopType.Tunnel);
  1532. _temporaryEvaluationContainer = exprContainer;
  1533. state.GoToStart ();
  1534. int evalStackHeight = state.evaluationStack.Count;
  1535. Continue ();
  1536. _temporaryEvaluationContainer = null;
  1537. // Should have fallen off the end of the Container, which should
  1538. // have auto-popped, but just in case we didn't for some reason,
  1539. // manually pop to restore the state (including currentPath).
  1540. if (state.callStack.elements.Count > startCallStackHeight) {
  1541. state.PopCallstack ();
  1542. }
  1543. int endStackHeight = state.evaluationStack.Count;
  1544. if (endStackHeight > evalStackHeight) {
  1545. return state.PopEvaluationStack ();
  1546. } else {
  1547. return null;
  1548. }
  1549. }
  1550. /// <summary>
  1551. /// An ink file can provide a fallback functions for when when an EXTERNAL has been left
  1552. /// unbound by the client, and the fallback function will be called instead. Useful when
  1553. /// testing a story in playmode, when it's not possible to write a client-side C# external
  1554. /// function, but you don't want it to fail to run.
  1555. /// </summary>
  1556. public bool allowExternalFunctionFallbacks { get; set; }
  1557. public bool TryGetExternalFunction(string functionName, out ExternalFunction externalFunction) {
  1558. ExternalFunctionDef externalFunctionDef;
  1559. if(_externals.TryGetValue (functionName, out externalFunctionDef)) {
  1560. externalFunction = externalFunctionDef.function;
  1561. return true;
  1562. } else {
  1563. externalFunction = null;
  1564. return false;
  1565. }
  1566. }
  1567. public void CallExternalFunction(string funcName, int numberOfArguments)
  1568. {
  1569. ExternalFunctionDef funcDef;
  1570. Container fallbackFunctionContainer = null;
  1571. var foundExternal = _externals.TryGetValue (funcName, out funcDef);
  1572. // Should this function break glue? Abort run if we've already seen a newline.
  1573. // Set a bool to tell it to restore the snapshot at the end of this instruction.
  1574. if( foundExternal && !funcDef.lookaheadSafe && _stateSnapshotAtLastNewline != null ) {
  1575. _sawLookaheadUnsafeFunctionAfterNewline = true;
  1576. return;
  1577. }
  1578. // Try to use fallback function?
  1579. if (!foundExternal) {
  1580. if (allowExternalFunctionFallbacks) {
  1581. fallbackFunctionContainer = KnotContainerWithName (funcName);
  1582. Assert (fallbackFunctionContainer != null, "Trying to call EXTERNAL function '" + funcName + "' which has not been bound, and fallback ink function could not be found.");
  1583. // Divert direct into fallback function and we're done
  1584. state.callStack.Push (
  1585. PushPopType.Function,
  1586. outputStreamLengthWithPushed:state.outputStream.Count
  1587. );
  1588. state.divertedPointer = Pointer.StartOf(fallbackFunctionContainer);
  1589. return;
  1590. } else {
  1591. Assert (false, "Trying to call EXTERNAL function '" + funcName + "' which has not been bound (and ink fallbacks disabled).");
  1592. }
  1593. }
  1594. // Pop arguments
  1595. var arguments = new List<object>();
  1596. for (int i = 0; i < numberOfArguments; ++i) {
  1597. var poppedObj = state.PopEvaluationStack () as Value;
  1598. var valueObj = poppedObj.valueObject;
  1599. arguments.Add (valueObj);
  1600. }
  1601. // Reverse arguments from the order they were popped,
  1602. // so they're the right way round again.
  1603. arguments.Reverse ();
  1604. // Run the function!
  1605. object funcResult = funcDef.function (arguments.ToArray());
  1606. // Convert return value (if any) to the a type that the ink engine can use
  1607. Runtime.Object returnObj = null;
  1608. if (funcResult != null) {
  1609. returnObj = Value.Create (funcResult);
  1610. Assert (returnObj != null, "Could not create ink value from returned object of type " + funcResult.GetType());
  1611. } else {
  1612. returnObj = new Runtime.Void ();
  1613. }
  1614. state.PushEvaluationStack (returnObj);
  1615. }
  1616. /// <summary>
  1617. /// General purpose delegate definition for bound EXTERNAL function definitions
  1618. /// from ink. Note that this version isn't necessary if you have a function
  1619. /// with three arguments or less - see the overloads of BindExternalFunction.
  1620. /// </summary>
  1621. public delegate object ExternalFunction(object[] args);
  1622. /// <summary>
  1623. /// Most general form of function binding that returns an object
  1624. /// and takes an array of object parameters.
  1625. /// The only way to bind a function with more than 3 arguments.
  1626. /// </summary>
  1627. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1628. /// <param name="func">The C# function to bind.</param>
  1629. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1630. /// than you might expect beyond the current line just in case it sees
  1631. /// glue that will cause the two lines to become one. In this case it's
  1632. /// possible that a function can appear to be called twice instead of
  1633. /// just once, and earlier than you expect. If it's safe for your
  1634. /// function to be called in this way (since the result and side effect
  1635. /// of the function will not change), then you can pass 'true'.
  1636. /// Usually, you want to pass 'false', especially if you want some action
  1637. /// to be performed in game code when this function is called.</param>
  1638. public void BindExternalFunctionGeneral(string funcName, ExternalFunction func, bool lookaheadSafe = true)
  1639. {
  1640. IfAsyncWeCant ("bind an external function");
  1641. Assert (!_externals.ContainsKey (funcName), "Function '" + funcName + "' has already been bound.");
  1642. _externals [funcName] = new ExternalFunctionDef {
  1643. function = func,
  1644. lookaheadSafe = lookaheadSafe
  1645. };
  1646. }
  1647. object TryCoerce<T>(object value)
  1648. {
  1649. if (value == null)
  1650. return null;
  1651. if (value is T)
  1652. return (T) value;
  1653. if (value is float && typeof(T) == typeof(int)) {
  1654. int intVal = (int)Math.Round ((float)value);
  1655. return intVal;
  1656. }
  1657. if (value is int && typeof(T) == typeof(float)) {
  1658. float floatVal = (float)(int)value;
  1659. return floatVal;
  1660. }
  1661. if (value is int && typeof(T) == typeof(bool)) {
  1662. int intVal = (int)value;
  1663. return intVal == 0 ? false : true;
  1664. }
  1665. if (value is bool && typeof(T) == typeof(int)) {
  1666. bool boolVal = (bool)value;
  1667. return boolVal ? 1 : 0;
  1668. }
  1669. if (typeof(T) == typeof(string)) {
  1670. return value.ToString ();
  1671. }
  1672. Assert (false, "Failed to cast " + value.GetType ().Name + " to " + typeof(T).Name);
  1673. return null;
  1674. }
  1675. // Convenience overloads for standard functions and actions of various arities
  1676. // Is there a better way of doing this?!
  1677. /// <summary>
  1678. /// Bind a C# function to an ink EXTERNAL function declaration.
  1679. /// </summary>
  1680. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1681. /// <param name="func">The C# function to bind.</param>
  1682. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1683. /// than you might expect beyond the current line just in case it sees
  1684. /// glue that will cause the two lines to become one. In this case it's
  1685. /// possible that a function can appear to be called twice instead of
  1686. /// just once, and earlier than you expect. If it's safe for your
  1687. /// function to be called in this way (since the result and side effect
  1688. /// of the function will not change), then you can pass 'true'.
  1689. /// Usually, you want to pass 'false', especially if you want some action
  1690. /// to be performed in game code when this function is called.</param>
  1691. public void BindExternalFunction(string funcName, Func<object> func, bool lookaheadSafe=false)
  1692. {
  1693. Assert(func != null, "Can't bind a null function");
  1694. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1695. Assert(args.Length == 0, "External function expected no arguments");
  1696. return func();
  1697. }, lookaheadSafe);
  1698. }
  1699. /// <summary>
  1700. /// Bind a C# Action to an ink EXTERNAL function declaration.
  1701. /// </summary>
  1702. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1703. /// <param name="act">The C# action to bind.</param>
  1704. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1705. /// than you might expect beyond the current line just in case it sees
  1706. /// glue that will cause the two lines to become one. In this case it's
  1707. /// possible that a function can appear to be called twice instead of
  1708. /// just once, and earlier than you expect. If it's safe for your
  1709. /// function to be called in this way (since the result and side effect
  1710. /// of the function will not change), then you can pass 'true'.
  1711. /// Usually, you want to pass 'false', especially if you want some action
  1712. /// to be performed in game code when this function is called.</param>
  1713. public void BindExternalFunction(string funcName, Action act, bool lookaheadSafe=false)
  1714. {
  1715. Assert(act != null, "Can't bind a null function");
  1716. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1717. Assert(args.Length == 0, "External function expected no arguments");
  1718. act();
  1719. return null;
  1720. }, lookaheadSafe);
  1721. }
  1722. /// <summary>
  1723. /// Bind a C# function to an ink EXTERNAL function declaration.
  1724. /// </summary>
  1725. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1726. /// <param name="func">The C# function to bind.</param>
  1727. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1728. /// than you might expect beyond the current line just in case it sees
  1729. /// glue that will cause the two lines to become one. In this case it's
  1730. /// possible that a function can appear to be called twice instead of
  1731. /// just once, and earlier than you expect. If it's safe for your
  1732. /// function to be called in this way (since the result and side effect
  1733. /// of the function will not change), then you can pass 'true'.
  1734. /// Usually, you want to pass 'false', especially if you want some action
  1735. /// to be performed in game code when this function is called.</param>
  1736. public void BindExternalFunction<T>(string funcName, Func<T, object> func, bool lookaheadSafe=false)
  1737. {
  1738. Assert(func != null, "Can't bind a null function");
  1739. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1740. Assert(args.Length == 1, "External function expected one argument");
  1741. return func( (T)TryCoerce<T>(args[0]) );
  1742. }, lookaheadSafe);
  1743. }
  1744. /// <summary>
  1745. /// Bind a C# action to an ink EXTERNAL function declaration.
  1746. /// </summary>
  1747. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1748. /// <param name="act">The C# action to bind.</param>
  1749. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1750. /// than you might expect beyond the current line just in case it sees
  1751. /// glue that will cause the two lines to become one. In this case it's
  1752. /// possible that a function can appear to be called twice instead of
  1753. /// just once, and earlier than you expect. If it's safe for your
  1754. /// function to be called in this way (since the result and side effect
  1755. /// of the function will not change), then you can pass 'true'.
  1756. /// Usually, you want to pass 'false', especially if you want some action
  1757. /// to be performed in game code when this function is called.</param>
  1758. public void BindExternalFunction<T>(string funcName, Action<T> act, bool lookaheadSafe=false)
  1759. {
  1760. Assert(act != null, "Can't bind a null function");
  1761. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1762. Assert(args.Length == 1, "External function expected one argument");
  1763. act( (T)TryCoerce<T>(args[0]) );
  1764. return null;
  1765. }, lookaheadSafe);
  1766. }
  1767. /// <summary>
  1768. /// Bind a C# function to an ink EXTERNAL function declaration.
  1769. /// </summary>
  1770. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1771. /// <param name="func">The C# function to bind.</param>
  1772. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1773. /// than you might expect beyond the current line just in case it sees
  1774. /// glue that will cause the two lines to become one. In this case it's
  1775. /// possible that a function can appear to be called twice instead of
  1776. /// just once, and earlier than you expect. If it's safe for your
  1777. /// function to be called in this way (since the result and side effect
  1778. /// of the function will not change), then you can pass 'true'.
  1779. /// Usually, you want to pass 'false', especially if you want some action
  1780. /// to be performed in game code when this function is called.</param>
  1781. public void BindExternalFunction<T1, T2>(string funcName, Func<T1, T2, object> func, bool lookaheadSafe = false)
  1782. {
  1783. Assert(func != null, "Can't bind a null function");
  1784. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1785. Assert(args.Length == 2, "External function expected two arguments");
  1786. return func(
  1787. (T1)TryCoerce<T1>(args[0]),
  1788. (T2)TryCoerce<T2>(args[1])
  1789. );
  1790. }, lookaheadSafe);
  1791. }
  1792. /// <summary>
  1793. /// Bind a C# action to an ink EXTERNAL function declaration.
  1794. /// </summary>
  1795. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1796. /// <param name="act">The C# action to bind.</param>
  1797. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1798. /// than you might expect beyond the current line just in case it sees
  1799. /// glue that will cause the two lines to become one. In this case it's
  1800. /// possible that a function can appear to be called twice instead of
  1801. /// just once, and earlier than you expect. If it's safe for your
  1802. /// function to be called in this way (since the result and side effect
  1803. /// of the function will not change), then you can pass 'true'.
  1804. /// Usually, you want to pass 'false', especially if you want some action
  1805. /// to be performed in game code when this function is called.</param>
  1806. public void BindExternalFunction<T1, T2>(string funcName, Action<T1, T2> act, bool lookaheadSafe=false)
  1807. {
  1808. Assert(act != null, "Can't bind a null function");
  1809. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1810. Assert(args.Length == 2, "External function expected two arguments");
  1811. act(
  1812. (T1)TryCoerce<T1>(args[0]),
  1813. (T2)TryCoerce<T2>(args[1])
  1814. );
  1815. return null;
  1816. }, lookaheadSafe);
  1817. }
  1818. /// <summary>
  1819. /// Bind a C# function to an ink EXTERNAL function declaration.
  1820. /// </summary>
  1821. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1822. /// <param name="func">The C# function to bind.</param>
  1823. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1824. /// than you might expect beyond the current line just in case it sees
  1825. /// glue that will cause the two lines to become one. In this case it's
  1826. /// possible that a function can appear to be called twice instead of
  1827. /// just once, and earlier than you expect. If it's safe for your
  1828. /// function to be called in this way (since the result and side effect
  1829. /// of the function will not change), then you can pass 'true'.
  1830. /// Usually, you want to pass 'false', especially if you want some action
  1831. /// to be performed in game code when this function is called.</param>
  1832. public void BindExternalFunction<T1, T2, T3>(string funcName, Func<T1, T2, T3, object> func, bool lookaheadSafe=false)
  1833. {
  1834. Assert(func != null, "Can't bind a null function");
  1835. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1836. Assert(args.Length == 3, "External function expected three arguments");
  1837. return func(
  1838. (T1)TryCoerce<T1>(args[0]),
  1839. (T2)TryCoerce<T2>(args[1]),
  1840. (T3)TryCoerce<T3>(args[2])
  1841. );
  1842. }, lookaheadSafe);
  1843. }
  1844. /// <summary>
  1845. /// Bind a C# action to an ink EXTERNAL function declaration.
  1846. /// </summary>
  1847. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1848. /// <param name="act">The C# action to bind.</param>
  1849. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1850. /// than you might expect beyond the current line just in case it sees
  1851. /// glue that will cause the two lines to become one. In this case it's
  1852. /// possible that a function can appear to be called twice instead of
  1853. /// just once, and earlier than you expect. If it's safe for your
  1854. /// function to be called in this way (since the result and side effect
  1855. /// of the function will not change), then you can pass 'true'.
  1856. /// Usually, you want to pass 'false', especially if you want some action
  1857. /// to be performed in game code when this function is called.</param>
  1858. public void BindExternalFunction<T1, T2, T3>(string funcName, Action<T1, T2, T3> act, bool lookaheadSafe=false)
  1859. {
  1860. Assert(act != null, "Can't bind a null function");
  1861. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1862. Assert(args.Length == 3, "External function expected three arguments");
  1863. act(
  1864. (T1)TryCoerce<T1>(args[0]),
  1865. (T2)TryCoerce<T2>(args[1]),
  1866. (T3)TryCoerce<T3>(args[2])
  1867. );
  1868. return null;
  1869. }, lookaheadSafe);
  1870. }
  1871. /// <summary>
  1872. /// Bind a C# function to an ink EXTERNAL function declaration.
  1873. /// </summary>
  1874. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1875. /// <param name="func">The C# function to bind.</param>
  1876. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1877. /// than you might expect beyond the current line just in case it sees
  1878. /// glue that will cause the two lines to become one. In this case it's
  1879. /// possible that a function can appear to be called twice instead of
  1880. /// just once, and earlier than you expect. If it's safe for your
  1881. /// function to be called in this way (since the result and side effect
  1882. /// of the function will not change), then you can pass 'true'.
  1883. /// Usually, you want to pass 'false', especially if you want some action
  1884. /// to be performed in game code when this function is called.</param>
  1885. public void BindExternalFunction<T1, T2, T3, T4>(string funcName, Func<T1, T2, T3, T4, object> func, bool lookaheadSafe=false)
  1886. {
  1887. Assert(func != null, "Can't bind a null function");
  1888. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1889. Assert(args.Length == 4, "External function expected four arguments");
  1890. return func(
  1891. (T1)TryCoerce<T1>(args[0]),
  1892. (T2)TryCoerce<T2>(args[1]),
  1893. (T3)TryCoerce<T3>(args[2]),
  1894. (T4)TryCoerce<T4>(args[3])
  1895. );
  1896. }, lookaheadSafe);
  1897. }
  1898. /// <summary>
  1899. /// Bind a C# action to an ink EXTERNAL function declaration.
  1900. /// </summary>
  1901. /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
  1902. /// <param name="act">The C# action to bind.</param>
  1903. /// <param name="lookaheadSafe">The ink engine often evaluates further
  1904. /// than you might expect beyond the current line just in case it sees
  1905. /// glue that will cause the two lines to become one. In this case it's
  1906. /// possible that a function can appear to be called twice instead of
  1907. /// just once, and earlier than you expect. If it's safe for your
  1908. /// function to be called in this way (since the result and side effect
  1909. /// of the function will not change), then you can pass 'true'.
  1910. /// Usually, you want to pass 'false', especially if you want some action
  1911. /// to be performed in game code when this function is called.</param>
  1912. public void BindExternalFunction<T1, T2, T3, T4>(string funcName, Action<T1, T2, T3, T4> act, bool lookaheadSafe=false)
  1913. {
  1914. Assert(act != null, "Can't bind a null function");
  1915. BindExternalFunctionGeneral (funcName, (object[] args) => {
  1916. Assert(args.Length == 4, "External function expected four arguments");
  1917. act(
  1918. (T1)TryCoerce<T1>(args[0]),
  1919. (T2)TryCoerce<T2>(args[1]),
  1920. (T3)TryCoerce<T3>(args[2]),
  1921. (T4)TryCoerce<T4>(args[3])
  1922. );
  1923. return null;
  1924. }, lookaheadSafe);
  1925. }
  1926. /// <summary>
  1927. /// Remove a binding for a named EXTERNAL ink function.
  1928. /// </summary>
  1929. public void UnbindExternalFunction(string funcName)
  1930. {
  1931. IfAsyncWeCant ("unbind an external a function");
  1932. Assert (_externals.ContainsKey (funcName), "Function '" + funcName + "' has not been bound.");
  1933. _externals.Remove (funcName);
  1934. }
  1935. /// <summary>
  1936. /// Check that all EXTERNAL ink functions have a valid bound C# function.
  1937. /// Note that this is automatically called on the first call to Continue().
  1938. /// </summary>
  1939. public void ValidateExternalBindings()
  1940. {
  1941. var missingExternals = new HashSet<string>();
  1942. ValidateExternalBindings (_mainContentContainer, missingExternals);
  1943. _hasValidatedExternals = true;
  1944. // No problem! Validation complete
  1945. if( missingExternals.Count == 0 ) {
  1946. _hasValidatedExternals = true;
  1947. }
  1948. // Error for all missing externals
  1949. else {
  1950. var message = string.Format("ERROR: Missing function binding for external{0}: '{1}' {2}",
  1951. missingExternals.Count > 1 ? "s" : string.Empty,
  1952. string.Join("', '", missingExternals.ToArray()),
  1953. allowExternalFunctionFallbacks ? ", and no fallback ink function found." : " (ink fallbacks disabled)"
  1954. );
  1955. Error(message);
  1956. }
  1957. }
  1958. void ValidateExternalBindings(Container c, HashSet<string> missingExternals)
  1959. {
  1960. foreach (var innerContent in c.content) {
  1961. var container = innerContent as Container;
  1962. if( container == null || !container.hasValidName )
  1963. ValidateExternalBindings (innerContent, missingExternals);
  1964. }
  1965. foreach (var innerKeyValue in c.namedContent) {
  1966. ValidateExternalBindings (innerKeyValue.Value as Runtime.Object, missingExternals);
  1967. }
  1968. }
  1969. void ValidateExternalBindings(Runtime.Object o, HashSet<string> missingExternals)
  1970. {
  1971. var container = o as Container;
  1972. if (container) {
  1973. ValidateExternalBindings (container, missingExternals);
  1974. return;
  1975. }
  1976. var divert = o as Divert;
  1977. if (divert && divert.isExternal) {
  1978. var name = divert.targetPathString;
  1979. if (!_externals.ContainsKey (name)) {
  1980. if( allowExternalFunctionFallbacks ) {
  1981. bool fallbackFound = mainContentContainer.namedContent.ContainsKey(name);
  1982. if( !fallbackFound ) {
  1983. missingExternals.Add(name);
  1984. }
  1985. } else {
  1986. missingExternals.Add(name);
  1987. }
  1988. }
  1989. }
  1990. }
  1991. /// <summary>
  1992. /// Delegate definition for variable observation - see ObserveVariable.
  1993. /// </summary>
  1994. public delegate void VariableObserver(string variableName, object newValue);
  1995. /// <summary>
  1996. /// When the named global variable changes it's value, the observer will be
  1997. /// called to notify it of the change. Note that if the value changes multiple
  1998. /// times within the ink, the observer will only be called once, at the end
  1999. /// of the ink's evaluation. If, during the evaluation, it changes and then
  2000. /// changes back again to its original value, it will still be called.
  2001. /// Note that the observer will also be fired if the value of the variable
  2002. /// is changed externally to the ink, by directly setting a value in
  2003. /// story.variablesState.
  2004. /// </summary>
  2005. /// <param name="variableName">The name of the global variable to observe.</param>
  2006. /// <param name="observer">A delegate function to call when the variable changes.</param>
  2007. public void ObserveVariable(string variableName, VariableObserver observer)
  2008. {
  2009. IfAsyncWeCant ("observe a new variable");
  2010. if (_variableObservers == null)
  2011. _variableObservers = new Dictionary<string, VariableObserver> ();
  2012. if( !state.variablesState.GlobalVariableExistsWithName(variableName) )
  2013. throw new Exception("Cannot observe variable '"+variableName+"' because it wasn't declared in the ink story.");
  2014. if (_variableObservers.ContainsKey (variableName)) {
  2015. _variableObservers[variableName] += observer;
  2016. } else {
  2017. _variableObservers[variableName] = observer;
  2018. }
  2019. }
  2020. /// <summary>
  2021. /// Convenience function to allow multiple variables to be observed with the same
  2022. /// observer delegate function. See the singular ObserveVariable for details.
  2023. /// The observer will get one call for every variable that has changed.
  2024. /// </summary>
  2025. /// <param name="variableNames">The set of variables to observe.</param>
  2026. /// <param name="observer">The delegate function to call when any of the named variables change.</param>
  2027. public void ObserveVariables(IList<string> variableNames, VariableObserver observer)
  2028. {
  2029. foreach (var varName in variableNames) {
  2030. ObserveVariable (varName, observer);
  2031. }
  2032. }
  2033. /// <summary>
  2034. /// Removes the variable observer, to stop getting variable change notifications.
  2035. /// If you pass a specific variable name, it will stop observing that particular one. If you
  2036. /// pass null (or leave it blank, since it's optional), then the observer will be removed
  2037. /// from all variables that it's subscribed to. If you pass in a specific variable name and
  2038. /// null for the the observer, all observers for that variable will be removed.
  2039. /// </summary>
  2040. /// <param name="observer">(Optional) The observer to stop observing.</param>
  2041. /// <param name="specificVariableName">(Optional) Specific variable name to stop observing.</param>
  2042. public void RemoveVariableObserver(VariableObserver observer = null, string specificVariableName = null)
  2043. {
  2044. IfAsyncWeCant ("remove a variable observer");
  2045. if (_variableObservers == null)
  2046. return;
  2047. // Remove observer for this specific variable
  2048. if (specificVariableName != null) {
  2049. if (_variableObservers.ContainsKey (specificVariableName)) {
  2050. if( observer != null) {
  2051. _variableObservers [specificVariableName] -= observer;
  2052. if (_variableObservers[specificVariableName] == null) {
  2053. _variableObservers.Remove(specificVariableName);
  2054. }
  2055. }
  2056. else {
  2057. _variableObservers.Remove(specificVariableName);
  2058. }
  2059. }
  2060. }
  2061. // Remove observer for all variables
  2062. else if( observer != null) {
  2063. var keys = new List<string>(_variableObservers.Keys);
  2064. foreach (var varName in keys) {
  2065. _variableObservers[varName] -= observer;
  2066. if (_variableObservers[varName] == null) {
  2067. _variableObservers.Remove(varName);
  2068. }
  2069. }
  2070. }
  2071. }
  2072. void VariableStateDidChangeEvent(string variableName, Runtime.Object newValueObj)
  2073. {
  2074. if (_variableObservers == null)
  2075. return;
  2076. VariableObserver observers = null;
  2077. if (_variableObservers.TryGetValue (variableName, out observers)) {
  2078. if (!(newValueObj is Value)) {
  2079. throw new System.Exception ("Tried to get the value of a variable that isn't a standard type");
  2080. }
  2081. var val = newValueObj as Value;
  2082. observers (variableName, val.valueObject);
  2083. }
  2084. }
  2085. /// <summary>
  2086. /// Get any global tags associated with the story. These are defined as
  2087. /// hash tags defined at the very top of the story.
  2088. /// </summary>
  2089. public List<string> globalTags {
  2090. get {
  2091. return TagsAtStartOfFlowContainerWithPathString ("");
  2092. }
  2093. }
  2094. /// <summary>
  2095. /// Gets any tags associated with a particular knot or knot.stitch.
  2096. /// These are defined as hash tags defined at the very top of a
  2097. /// knot or stitch.
  2098. /// </summary>
  2099. /// <param name="path">The path of the knot or stitch, in the form "knot" or "knot.stitch".</param>
  2100. public List<string> TagsForContentAtPath (string path)
  2101. {
  2102. return TagsAtStartOfFlowContainerWithPathString (path);
  2103. }
  2104. List<string> TagsAtStartOfFlowContainerWithPathString (string pathString)
  2105. {
  2106. var path = new Runtime.Path (pathString);
  2107. // Expected to be global story, knot or stitch
  2108. var flowContainer = ContentAtPath (path).container;
  2109. while(true) {
  2110. var firstContent = flowContainer.content [0];
  2111. if (firstContent is Container)
  2112. flowContainer = (Container)firstContent;
  2113. else break;
  2114. }
  2115. // Any initial tag objects count as the "main tags" associated with that story/knot/stitch
  2116. bool inTag = false;
  2117. List<string> tags = null;
  2118. foreach (var c in flowContainer.content) {
  2119. var command = c as Runtime.ControlCommand;
  2120. if( command != null ) {
  2121. if( command.commandType == Runtime.ControlCommand.CommandType.BeginTag ) {
  2122. inTag = true;
  2123. } else if( command.commandType == Runtime.ControlCommand.CommandType.EndTag ) {
  2124. inTag = false;
  2125. }
  2126. }
  2127. else if( inTag ) {
  2128. var str = c as Runtime.StringValue;
  2129. if( str != null ) {
  2130. if( tags == null ) tags = new List<string>();
  2131. tags.Add(str.value);
  2132. } else {
  2133. Error("Tag contained non-text content. Only plain text is allowed when using globalTags or TagsAtContentPath. If you want to evaluate dynamic content, you need to use story.Continue().");
  2134. }
  2135. }
  2136. // Any other content - we're done
  2137. // We only recognise initial text-only tags
  2138. else {
  2139. break;
  2140. }
  2141. }
  2142. return tags;
  2143. }
  2144. /// <summary>
  2145. /// Useful when debugging a (very short) story, to visualise the state of the
  2146. /// story. Add this call as a watch and open the extended text. A left-arrow mark
  2147. /// will denote the current point of the story.
  2148. /// It's only recommended that this is used on very short debug stories, since
  2149. /// it can end up generate a large quantity of text otherwise.
  2150. /// </summary>
  2151. public virtual string BuildStringOfHierarchy()
  2152. {
  2153. var sb = new StringBuilder ();
  2154. mainContentContainer.BuildStringOfHierarchy (sb, 0, state.currentPointer.Resolve());
  2155. return sb.ToString ();
  2156. }
  2157. string BuildStringOfContainer (Container container)
  2158. {
  2159. var sb = new StringBuilder ();
  2160. container.BuildStringOfHierarchy (sb, 0, state.currentPointer.Resolve());
  2161. return sb.ToString();
  2162. }
  2163. private void NextContent()
  2164. {
  2165. // Setting previousContentObject is critical for VisitChangedContainersDueToDivert
  2166. state.previousPointer = state.currentPointer;
  2167. // Divert step?
  2168. if (!state.divertedPointer.isNull) {
  2169. state.currentPointer = state.divertedPointer;
  2170. state.divertedPointer = Pointer.Null;
  2171. // Internally uses state.previousContentObject and state.currentContentObject
  2172. VisitChangedContainersDueToDivert ();
  2173. // Diverted location has valid content?
  2174. if (!state.currentPointer.isNull) {
  2175. return;
  2176. }
  2177. // Otherwise, if diverted location doesn't have valid content,
  2178. // drop down and attempt to increment.
  2179. // This can happen if the diverted path is intentionally jumping
  2180. // to the end of a container - e.g. a Conditional that's re-joining
  2181. }
  2182. bool successfulPointerIncrement = IncrementContentPointer ();
  2183. // Ran out of content? Try to auto-exit from a function,
  2184. // or finish evaluating the content of a thread
  2185. if (!successfulPointerIncrement) {
  2186. bool didPop = false;
  2187. if (state.callStack.CanPop (PushPopType.Function)) {
  2188. // Pop from the call stack
  2189. state.PopCallstack (PushPopType.Function);
  2190. // This pop was due to dropping off the end of a function that didn't return anything,
  2191. // so in this case, we make sure that the evaluator has something to chomp on if it needs it
  2192. if (state.inExpressionEvaluation) {
  2193. state.PushEvaluationStack (new Runtime.Void ());
  2194. }
  2195. didPop = true;
  2196. } else if (state.callStack.canPopThread) {
  2197. state.callStack.PopThread ();
  2198. didPop = true;
  2199. } else {
  2200. state.TryExitFunctionEvaluationFromGame ();
  2201. }
  2202. // Step past the point where we last called out
  2203. if (didPop && !state.currentPointer.isNull) {
  2204. NextContent ();
  2205. }
  2206. }
  2207. }
  2208. bool IncrementContentPointer()
  2209. {
  2210. bool successfulIncrement = true;
  2211. var pointer = state.callStack.currentElement.currentPointer;
  2212. pointer.index++;
  2213. // Each time we step off the end, we fall out to the next container, all the
  2214. // while we're in indexed rather than named content
  2215. while (pointer.index >= pointer.container.content.Count) {
  2216. successfulIncrement = false;
  2217. Container nextAncestor = pointer.container.parent as Container;
  2218. if (!nextAncestor) {
  2219. break;
  2220. }
  2221. var indexInAncestor = nextAncestor.content.IndexOf (pointer.container);
  2222. if (indexInAncestor == -1) {
  2223. break;
  2224. }
  2225. pointer = new Pointer (nextAncestor, indexInAncestor);
  2226. // Increment to next content in outer container
  2227. pointer.index++;
  2228. successfulIncrement = true;
  2229. }
  2230. if (!successfulIncrement) pointer = Pointer.Null;
  2231. state.callStack.currentElement.currentPointer = pointer;
  2232. return successfulIncrement;
  2233. }
  2234. bool TryFollowDefaultInvisibleChoice()
  2235. {
  2236. var allChoices = _state.currentChoices;
  2237. // Is a default invisible choice the ONLY choice?
  2238. var invisibleChoices = allChoices.Where (c => c.isInvisibleDefault).ToList();
  2239. if (invisibleChoices.Count == 0 || allChoices.Count > invisibleChoices.Count)
  2240. return false;
  2241. var choice = invisibleChoices [0];
  2242. // Invisible choice may have been generated on a different thread,
  2243. // in which case we need to restore it before we continue
  2244. state.callStack.currentThread = choice.threadAtGeneration;
  2245. // If there's a chance that this state will be rolled back to before
  2246. // the invisible choice then make sure that the choice thread is
  2247. // left intact, and it isn't re-entered in an old state.
  2248. if ( _stateSnapshotAtLastNewline != null )
  2249. state.callStack.currentThread = state.callStack.ForkThread();
  2250. ChoosePath (choice.targetPath, incrementingTurnIndex: false);
  2251. return true;
  2252. }
  2253. // Note that this is O(n), since it re-evaluates the shuffle indices
  2254. // from a consistent seed each time.
  2255. // TODO: Is this the best algorithm it can be?
  2256. int NextSequenceShuffleIndex()
  2257. {
  2258. var numElementsIntVal = state.PopEvaluationStack () as IntValue;
  2259. if (numElementsIntVal == null) {
  2260. Error ("expected number of elements in sequence for shuffle index");
  2261. return 0;
  2262. }
  2263. var seqContainer = state.currentPointer.container;
  2264. int numElements = numElementsIntVal.value;
  2265. var seqCountVal = state.PopEvaluationStack () as IntValue;
  2266. var seqCount = seqCountVal.value;
  2267. var loopIndex = seqCount / numElements;
  2268. var iterationIndex = seqCount % numElements;
  2269. // Generate the same shuffle based on:
  2270. // - The hash of this container, to make sure it's consistent
  2271. // each time the runtime returns to the sequence
  2272. // - How many times the runtime has looped around this full shuffle
  2273. var seqPathStr = seqContainer.path.ToString();
  2274. int sequenceHash = 0;
  2275. foreach (char c in seqPathStr) {
  2276. sequenceHash += c;
  2277. }
  2278. var randomSeed = sequenceHash + loopIndex + state.storySeed;
  2279. var random = new Random (randomSeed);
  2280. var unpickedIndices = new List<int> ();
  2281. for (int i = 0; i < numElements; ++i) {
  2282. unpickedIndices.Add (i);
  2283. }
  2284. for (int i = 0; i <= iterationIndex; ++i) {
  2285. var chosen = random.Next () % unpickedIndices.Count;
  2286. var chosenIndex = unpickedIndices [chosen];
  2287. unpickedIndices.RemoveAt (chosen);
  2288. if (i == iterationIndex) {
  2289. return chosenIndex;
  2290. }
  2291. }
  2292. throw new System.Exception ("Should never reach here");
  2293. }
  2294. // Throw an exception that gets caught and causes AddError to be called,
  2295. // then exits the flow.
  2296. public void Error(string message, bool useEndLineNumber = false)
  2297. {
  2298. var e = new StoryException (message);
  2299. e.useEndLineNumber = useEndLineNumber;
  2300. throw e;
  2301. }
  2302. public void Warning (string message)
  2303. {
  2304. AddError (message, isWarning:true);
  2305. }
  2306. void AddError (string message, bool isWarning = false, bool useEndLineNumber = false)
  2307. {
  2308. var dm = currentDebugMetadata;
  2309. var errorTypeStr = isWarning ? "WARNING" : "ERROR";
  2310. if (dm != null) {
  2311. int lineNum = useEndLineNumber ? dm.endLineNumber : dm.startLineNumber;
  2312. message = string.Format ("RUNTIME {0}: '{1}' line {2}: {3}", errorTypeStr, dm.fileName, lineNum, message);
  2313. } else if( !state.currentPointer.isNull ) {
  2314. message = string.Format ("RUNTIME {0}: ({1}): {2}", errorTypeStr, state.currentPointer.path, message);
  2315. } else {
  2316. message = "RUNTIME "+errorTypeStr+": " + message;
  2317. }
  2318. state.AddError (message, isWarning);
  2319. // In a broken state don't need to know about any other errors.
  2320. if( !isWarning )
  2321. state.ForceEnd ();
  2322. }
  2323. void Assert(bool condition, string message = null, params object[] formatParams)
  2324. {
  2325. if (condition == false) {
  2326. if (message == null) {
  2327. message = "Story assert";
  2328. }
  2329. if (formatParams != null && formatParams.Count() > 0) {
  2330. message = string.Format (message, formatParams);
  2331. }
  2332. throw new System.Exception (message + " " + currentDebugMetadata);
  2333. }
  2334. }
  2335. DebugMetadata currentDebugMetadata
  2336. {
  2337. get {
  2338. DebugMetadata dm;
  2339. // Try to get from the current path first
  2340. var pointer = state.currentPointer;
  2341. if (!pointer.isNull) {
  2342. dm = pointer.Resolve().debugMetadata;
  2343. if (dm != null) {
  2344. return dm;
  2345. }
  2346. }
  2347. // Move up callstack if possible
  2348. for (int i = state.callStack.elements.Count - 1; i >= 0; --i) {
  2349. pointer = state.callStack.elements [i].currentPointer;
  2350. if (!pointer.isNull && pointer.Resolve() != null) {
  2351. dm = pointer.Resolve().debugMetadata;
  2352. if (dm != null) {
  2353. return dm;
  2354. }
  2355. }
  2356. }
  2357. // Current/previous path may not be valid if we've just had an error,
  2358. // or if we've simply run out of content.
  2359. // As a last resort, try to grab something from the output stream
  2360. for (int i = state.outputStream.Count - 1; i >= 0; --i) {
  2361. var outputObj = state.outputStream [i];
  2362. dm = outputObj.debugMetadata;
  2363. if (dm != null) {
  2364. return dm;
  2365. }
  2366. }
  2367. return null;
  2368. }
  2369. }
  2370. int currentLineNumber
  2371. {
  2372. get {
  2373. var dm = currentDebugMetadata;
  2374. if (dm != null) {
  2375. return dm.startLineNumber;
  2376. }
  2377. return 0;
  2378. }
  2379. }
  2380. public Container mainContentContainer {
  2381. get {
  2382. if (_temporaryEvaluationContainer) {
  2383. return _temporaryEvaluationContainer;
  2384. } else {
  2385. return _mainContentContainer;
  2386. }
  2387. }
  2388. }
  2389. Container _mainContentContainer;
  2390. ListDefinitionsOrigin _listDefinitions;
  2391. struct ExternalFunctionDef {
  2392. public ExternalFunction function;
  2393. public bool lookaheadSafe;
  2394. }
  2395. Dictionary<string, ExternalFunctionDef> _externals;
  2396. Dictionary<string, VariableObserver> _variableObservers;
  2397. bool _hasValidatedExternals;
  2398. Container _temporaryEvaluationContainer;
  2399. StoryState _state;
  2400. bool _asyncContinueActive;
  2401. StoryState _stateSnapshotAtLastNewline = null;
  2402. bool _sawLookaheadUnsafeFunctionAfterNewline = false;
  2403. int _recursiveContinueCount = 0;
  2404. bool _asyncSaving;
  2405. Profiler _profiler;
  2406. }
  2407. }