diff --git a/Tests/RandomOptions.node b/Tests/RandomOptions.node new file mode 100644 index 000000000..4e80f0882 --- /dev/null +++ b/Tests/RandomOptions.node @@ -0,0 +1,42 @@ +// Randomizing options + +These options should be shuffled: + +<> +-> One +-> Two +-> Three +-> Four + +These options should NOT be shuffled: + +-> One +-> Two +-> Three +-> Four + +These options should NOT be shuffled + +-> One: Select this option to see shuffled options. + <> + -> One + -> Two + -> Three + -> Four +-> Two: Select this option to do nothing. + + +These options should NOT be shuffled: + +-> One +-> Two +-> Three +-> Four + +These options should be shuffled: + +[[One|One]] +[[Two|One]] +[[Three|One]] +[[Four|One]] + diff --git a/Tests/TestCases/Commands.node b/Tests/TestCases/Commands.node new file mode 100644 index 000000000..7328b8300 --- /dev/null +++ b/Tests/TestCases/Commands.node @@ -0,0 +1,4 @@ +// Testing commands + +<> +<> \ No newline at end of file diff --git a/YarnSpinner/Compiler.cs b/YarnSpinner/Compiler.cs index 2e5b2254c..77cd1444c 100644 --- a/YarnSpinner/Compiler.cs +++ b/YarnSpinner/Compiler.cs @@ -221,9 +221,19 @@ internal enum ByteCode { } + internal class Compiler { + + struct CompileFlags { + // should we emit code that turns (VAR_SHUFFLE_OPTIONS) off + // after the next RunOptions bytecode? + public bool DisableShuffleOptionsAfterNextSet; + } + + CompileFlags flags; + internal Program program { get; private set; } internal Compiler () @@ -268,7 +278,16 @@ internal void CompileNode(Parser.Node node) { Emit (compiledNode, ByteCode.Stop); } else { // Otherwise, show the accumulated nodes and then jump to the selected node + Emit (compiledNode, ByteCode.ShowOptions); + + if (flags.DisableShuffleOptionsAfterNextSet == true) { + Emit (compiledNode, ByteCode.PushBool, false); + Emit (compiledNode, ByteCode.StoreVariable, VirtualMachine.SpecialVariables.ShuffleOptions); + Emit (compiledNode, ByteCode.Pop); + flags.DisableShuffleOptionsAfterNextSet = false; + } + Emit (compiledNode, ByteCode.RunNode); } @@ -339,12 +358,23 @@ void GenerateCode(Node node, Parser.CustomCommand statement) { // If this command is an evaluable expression, evaluate it if (statement.expression != null) { GenerateCode (node, statement.expression); - } else if (statement.clientCommand == "stop") { - // If it's the special command "stop", emit the Stop bytecode - Emit (node, ByteCode.Stop); } else { - // Emit the code that passes the command to the client - Emit (node, ByteCode.RunCommand, statement.clientCommand); + switch (statement.clientCommand) { + case "stop": + Emit (node, ByteCode.Stop); + break; + case "shuffleNextOptions": + // Emit code that sets "VAR_SHUFFLE_OPTIONS" to true + Emit (node, ByteCode.PushBool, true); + Emit (node, ByteCode.StoreVariable, VirtualMachine.SpecialVariables.ShuffleOptions); + Emit (node, ByteCode.Pop); + flags.DisableShuffleOptionsAfterNextSet = true; + break; + + default: + Emit (node, ByteCode.RunCommand, statement.clientCommand); + break; + } } } @@ -391,6 +421,13 @@ void GenerateCode(Node node, Parser.ShortcutOptionGroup statement) { Emit (node, ByteCode.ShowOptions); + if (flags.DisableShuffleOptionsAfterNextSet == true) { + Emit (node, ByteCode.PushBool, false); + Emit (node, ByteCode.StoreVariable, VirtualMachine.SpecialVariables.ShuffleOptions); + Emit (node, ByteCode.Pop); + flags.DisableShuffleOptionsAfterNextSet = false; + } + Emit (node, ByteCode.Jump); optionCount = 0; diff --git a/YarnSpinner/Dialogue.cs b/YarnSpinner/Dialogue.cs index b64f013ab..3398e8b92 100644 --- a/YarnSpinner/Dialogue.cs +++ b/YarnSpinner/Dialogue.cs @@ -254,8 +254,6 @@ public int LoadString(string text, bool showTokens=false, bool showParseTree=fal } while (vm.executionState != VirtualMachine.ExecutionState.Stopped && stopExecuting == false); - - } public void Stop() { diff --git a/YarnSpinner/Lexer.cs b/YarnSpinner/Lexer.cs index a0956f927..ee7c27b7b 100644 --- a/YarnSpinner/Lexer.cs +++ b/YarnSpinner/Lexer.cs @@ -141,6 +141,10 @@ internal class Token { public int columnNumber; public string context; + // For <<, we record everything up to the next >> on the line, + // so that the parser can get it back for custom commands + public string associatedRawText; + // If this is a function in an expression, this is the number // of parameters that were encountered public int parameterCount; @@ -528,6 +532,33 @@ private TokenList TokeniseLine(string context, string input, out int lineIndenta } } + // Attach text between << and >> to the << token + for (int i = 0; i < tokensToReturn.Count; i++) { + if (i == tokensToReturn.Count - 1) { + // don't bother checking if we're the last token in the line + continue; + } + var startToken = tokensToReturn[i]; + if (startToken.type == TokenType.BeginCommand) { + int startIndex = tokensToReturn[i+1].columnNumber; + int endIndex = -1; + // Find the next >> token + for (int j = i; j < tokensToReturn.Count; j++) { + var endToken = tokensToReturn [j]; + if (endToken.type == TokenType.EndCommand) { + endIndex = endToken.columnNumber; + break; + } + } + + if (endIndex != -1) { + var text = input.Substring (startIndex, endIndex - startIndex); + startToken.associatedRawText = text; + } + + } + } + // Return the list of tokens we found return tokensToReturn; } diff --git a/YarnSpinner/Parser.cs b/YarnSpinner/Parser.cs index 150d0c9a4..b86c7eaea 100644 --- a/YarnSpinner/Parser.cs +++ b/YarnSpinner/Parser.cs @@ -413,7 +413,7 @@ internal CustomCommand(ParseNode parent, Parser p) : base(parent, p) { // Custom commands can have ANY token in them. Read them all until we hit the // end command token. - p.ExpectSymbol(TokenType.BeginCommand); + var beginSymbol = p.ExpectSymbol(TokenType.BeginCommand); var commandTokens = new List(); do { commandTokens.Add(p.ExpectSymbol()); @@ -434,12 +434,7 @@ internal CustomCommand(ParseNode parent, Parser p) : base(parent, p) { // Otherwise, evaluate it as a command type = Type.ClientCommand; - var tokenStrings = new List(); - foreach (var token in commandTokens) { - tokenStrings.Add(token.value as string); - } - this.clientCommand = string.Join(" ", tokenStrings.ToArray()); - + this.clientCommand = beginSymbol.associatedRawText; } diff --git a/YarnSpinner/VirtualMachine.cs b/YarnSpinner/VirtualMachine.cs index 2de899185..4073898f3 100644 --- a/YarnSpinner/VirtualMachine.cs +++ b/YarnSpinner/VirtualMachine.cs @@ -6,6 +6,10 @@ namespace Yarn internal class VirtualMachine { + internal static class SpecialVariables { + public const string ShuffleOptions = "$Yarn.ShuffleOptions"; + } + internal VirtualMachine (Dialogue d, Program p) { program = p; @@ -276,12 +280,26 @@ internal void RunInstruction(Instruction i) { break; } + if (dialogue.continuity.GetNumber(SpecialVariables.ShuffleOptions) != 0.0f) { + // Shuffle the dialog options if needed + var r = new Random(); + for (int opt1 = state.currentOptions.Count-1; opt1 >= 0; opt1--) { + int opt2 = r.Next(0, state.currentOptions.Count-1); + var temp = state.currentOptions [opt2]; + state.currentOptions [opt2] = state.currentOptions [opt1]; + state.currentOptions [opt1] = temp; + } + } + // Otherwise, present the list of options to the user and let them pick var optionStrings = new List (); + foreach (var option in state.currentOptions) { optionStrings.Add (program.GetString (option.Key)); } + + // We can't continue until our client tell us which option to pick executionState = ExecutionState.WaitingOnOptionSelection; diff --git a/YarnSpinnerConsole/Main.cs b/YarnSpinnerConsole/Main.cs index 6245a3ff5..76d3f0338 100644 --- a/YarnSpinnerConsole/Main.cs +++ b/YarnSpinnerConsole/Main.cs @@ -224,6 +224,10 @@ public static void Main (string[] args) impl.expectedNextLine = parameters[0].AsString; }); + dialogue.library.RegisterFunction ("expect_command", 1, delegate(Value[] parameters) { + impl.expectedNextCommand = parameters[0].AsString; + }); + // If debugging is enabled, log debug messages; otherwise, ignore them if (showDebugging) { dialogue.LogDebugMessage = delegate(string message) { @@ -295,6 +299,8 @@ private class ConsoleRunnerImplementation : Yarn.VariableStorage { public string expectedNextLine = null; + public string expectedNextCommand = null; + public ConsoleRunnerImplementation(bool waitForLines = false) { this.variableStore = new MemoryVariableStore(); this.waitForLines = waitForLines; @@ -373,6 +379,14 @@ public void RunOptions (Options optionsGroup, OptionChooser optionChooser) public void RunCommand (string command) { + + if (expectedNextCommand != null && expectedNextCommand != command) { + // TODO: Output diagnostic info here + Console.WriteLine(string.Format("Unexpected line.\nExpected: {0}\nReceived: {1}", + expectedNextCommand, command)); + Environment.Exit (1); + } + Console.WriteLine("Command: <<"+command+">>"); } diff --git a/YarnSpinnerTests/Test.cs b/YarnSpinnerTests/Test.cs index e7a92d90e..4aea30e3d 100644 --- a/YarnSpinnerTests/Test.cs +++ b/YarnSpinnerTests/Test.cs @@ -85,6 +85,18 @@ public void TestParsingSmileys() dialogue.LoadFile (path); dialogue.Compile (); } + + [Test()] + public void TestCommands() + { + var path = System.IO.Path.Combine ("TestCases", "Commands.node"); + dialogue.LoadFile (path); + dialogue.Compile (); + + foreach (var result in dialogue.Run()) { + Console.WriteLine (result); + } + } } }