VirtualMachine.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. using System;
  2. using System.Collections.Generic;
  3. namespace Yarn
  4. {
  5. internal class VirtualMachine
  6. {
  7. internal class State {
  8. /// The name of the node that we're currently in
  9. public string currentNodeName;
  10. /// The instruction number in the current node
  11. public int programCounter = 0;
  12. /// List of options, where each option = <string id, destination node>
  13. public List<KeyValuePair<string,string>> currentOptions = new List<KeyValuePair<string, string>>();
  14. /// The value stack
  15. private Stack<Value> stack = new Stack<Value>();
  16. /// Methods for working with the stack
  17. public void PushValue(object o) {
  18. if( o is Value ) {
  19. stack.Push(o as Value);
  20. } else {
  21. stack.Push (new Value(o));
  22. }
  23. }
  24. /// Pop a value from the stack
  25. public Value PopValue() {
  26. return stack.Pop ();
  27. }
  28. /// Peek at a value from the stack
  29. public Value PeekValue() {
  30. return stack.Peek ();
  31. }
  32. /// Clear the stack
  33. public void ClearStack() {
  34. stack.Clear ();
  35. }
  36. }
  37. internal static class SpecialVariables {
  38. public const string ShuffleOptions = "$Yarn.ShuffleOptions";
  39. }
  40. internal VirtualMachine (Dialogue d, Program p)
  41. {
  42. program = p;
  43. dialogue = d;
  44. state = new State ();
  45. }
  46. /// Reset the state of the VM
  47. void ResetState() {
  48. state = new State();
  49. }
  50. public delegate void LineHandler(Dialogue.LineResult line);
  51. public delegate void OptionsHandler(Dialogue.OptionSetResult options);
  52. public delegate void CommandHandler(Dialogue.CommandResult command);
  53. public delegate void NodeCompleteHandler(Dialogue.NodeCompleteResult complete);
  54. public LineHandler lineHandler;
  55. public OptionsHandler optionsHandler;
  56. public CommandHandler commandHandler;
  57. public NodeCompleteHandler nodeCompleteHandler;
  58. private Dialogue dialogue;
  59. private Program program;
  60. private State state = new State();
  61. private Random random = new Random();
  62. public string currentNodeName {
  63. get {
  64. return state.currentNodeName;
  65. }
  66. }
  67. public enum ExecutionState {
  68. /** Stopped */
  69. Stopped,
  70. /** Waiting on option selection */
  71. WaitingOnOptionSelection,
  72. /** Running */
  73. Running
  74. }
  75. private ExecutionState _executionState;
  76. public ExecutionState executionState {
  77. get {
  78. return _executionState;
  79. }
  80. private set {
  81. _executionState = value;
  82. if (_executionState == ExecutionState.Stopped) {
  83. ResetState ();
  84. }
  85. }
  86. }
  87. Node currentNode;
  88. public bool SetNode(string nodeName) {
  89. if (program.nodes.ContainsKey(nodeName) == false) {
  90. var error = "No node named " + nodeName;
  91. dialogue.LogErrorMessage(error);
  92. executionState = ExecutionState.Stopped;
  93. return false;
  94. }
  95. dialogue.LogDebugMessage ("Running node " + nodeName);
  96. // Clear the special variables
  97. dialogue.continuity.SetValue(SpecialVariables.ShuffleOptions, new Value(false));
  98. currentNode = program.nodes [nodeName];
  99. ResetState ();
  100. state.currentNodeName = nodeName;
  101. return true;
  102. }
  103. public void Stop() {
  104. executionState = ExecutionState.Stopped;
  105. }
  106. /// Executes the next instruction in the current node.
  107. internal void RunNext() {
  108. if (executionState == ExecutionState.WaitingOnOptionSelection) {
  109. dialogue.LogErrorMessage ("Cannot continue running dialogue. Still waiting on option selection.");
  110. executionState = ExecutionState.Stopped;
  111. return;
  112. }
  113. if (executionState == ExecutionState.Stopped)
  114. executionState = ExecutionState.Running;
  115. Instruction currentInstruction = currentNode.instructions [state.programCounter];
  116. RunInstruction (currentInstruction);
  117. state.programCounter++;
  118. if (state.programCounter >= currentNode.instructions.Count) {
  119. executionState = ExecutionState.Stopped;
  120. nodeCompleteHandler(new Dialogue.NodeCompleteResult(null));
  121. dialogue.LogDebugMessage ("Run complete.");
  122. }
  123. }
  124. /// Looks up the instruction number for a named label in the current node.
  125. internal int FindInstructionPointForLabel(string labelName) {
  126. if (currentNode.labels.ContainsKey(labelName) == false) {
  127. // Couldn't find the node..
  128. throw new IndexOutOfRangeException ("Unknown label " +
  129. labelName + " in node " + state.currentNodeName);
  130. }
  131. return currentNode.labels [labelName];
  132. }
  133. internal void RunInstruction(Instruction i) {
  134. switch (i.operation) {
  135. case ByteCode.Label:
  136. /// - Label
  137. /** No-op, used as a destination for JumpTo and Jump.
  138. */
  139. break;
  140. case ByteCode.JumpTo:
  141. /// - JumpTo
  142. /** Jumps to a named label
  143. */
  144. state.programCounter = FindInstructionPointForLabel ((string)i.operandA);
  145. break;
  146. case ByteCode.RunLine:
  147. /// - RunLine
  148. /** Looks up a string from the string table and
  149. * passes it to the client as a line
  150. */
  151. var lineText = program.GetString ((string)i.operandA);
  152. if (lineText == null) {
  153. dialogue.LogErrorMessage("No loaded string table includes line " + i.operandA);
  154. break;
  155. }
  156. lineHandler (new Dialogue.LineResult (lineText));
  157. break;
  158. case ByteCode.RunCommand:
  159. /// - RunCommand
  160. /** Passes a string to the client as a custom command
  161. */
  162. commandHandler (
  163. new Dialogue.CommandResult ((string)i.operandA)
  164. );
  165. break;
  166. case ByteCode.PushString:
  167. /// - PushString
  168. /** Pushes a string value onto the stack. The operand is an index into
  169. * the string table, so that's looked up first.
  170. */
  171. state.PushValue (program.GetString ((string)i.operandA));
  172. break;
  173. case ByteCode.PushNumber:
  174. /// - PushNumber
  175. /** Pushes a number onto the stack.
  176. */
  177. state.PushValue (Convert.ToSingle(i.operandA));
  178. break;
  179. case ByteCode.PushBool:
  180. /// - PushBool
  181. /** Pushes a boolean value onto the stack.
  182. */
  183. state.PushValue (Convert.ToBoolean(i.operandA));
  184. break;
  185. case ByteCode.PushNull:
  186. /// - PushNull
  187. /** Pushes a null value onto the stack.
  188. */
  189. state.PushValue (Value.NULL);
  190. break;
  191. case ByteCode.JumpIfFalse:
  192. /// - JumpIfFalse
  193. /** Jumps to a named label if the value on the top of the stack
  194. * evaluates to the boolean value 'false'.
  195. */
  196. if (state.PeekValue ().AsBool == false) {
  197. state.programCounter = FindInstructionPointForLabel ((string)i.operandA);
  198. }
  199. break;
  200. case ByteCode.Jump:
  201. /// - Jump
  202. /** Jumps to a label whose name is on the stack.
  203. */
  204. var jumpDestination = state.PeekValue ().AsString;
  205. state.programCounter = FindInstructionPointForLabel (jumpDestination);
  206. break;
  207. case ByteCode.Pop:
  208. /// - Pop
  209. /** Pops a value from the stack.
  210. */
  211. state.PopValue ();
  212. break;
  213. case ByteCode.CallFunc:
  214. /// - CallFunc
  215. /** Call a function, whose parameters are expected to
  216. * be on the stack. Pushes the function's return value,
  217. * if it returns one.
  218. */
  219. var functionName = (string)i.operandA;
  220. var function = dialogue.library.GetFunction (functionName);
  221. {
  222. var paramCount = function.paramCount;
  223. // If this function takes "-1" parameters, it is variadic.
  224. // Expect the compiler to have placed the number of parameters
  225. // actually passed at the top of the stack.
  226. if (paramCount == -1) {
  227. paramCount = (int)state.PopValue ().AsNumber;
  228. }
  229. Value result;
  230. if (paramCount == 0) {
  231. result = function.Invoke();
  232. } else {
  233. // Get the parameters, which were pushed in reverse
  234. Value[] parameters = new Value[paramCount];
  235. for (int param = paramCount - 1; param >= 0; param--) {
  236. parameters [param] = state.PopValue ();
  237. }
  238. // Invoke the function
  239. result = function.InvokeWithArray (parameters);
  240. }
  241. // If the function returns a value, push it
  242. if (function.returnsValue) {
  243. state.PushValue (result);
  244. }
  245. }
  246. break;
  247. case ByteCode.PushVariable:
  248. /// - PushVariable
  249. /** Get the contents of a variable, push that onto the stack.
  250. */
  251. var variableName = (string)i.operandA;
  252. var loadedValue = dialogue.continuity.GetValue (variableName);
  253. state.PushValue (loadedValue);
  254. break;
  255. case ByteCode.StoreVariable:
  256. /// - StoreVariable
  257. /** Store the top value on the stack in a variable.
  258. */
  259. var topValue = state.PeekValue ();
  260. var destinationVariableName = (string)i.operandA;
  261. dialogue.continuity.SetValue (destinationVariableName, topValue);
  262. break;
  263. case ByteCode.Stop:
  264. /// - Stop
  265. /** Immediately stop execution, and report that fact.
  266. */
  267. nodeCompleteHandler (new Dialogue.NodeCompleteResult (null));
  268. executionState = ExecutionState.Stopped;
  269. break;
  270. case ByteCode.RunNode:
  271. /// - RunNode
  272. /** Run a node
  273. */
  274. string nodeName;
  275. if (string.IsNullOrEmpty((string) i.operandA)) {
  276. // Get a string from the stack, and jump to a node with that name.
  277. nodeName = state.PeekValue ().AsString;
  278. } else {
  279. // jump straight to the node
  280. nodeName = (string)i.operandA;
  281. }
  282. nodeCompleteHandler (new Dialogue.NodeCompleteResult (nodeName));
  283. SetNode (nodeName);
  284. break;
  285. case ByteCode.AddOption:
  286. /// - AddOption
  287. /** Add an option to the current state.
  288. */
  289. state.currentOptions.Add (new KeyValuePair<string, string> ((string)i.operandA, (string)i.operandB));
  290. break;
  291. case ByteCode.ShowOptions:
  292. /// - ShowOptions
  293. /** If we have no options to show, immediately stop.
  294. */
  295. if (state.currentOptions.Count == 0) {
  296. nodeCompleteHandler(new Dialogue.NodeCompleteResult(null));
  297. executionState = ExecutionState.Stopped;
  298. break;
  299. }
  300. /** If we have a single option, and it has no label, select it immediately and continue execution
  301. */
  302. if (state.currentOptions.Count == 1 && state.currentOptions[0].Key == null) {
  303. var destinationNode = state.currentOptions[0].Value;
  304. state.PushValue(destinationNode);
  305. state.currentOptions.Clear();
  306. break;
  307. }
  308. if (dialogue.continuity.GetValue(SpecialVariables.ShuffleOptions).AsBool) {
  309. // Shuffle the dialog options if needed
  310. var n = state.currentOptions.Count;
  311. for (int opt1 = 0; opt1 < n; opt1++) {
  312. int opt2 = opt1 + (int)(random.NextDouble () * (n - opt1)); // r.Next(0, state.currentOptions.Count-1);
  313. var temp = state.currentOptions [opt2];
  314. state.currentOptions [opt2] = state.currentOptions [opt1];
  315. state.currentOptions [opt1] = temp;
  316. }
  317. }
  318. // Otherwise, present the list of options to the user and let them pick
  319. var optionStrings = new List<string> ();
  320. foreach (var option in state.currentOptions) {
  321. optionStrings.Add (program.GetString (option.Key));
  322. }
  323. // We can't continue until our client tell us which option to pick
  324. executionState = ExecutionState.WaitingOnOptionSelection;
  325. // Pass the options set to the client, as well as a delegate for them to call when the
  326. // user has made a selection
  327. optionsHandler (new Dialogue.OptionSetResult (optionStrings, delegate (int selectedOption) {
  328. // we now know what number option was selected; push the corresponding node name
  329. // to the stack
  330. var destinationNode = state.currentOptions[selectedOption].Value;
  331. state.PushValue(destinationNode);
  332. // We no longer need the accumulated list of options; clear it so that it's
  333. // ready for the next one
  334. state.currentOptions.Clear();
  335. // We can now also keep running
  336. executionState = ExecutionState.Running;
  337. }));
  338. break;
  339. case ByteCode.Concat:
  340. /// = Concat
  341. /** Pop two items off the stack, concat them, and push the result onto the stack.
  342. */
  343. {
  344. var second = state.PopValue();
  345. var first = state.PopValue();
  346. state.PushValue(first.AsString + second.AsString);
  347. }
  348. break;
  349. case ByteCode.RunLineFromStack:
  350. /// - RunLineFromStack
  351. /** Pop a string from the stack and pass to client as a line.
  352. */
  353. lineHandler(new Dialogue.LineResult(state.PopValue().AsString));
  354. break;
  355. default:
  356. /// - default
  357. /** Whoa, no idea what bytecode this is. Stop the program
  358. * and throw an exception.
  359. */
  360. executionState = ExecutionState.Stopped;
  361. throw new ArgumentOutOfRangeException ();
  362. }
  363. }
  364. }
  365. }