Skip to content

Commit

Permalink
feat(jenny): Added support for markup attributes (#2183)
Browse files Browse the repository at this point in the history
Markup allows inserting additional information into the text, without affecting dialogue views who cannot handle that information. See https://docs.yarnspinner.dev/getting-started/writing-in-yarn/markup
  • Loading branch information
st-pasha committed Nov 24, 2022
1 parent 1536e87 commit f887545
Show file tree
Hide file tree
Showing 30 changed files with 1,276 additions and 260 deletions.
4 changes: 3 additions & 1 deletion .github/.cspell/flame_dictionary.txt
Expand Up @@ -4,6 +4,7 @@ Audioplayers
BGUG
Dashbook
Fireslime
Hermione
Kawabunga
Kinect
Klingsbo
Expand All @@ -20,9 +21,10 @@ erayzesen
padracing
ptero
spydon
stpasha
tavian
trex
wolfenrain
Wyrmsun
Bodymovin
xaha
xaha
1 change: 1 addition & 0 deletions .github/.cspell/gamedev_dictionary.txt
Expand Up @@ -142,6 +142,7 @@ unfollow
unmount
unproject
unscale
vantablack
viewport
viewport's
viewports
Expand Down
5 changes: 5 additions & 0 deletions packages/flame_jenny/jenny/analysis_options.yaml
@@ -1 +1,6 @@
include: package:flame_lint/analysis_options.yaml

linter:
rules:
avoid_equals_and_hash_code_on_mutable_classes: false
hash_and_equals: false
2 changes: 1 addition & 1 deletion packages/flame_jenny/jenny/lib/jenny.dart
Expand Up @@ -3,8 +3,8 @@ export 'src/dialogue_view.dart' show DialogueView;
export 'src/errors.dart' show SyntaxError, NameError, TypeError, DialogueError;
export 'src/structure/dialogue_choice.dart' show DialogueChoice;
export 'src/structure/dialogue_line.dart' show DialogueLine;
export 'src/structure/dialogue_option.dart' show DialogueOption;
export 'src/structure/expressions/expression_type.dart' show ExpressionType;
export 'src/structure/node.dart' show Node;
export 'src/structure/option.dart' show Option;
export 'src/variable_storage.dart' show VariableStorage;
export 'src/yarn_project.dart' show YarnProject;
34 changes: 10 additions & 24 deletions packages/flame_jenny/jenny/lib/src/dialogue_runner.dart
Expand Up @@ -8,7 +8,6 @@ import 'package:jenny/src/structure/commands/user_defined_command.dart';
import 'package:jenny/src/structure/dialogue_choice.dart';
import 'package:jenny/src/structure/dialogue_line.dart';
import 'package:jenny/src/structure/node.dart';
import 'package:jenny/src/structure/statement.dart';
import 'package:jenny/src/yarn_project.dart';
import 'package:meta/meta.dart';

Expand Down Expand Up @@ -46,7 +45,7 @@ class DialogueRunner {
final List<NodeIterator> _iterators;
_LineDeliveryPipeline? _linePipeline;

/// Executes the node with the given name, and returns a future that finished
/// Executes the node with the given name, and returns a future that finishes
/// once the dialogue stops running.
Future<void> runNode(String nodeName) async {
if (_currentNodes.isNotEmpty) {
Expand All @@ -71,18 +70,8 @@ class DialogueRunner {
while (_iterators.isNotEmpty) {
final iterator = _iterators.last;
if (iterator.moveNext()) {
final nextLine = iterator.current;
switch (nextLine.kind) {
case StatementKind.line:
await _deliverLine(nextLine as DialogueLine);
break;
case StatementKind.choice:
await _deliverChoices(nextLine as DialogueChoice);
break;
case StatementKind.command:
await _deliverCommand(nextLine as Command);
break;
}
final entry = iterator.current;
await entry.processInDialogueRunner(this);
} else {
_iterators.removeLast();
_currentNodes.removeLast();
Expand All @@ -105,21 +94,17 @@ class DialogueRunner {
_linePipeline?.stop();
}

Future<void> _deliverLine(DialogueLine line) async {
@internal
Future<void> deliverLine(DialogueLine line) async {
final pipeline = _LineDeliveryPipeline(line, _dialogueViews);
_linePipeline = pipeline;
pipeline.start();
await pipeline.future;
_linePipeline = null;
}

Future<void> _deliverChoices(DialogueChoice choice) async {
// Compute which options are available and which aren't. This must be done
// only once, because some options may have non-deterministic conditionals
// which may produce different results on each invocation.
for (final option in choice.options) {
option.available = option.condition?.value ?? true;
}
@internal
Future<void> deliverChoices(DialogueChoice choice) async {
final futures = [
for (final view in _dialogueViews) view.onChoiceStart(choice)
];
Expand All @@ -140,8 +125,9 @@ class DialogueRunner {
enterBlock(chosenOption.block);
}

FutureOr<void> _deliverCommand(Command command) {
return _combineFutures([
@internal
Future<void> deliverCommand(Command command) async {
await _combineFutures([
command.execute(this),
if (command is UserDefinedCommand)
for (final view in _dialogueViews) view.onCommand(command)
Expand Down
4 changes: 2 additions & 2 deletions packages/flame_jenny/jenny/lib/src/dialogue_view.dart
Expand Up @@ -4,8 +4,8 @@ import 'package:jenny/src/errors.dart';
import 'package:jenny/src/structure/commands/user_defined_command.dart';
import 'package:jenny/src/structure/dialogue_choice.dart';
import 'package:jenny/src/structure/dialogue_line.dart';
import 'package:jenny/src/structure/dialogue_option.dart';
import 'package:jenny/src/structure/node.dart';
import 'package:jenny/src/structure/option.dart';
import 'package:meta/meta.dart';

abstract class DialogueView {
Expand Down Expand Up @@ -129,7 +129,7 @@ abstract class DialogueView {
///
/// If this method returns a future, the dialogue runner will wait for that
/// future to complete before proceeding with the dialogue.
FutureOr<void> onChoiceFinish(Option option) {}
FutureOr<void> onChoiceFinish(DialogueOption option) {}

/// Called when the dialogue encounters a user-defined command.
///
Expand Down
166 changes: 144 additions & 22 deletions packages/flame_jenny/jenny/lib/src/parse/parse.dart
Expand Up @@ -11,17 +11,19 @@ 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/wait_command.dart';
import 'package:jenny/src/structure/dialogue_choice.dart';
import 'package:jenny/src/structure/dialogue_entry.dart';
import 'package:jenny/src/structure/dialogue_line.dart';
import 'package:jenny/src/structure/dialogue_option.dart';
import 'package:jenny/src/structure/expressions/arithmetic.dart';
import 'package:jenny/src/structure/expressions/expression.dart';
import 'package:jenny/src/structure/expressions/functions.dart';
import 'package:jenny/src/structure/expressions/literal.dart';
import 'package:jenny/src/structure/expressions/logical.dart';
import 'package:jenny/src/structure/expressions/relational.dart';
import 'package:jenny/src/structure/expressions/string.dart';
import 'package:jenny/src/structure/line_content.dart';
import 'package:jenny/src/structure/markup_attribute.dart';
import 'package:jenny/src/structure/node.dart';
import 'package:jenny/src/structure/option.dart';
import 'package:jenny/src/structure/statement.dart';
import 'package:jenny/src/yarn_project.dart';
import 'package:meta/meta.dart';

Expand Down Expand Up @@ -122,7 +124,7 @@ class _Parser {
}

Block parseStatementList() {
final lines = <Statement>[];
final lines = <DialogueEntry>[];
while (true) {
final nextToken = peekToken();
if (nextToken == Token.arrow) {
Expand All @@ -142,7 +144,8 @@ class _Parser {
lines.add(command);
} else if (nextToken.isText ||
nextToken.isPerson ||
nextToken == Token.startExpression) {
nextToken == Token.startExpression ||
nextToken == Token.startMarkupTag) {
lines.add(parseDialogueLine());
} else if (nextToken == Token.newline) {
position += 1;
Expand Down Expand Up @@ -172,7 +175,7 @@ class _Parser {
);
}

Option parseOption() {
DialogueOption parseOption() {
take(Token.arrow);
final person = maybeParseLinePerson();
final content = parseLineContent();
Expand All @@ -188,7 +191,7 @@ class _Parser {
block = parseStatementList();
take(Token.endIndent);
}
return Option(
return DialogueOption(
content: content,
character: person,
tags: tags,
Expand All @@ -207,35 +210,85 @@ class _Parser {
return null;
}

StringExpression parseLineContent() {
final parts = <StringExpression>[];
LineContent parseLineContent() {
final stringBuilder = StringBuffer();
final expressions = <InlineExpression>[];
final attributes = <MarkupAttribute>[];
final markupStack = <_Markup>[];
var subIndex = 0;
while (true) {
final token = peekToken();
if (token.isText) {
parts.add(StringLiteral(token.content));
subIndex = 0;
stringBuilder.write(token.content);
position += 1;
} else if (token == Token.startExpression) {
subIndex += 1;
take(Token.startExpression);
final expression = parseExpression();
if (expression.isString) {
parts.add(expression as StringExpression);
} else if (expression.isNumeric) {
parts.add(NumToStringFn(expression as NumExpression));
} else if (expression.isBoolean) {
parts.add(BoolToStringFn(expression as BoolExpression));
}
take(Token.endExpression);
expressions.add(
InlineExpression(
stringBuilder.length,
expression.isNumeric
? NumToStringFn(expression as NumExpression)
: expression.isBoolean
? BoolToStringFn(expression as BoolExpression)
: expression as StringExpression,
),
);
} else if (token == Token.startMarkupTag) {
take(Token.startMarkupTag);
final position0 = position;
final markupTag = parseMarkupTag();
take(Token.endMarkupTag);
if (markupTag.closing) {
if (markupStack.isEmpty) {
position = position0;
syntaxError('unexpected closing markup tag');
}
// close-all tag
if (markupTag.name == null) {
while (markupStack.isNotEmpty) {
final tag = markupStack.removeLast();
tag.endTextPosition = stringBuilder.length;
tag.endSubIndex = subIndex;
attributes.add(tag.build());
}
} else {
final openTag = markupStack.removeLast();
if (openTag.name != markupTag.name) {
position = position0 + 1;
syntaxError('Expected closing tag for [${openTag.name}]');
}
openTag.endTextPosition = stringBuilder.length;
openTag.endSubIndex = subIndex;
attributes.add(openTag.build());
}
} else {
markupTag.startTextPosition = stringBuilder.length;
markupTag.startSubIndex = subIndex;
// TODO(stpasha): check that the name of the markup tag is known
if (markupTag.selfClosing) {
markupTag.endTextPosition = stringBuilder.length;
markupTag.endSubIndex = subIndex;
attributes.add(markupTag.build());
} else {
markupStack.add(markupTag);
}
}
} else {
break;
}
}
if (parts.length == 1) {
return parts.first;
} else if (parts.length > 1) {
return Concatenate(parts);
} else {
return constEmptyString;
if (markupStack.isNotEmpty) {
syntaxError('markup tag [${markupStack.last.name}] was not closed');
}
return LineContent(
stringBuilder.toString(),
expressions.isEmpty ? null : expressions,
attributes.isEmpty ? null : attributes,
);
}

BoolExpression? maybeParseLineCondition() {
Expand Down Expand Up @@ -274,6 +327,52 @@ class _Parser {
return out.isEmpty ? null : out;
}

_Markup parseMarkupTag() {
final result = _Markup();
if (peekToken() == Token.closeMarkupTag) {
position += 1;
result.closing = true;
final nextToken = peekToken();
if (nextToken.isId) {
result.name = nextToken.content;
position += 1;
} else if (nextToken != Token.endMarkupTag) {
syntaxError('a markup tag name is expected');
}
} else {
final nextToken = peekToken();
if (nextToken.isId) {
result.name = nextToken.content;
position += 1;
} else {
syntaxError('a markup tag name is expected');
}
while (peekToken().isId) {
final position0 = position;
final parameter = peekToken().content;
position += 1;
final Expression expression;
if (peekToken() == Token.operatorAssign) {
position += 1;
expression = parseExpression();
} else {
expression = constTrue;
}
if (result.parameters.containsKey(parameter)) {
position = position0;
syntaxError('duplicate parameter $parameter in a markup attribute');
}
result.parameters[parameter] = expression;
}
final lastToken = peekToken();
if (lastToken == Token.closeMarkupTag) {
result.selfClosing = true;
position += 1;
}
}
return result;
}

//#endregion

//#region Commands parsing
Expand Down Expand Up @@ -897,3 +996,26 @@ class _NodeHeader {
String? title;
Map<String, String>? tags;
}

class _Markup {
bool closing = false;
bool selfClosing = false;
String? name;
int? startTextPosition;
int? endTextPosition;
int? startSubIndex;
int? endSubIndex;
Map<String, Expression> parameters = {};

MarkupAttribute build() {
assert(!closing);
return MarkupAttribute(
name!,
startTextPosition!,
endTextPosition!,
startSubIndex!,
endSubIndex!,
parameters.isEmpty ? null : parameters,
);
}
}

0 comments on commit f887545

Please sign in to comment.