From e9af9962f2db5cc5ec5babca270564e1b923ec82 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 22 Dec 2022 17:51:17 -0800 Subject: [PATCH 1/4] token for command visit --- packages/flame_jenny/jenny/lib/src/parse/token.dart | 2 ++ packages/flame_jenny/jenny/test/parse/token_test.dart | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/flame_jenny/jenny/lib/src/parse/token.dart b/packages/flame_jenny/jenny/lib/src/parse/token.dart index a188d96ac02..787399d77a1 100644 --- a/packages/flame_jenny/jenny/lib/src/parse/token.dart +++ b/packages/flame_jenny/jenny/lib/src/parse/token.dart @@ -30,6 +30,7 @@ class Token { static const commandLocal = Token._(TokenType.commandLocal); static const commandSet = Token._(TokenType.commandSet); static const commandStop = Token._(TokenType.commandStop); + static const commandVisit = Token._(TokenType.commandVisit); static const commandWait = Token._(TokenType.commandWait); static const constFalse = Token._(TokenType.constFalse); static const constTrue = Token._(TokenType.constTrue); @@ -129,6 +130,7 @@ enum TokenType { commandLocal, // 'local' commandSet, // 'set' commandStop, // 'stop' + commandVisit, // 'visit' commandWait, // 'wait' constFalse, // 'false' constTrue, // 'true' diff --git a/packages/flame_jenny/jenny/test/parse/token_test.dart b/packages/flame_jenny/jenny/test/parse/token_test.dart index b5168c802f1..cd9b2c52e29 100644 --- a/packages/flame_jenny/jenny/test/parse/token_test.dart +++ b/packages/flame_jenny/jenny/test/parse/token_test.dart @@ -9,6 +9,7 @@ void main() { expect('${Token.colon}', 'Token.colon'); expect('${Token.commandEndif}', 'Token.commandEndif'); expect('${Token.commandLocal}', 'Token.commandLocal'); + expect('${Token.commandVisit}', 'Token.commandVisit'); expect('${Token.constTrue}', 'Token.constTrue'); expect('${Token.endIndent}', 'Token.endIndent'); expect('${Token.endMarkupTag}', 'Token.endMarkupTag'); From 7db5af9531ef74f0485feae6b1ccd0bdd0011f1b Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 22 Dec 2022 18:03:08 -0800 Subject: [PATCH 2/4] tokenize <> command --- .../jenny/lib/src/parse/tokenize.dart | 16 +++++++-- .../commands/visit_command_test.dart | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 packages/flame_jenny/jenny/test/structure/commands/visit_command_test.dart diff --git a/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart b/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart index 9e50e8e0189..5cf21689003 100644 --- a/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart +++ b/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart @@ -35,7 +35,13 @@ class _Lexer { lineStart = 0, tokens = [], modeStack = [], - indentStack = []; + indentStack = [], + assert( + commandTokens.length == + simpleCommands.length + + bareExpressionCommands.length + + nodeTargetingCommands.length, + ); final String text; final List tokens; @@ -229,7 +235,7 @@ class _Lexer { (bareExpressionCommands.contains(tokens.last) && pushToken(Token.startExpression, position) && pushMode(modeCommandExpression)) || - (tokens.last == Token.commandJump && + (nodeTargetingCommands.contains(tokens.last) && (eatId() || (eatExpressionStart() && pushMode(modeTextExpression)) || @@ -973,6 +979,7 @@ class _Lexer { 'local': Token.commandLocal, 'set': Token.commandSet, 'stop': Token.commandStop, + 'visit': Token.commandVisit, 'wait': Token.commandWait, }; @@ -993,6 +1000,11 @@ class _Lexer { Token.commandWait, }; + static final Set nodeTargetingCommands = { + Token.commandJump, + Token.commandVisit, + }; + /// Throws a [SyntaxError] with the given [message], augmenting it with the /// information about the current parsing location. /// diff --git a/packages/flame_jenny/jenny/test/structure/commands/visit_command_test.dart b/packages/flame_jenny/jenny/test/structure/commands/visit_command_test.dart new file mode 100644 index 00000000000..5737202f8fd --- /dev/null +++ b/packages/flame_jenny/jenny/test/structure/commands/visit_command_test.dart @@ -0,0 +1,35 @@ + +import 'package:jenny/src/parse/token.dart'; +import 'package:jenny/src/parse/tokenize.dart'; +import 'package:test/test.dart'; + +void main() { + group('VisitCommand', () { + test('tokenize bare-word node target', () { + expect( + tokenize('<>'), + const [ + Token.startCommand, + Token.commandVisit, + Token.id('WhiteHouse'), + Token.endCommand, + ], + ); + }); + + test('tokenize expression node target', () { + expect( + tokenize(r'<>'), + const [ + Token.startCommand, + Token.commandVisit, + Token.startExpression, + Token.variable(r'$destination'), + Token.endExpression, + Token.endCommand, + ], + ); + }); + + }); +} From a899d8a23c0902682cd274947a9de5a68eb95dcf Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 23 Dec 2022 09:09:01 -0800 Subject: [PATCH 3/4] visit command and tests --- .../jenny/lib/src/dialogue_runner.dart | 49 +++-- .../jenny/lib/src/parse/parse.dart | 16 +- .../jenny/lib/src/parse/tokenize.dart | 3 +- .../src/structure/commands/visit_command.dart | 17 ++ .../jenny/test/dialogue_view_test.dart | 56 ++++++ .../structure/commands/jump_command_test.dart | 2 +- .../commands/visit_command_test.dart | 190 +++++++++++++++++- 7 files changed, 304 insertions(+), 29 deletions(-) create mode 100644 packages/flame_jenny/jenny/lib/src/structure/commands/visit_command.dart diff --git a/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart b/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart index cdbcc8dc40a..d3ab07bb887 100644 --- a/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart +++ b/packages/flame_jenny/jenny/lib/src/dialogue_runner.dart @@ -65,10 +65,7 @@ class DialogueRunner { view.dialogueRunner = this; }); await _event((view) => view.onDialogueStart()); - _nextNode = nodeName; - while (_nextNode != null) { - await _runNode(_nextNode!); - } + await _runNode(nodeName); await _event((view) => view.onDialogueFinish()); } finally { _dialogueViews.forEach((dv) => dv.dialogueRunner = null); @@ -92,25 +89,28 @@ class DialogueRunner { } Future _runNode(String nodeName) async { - final node = project.nodes[nodeName]; - if (node == null) { - throw NameError('Node "$nodeName" could not be found'); - } + _nextNode = nodeName; + while (_nextNode != null) { + final node = project.nodes[_nextNode!]; + if (node == null) { + throw NameError('Node "$_nextNode" could not be found'); + } - _nextNode = null; - _currentNode = node; - _currentIterator = node.iterator; + _nextNode = null; + _currentNode = node; + _currentIterator = node.iterator; - await _event((view) => view.onNodeStart(node)); - while (_currentIterator?.moveNext() ?? false) { - final entry = _currentIterator!.current; - await entry.processInDialogueRunner(this); - } - _incrementNodeVisitCount(); - await _event((view) => view.onNodeFinish(node)); + await _event((view) => view.onNodeStart(node)); + while (_currentIterator?.moveNext() ?? false) { + final entry = _currentIterator!.current; + await entry.processInDialogueRunner(this); + } + _incrementNodeVisitCount(); + await _event((view) => view.onNodeFinish(node)); - _currentNode = null; - _currentIterator = null; + _currentNode = null; + _currentIterator = null; + } } void _incrementNodeVisitCount() { @@ -181,6 +181,15 @@ class DialogueRunner { _nextNode = nodeName; } + @internal + Future visitNode(String nodeName) async { + final node = _currentNode; + final iterator = _currentIterator; + await _runNode(nodeName); + _currentNode = node; + _currentIterator = iterator; + } + /// Similar to `Future.wait()`, but accepts `FutureOr`s. FutureOr _combineFutures(List> maybeFutures) { final futures = maybeFutures.whereType>().toList(); diff --git a/packages/flame_jenny/jenny/lib/src/parse/parse.dart b/packages/flame_jenny/jenny/lib/src/parse/parse.dart index 6032580bfe7..10be350ef1a 100644 --- a/packages/flame_jenny/jenny/lib/src/parse/parse.dart +++ b/packages/flame_jenny/jenny/lib/src/parse/parse.dart @@ -10,6 +10,7 @@ import 'package:jenny/src/structure/commands/local_command.dart'; import 'package:jenny/src/structure/commands/set_command.dart'; import 'package:jenny/src/structure/commands/stop_command.dart'; import 'package:jenny/src/structure/commands/user_defined_command.dart'; +import 'package:jenny/src/structure/commands/visit_command.dart'; import 'package:jenny/src/structure/commands/wait_command.dart'; import 'package:jenny/src/structure/dialogue_choice.dart'; import 'package:jenny/src/structure/dialogue_entry.dart'; @@ -400,8 +401,8 @@ class _Parser { final token = peekToken(1); if (token == Token.commandIf) { return parseCommandIf(); - } else if (token == Token.commandJump) { - return parseCommandJump(); + } else if (token == Token.commandJump || token == Token.commandVisit) { + return parseCommandJumpOrVisit(); } else if (token == Token.commandStop) { return parseCommandStop(); } else if (token == Token.commandWait) { @@ -493,9 +494,12 @@ class _Parser { return IfBlock(constTrue, statements); } - Command parseCommandJump() { + Command parseCommandJumpOrVisit() { take(Token.startCommand); - take(Token.commandJump); + final isJump = peekToken() == Token.commandJump; + final isVisit = peekToken() == Token.commandVisit; + assert(isJump || isVisit); + position += 1; final token = peekToken(); StringExpression target; if (token.isId) { @@ -516,7 +520,7 @@ class _Parser { } take(Token.endCommand); take(Token.newline); - return JumpCommand(target); + return isJump ? JumpCommand(target) : VisitCommand(target); } Command parseCommandStop() { @@ -724,9 +728,9 @@ class _Parser { /// The initial [lhs] sub-expression is provided, and the parsing position /// should be at the start of the next operator. Expression _parseExpressionImpl(Expression lhs, int minPrecedence) { - var position0 = position; var result = lhs; while ((precedences[peekToken()] ?? -1) >= minPrecedence) { + var position0 = position; final op = peekToken(); final opPrecedence = precedences[op]!; position += 1; diff --git a/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart b/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart index 5cf21689003..5e17453dc65 100644 --- a/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart +++ b/packages/flame_jenny/jenny/lib/src/parse/tokenize.dart @@ -239,7 +239,8 @@ class _Lexer { (eatId() || (eatExpressionStart() && pushMode(modeTextExpression)) || - error('an ID or an expression expected'))) || + error('an ID or an expression in curly braces ' + 'expected'))) || (tokens.last.isCommand && // user-defined commands pushMode(modeCommandText)))) || (eatCommandEnd() && popMode(modeCommand)) || diff --git a/packages/flame_jenny/jenny/lib/src/structure/commands/visit_command.dart b/packages/flame_jenny/jenny/lib/src/structure/commands/visit_command.dart new file mode 100644 index 00000000000..e490616a083 --- /dev/null +++ b/packages/flame_jenny/jenny/lib/src/structure/commands/visit_command.dart @@ -0,0 +1,17 @@ +import 'package:jenny/src/dialogue_runner.dart'; +import 'package:jenny/src/structure/commands/command.dart'; +import 'package:jenny/src/structure/expressions/expression.dart'; + +class VisitCommand extends Command { + VisitCommand(this.target); + + final StringExpression target; + + @override + Future execute(DialogueRunner dialogue) { + return dialogue.visitNode(target.value); + } + + @override + String get name => 'visit'; +} diff --git a/packages/flame_jenny/jenny/test/dialogue_view_test.dart b/packages/flame_jenny/jenny/test/dialogue_view_test.dart index eef2b2e2312..139b16a4d10 100644 --- a/packages/flame_jenny/jenny/test/dialogue_view_test.dart +++ b/packages/flame_jenny/jenny/test/dialogue_view_test.dart @@ -52,6 +52,62 @@ void main() { ); }); + test('jumps and visits', () async { + final yarn = YarnProject() + ..parse( + dedent(''' + title: Start + --- + First line + <> + Second line + <> + === + title: AnotherNode + --- + Inside another node + <> + === + title: SomewhereElse + --- + This is nowhere... + === + '''), + ); + final view1 = _DefaultDialogueView(); + final view2 = _RecordingDialogueView(); + final dialogueRunner = DialogueRunner( + yarnProject: yarn, + dialogueViews: [view1, view2], + ); + await dialogueRunner.startDialogue('Start'); + expect( + view2.events, + const [ + 'onDialogueStart', + 'onNodeStart(Start)', + 'onLineStart(First line)', + 'onLineFinish(First line)', + 'onNodeStart(AnotherNode)', + 'onLineStart(Inside another node)', + 'onLineFinish(Inside another node)', + 'onNodeFinish(AnotherNode)', + 'onNodeStart(SomewhereElse)', + 'onLineStart(This is nowhere...)', + 'onLineFinish(This is nowhere...)', + 'onNodeFinish(SomewhereElse)', + 'onLineStart(Second line)', + 'onLineFinish(Second line)', + 'onNodeFinish(Start)', + 'onNodeStart(SomewhereElse)', + 'onLineStart(This is nowhere...)', + 'onLineFinish(This is nowhere...)', + 'onNodeFinish(SomewhereElse)', + 'onDialogueFinish()', + ], + ); + }); + test('stop line', () async { final yarn = YarnProject() ..parse( diff --git a/packages/flame_jenny/jenny/test/structure/commands/jump_command_test.dart b/packages/flame_jenny/jenny/test/structure/commands/jump_command_test.dart index 12841a408df..b746456ebb7 100644 --- a/packages/flame_jenny/jenny/test/structure/commands/jump_command_test.dart +++ b/packages/flame_jenny/jenny/test/structure/commands/jump_command_test.dart @@ -80,7 +80,7 @@ void main() { '===\n', ), hasSyntaxError( - 'SyntaxError: an ID or an expression expected\n' + 'SyntaxError: an ID or an expression in curly braces expected\n' '> at line 3 column 8:\n' '> <>\n' '> ^\n', diff --git a/packages/flame_jenny/jenny/test/structure/commands/visit_command_test.dart b/packages/flame_jenny/jenny/test/structure/commands/visit_command_test.dart index 5737202f8fd..15599dc6e33 100644 --- a/packages/flame_jenny/jenny/test/structure/commands/visit_command_test.dart +++ b/packages/flame_jenny/jenny/test/structure/commands/visit_command_test.dart @@ -1,8 +1,12 @@ - +import 'package:jenny/jenny.dart'; import 'package:jenny/src/parse/token.dart'; import 'package:jenny/src/parse/tokenize.dart'; +import 'package:jenny/src/structure/commands/visit_command.dart'; import 'package:test/test.dart'; +import '../../test_scenario.dart'; +import '../../utils.dart'; + void main() { group('VisitCommand', () { test('tokenize bare-word node target', () { @@ -31,5 +35,189 @@ void main() { ); }); + test('<> command parsing', () { + final yarn = YarnProject() + ..variables.setVariable(r'$target', 'Y') + ..parse('title:A\n---\n' + '<>\n' + '<>\n' + '===\n'); + final node = yarn.nodes['A']!; + expect(node.lines.length, 2); + expect(node.lines[0], isA()); + expect(node.lines[1], isA()); + expect((node.lines[0] as VisitCommand).target.value, 'X'); + expect((node.lines[1] as VisitCommand).target.value, 'Y'); + }); + + test('visiting another node', () async { + await testScenario( + input: ''' + title: Start + --- + First line + <> + Second line + === + + title: Second + --- + Just visiting! + === + ''', + testPlan: ''' + line: First line + line: Just visiting! + line: Second line + ''', + ); + }); + + test('nested visits', () async { + await testScenario( + input: ''' + title: Start + --- + Alpha + <> + Beta + <> + Gamma + === + + title: V1 + --- + Delta + <> + Eta + === + + title: V2 + --- + <> + Omega + === + + title: V3 + --- + Rho + === + ''', + testPlan: ''' + line: Alpha + line: Delta + line: Rho + line: Omega + line: Eta + line: Beta + line: Rho + line: Omega + line: Gamma + ''', + ); + }); + + test('visit node from expression', () async { + await testScenario( + input: r''' + title: Start + --- + <> + Line before + <> + Line after + === + --- + title: Destination_2 + --- + Here + === + ''', + testPlan: ''' + line: Line before + line: Here + line: Line after + ''', + ); + }); + + test('visit node with jumps', () async { + await testScenario( + input: ''' + title: Start + --- + First line of node + <> + And we're back in the node + === + -------------- + title: Another + -------------- + Inside node + <> + This line shouldn't be seen... + === + ---------------- + title: Somewhere + ---------------- + Reached node + <> + ERROR! + === + ''', + testPlan: ''' + line: First line of node + line: Inside node + line: Reached node + line: And we're back in the node + ''', + ); + }); + + group('errors', () { + test('<> at root level', () { + expect( + () => YarnProject()..parse('<>\n'), + hasTypeError( + 'TypeError: command <> is only allowed inside nodes\n' + '> at line 1 column 1:\n' + '> <>\n' + '> ^\n', + ), + ); + }); + + test('<> invalid syntax', () { + expect( + () => YarnProject() + ..parse( + 'title:A\n---\n' + '<>\n' + '===\n', + ), + hasSyntaxError( + 'SyntaxError: an ID or an expression in curly braces expected\n' + '> at line 3 column 9:\n' + '> <>\n' + '> ^\n', + ), + ); + }); + + test('<> with unknown destination', () async { + final yarn = YarnProject() + ..parse( + 'title: A\n' + '---\n' + '<>\n' + '===\n', + ); + expect( + () => DialogueRunner(yarnProject: yarn, dialogueViews: []) + .startDialogue('A'), + hasNameError('NameError: Node "Somewhere" could not be found'), + ); + }); + }); }); } From 0a2cdcfbb6be4c0eb64a74e31532cadf30a66c20 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 23 Dec 2022 09:42:13 -0800 Subject: [PATCH 4/4] docs --- .../jenny/language/commands/commands.md | 4 ++ .../jenny/language/commands/jump.md | 6 +++ .../jenny/language/commands/visit.md | 38 +++++++++++++++++++ .../jenny/lib/src/command_storage.dart | 1 + 4 files changed, 49 insertions(+) create mode 100644 doc/other_modules/jenny/language/commands/visit.md diff --git a/doc/other_modules/jenny/language/commands/commands.md b/doc/other_modules/jenny/language/commands/commands.md index adabd2be0da..dab95c499be 100644 --- a/doc/other_modules/jenny/language/commands/commands.md +++ b/doc/other_modules/jenny/language/commands/commands.md @@ -38,6 +38,9 @@ scripts. For a full description of these commands, see the document on [user-def **[\<\\>](stop.md)** : Stops executing the current node. +**[\<\\>](visit.md)** +: Temporarily jumps to another node, and then comes back. + **[\<\\>](wait.md)** : Pauses the dialogue for the specified amount of time. @@ -53,6 +56,7 @@ scripts. For a full description of these commands, see the document on [user-def <> <> <> +<> <> User-defined commands ``` diff --git a/doc/other_modules/jenny/language/commands/jump.md b/doc/other_modules/jenny/language/commands/jump.md index 336b157fe57..a6df718ac27 100644 --- a/doc/other_modules/jenny/language/commands/jump.md +++ b/doc/other_modules/jenny/language/commands/jump.md @@ -16,3 +16,9 @@ node ID, or as an expression in curly braces: If the expression evaluates at runtime to an unknown name, then a `NameError` exception will be thrown. + + +## See Also + +- [\<\\>](visit.md) command, which jumps into the destination node temporarily and then + returns to the same place in the dialogue as before. diff --git a/doc/other_modules/jenny/language/commands/visit.md b/doc/other_modules/jenny/language/commands/visit.md new file mode 100644 index 00000000000..461eb7cc032 --- /dev/null +++ b/doc/other_modules/jenny/language/commands/visit.md @@ -0,0 +1,38 @@ +# `<>` + +The **\<\\>** command temporarily puts the current node on hold, executes the target node, +and after it finishes, resumes execution of the previous node. This is similar to a function call +in many programming languages. + +The `<>` command can be useful for splitting a large dialogue into several smaller nodes, +or for reusing some common dialogue lines in several nodes. For example: + +```yarn +title: RoamingTrader1 +--- +<> + Hello again, {$player}! +<> + <> +<> + +-> What do you think about the Calamity? <> + <> +-> Have you seen a weird-looking girl running by? <> + <> +-> What do you have for trade? + <> + +Pleasure doing business with you! #auto +=== +``` + +The argument of this command is the id of the node to jump to. It can be given either as a plain +node ID, or as an expression in curly braces: + +```yarn +<> +``` + +If the expression evaluates at runtime to an unknown name, then a `NameError` exception will be +thrown. diff --git a/packages/flame_jenny/jenny/lib/src/command_storage.dart b/packages/flame_jenny/jenny/lib/src/command_storage.dart index 58b023aa28b..1dfe18217e6 100644 --- a/packages/flame_jenny/jenny/lib/src/command_storage.dart +++ b/packages/flame_jenny/jenny/lib/src/command_storage.dart @@ -104,6 +104,7 @@ class CommandStorage { 'set', 'stop', 'stop', + 'visit', 'wait', 'while', ];