diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a569ab1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +/addons/Wol/editor/WolEditor.tscn filter=lfs diff=lfs merge=lfs -text diff --git a/ExampleDialogue/ExampleScene.gd b/ExampleDialogue/ExampleScene.gd index b3e64b5..9b69dc4 100644 --- a/ExampleDialogue/ExampleScene.gd +++ b/ExampleDialogue/ExampleScene.gd @@ -8,17 +8,20 @@ func _ready(): $Ship/DialogueStarter.connect('body_entered', self, '_on_player_near_dialogue', [$Ship, true]) $Ship/DialogueStarter.connect('body_exited', self, '_on_player_near_dialogue', [$Ship, false]) + $Dialogue/Wol.connect('finished', self, '_on_finished') + func _on_player_near_dialogue(_player, node, entered): - print('body entered?', entered) if entered: current_dialogue = node.name else: current_dialogue = null +func _on_finished(): + $DialogueCooldown.start() + func _process(_delta): - if Input.is_action_just_released('ui_accept') and current_dialogue and not $Dialogue/Wol.running: - print(current_dialogue) + if Input.is_action_just_released('ui_accept') \ + and current_dialogue and not $Dialogue/Wol.running and $DialogueCooldown.time_left == 0: $Dialogue/Wol.starting_node = current_dialogue $Dialogue/Wol.path = 'res://ExampleDialogue/%s.yarn' % current_dialogue $Dialogue/Wol.start() - print($Dialogue/Wol.variable_storage) diff --git a/ExampleDialogue/ExampleScene.tscn b/ExampleDialogue/ExampleScene.tscn index 98806f7..114a5b2 100644 --- a/ExampleDialogue/ExampleScene.tscn +++ b/ExampleDialogue/ExampleScene.tscn @@ -230,3 +230,7 @@ __meta__ = { [node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"] polygon = PoolVector2Array( -3, 427, -40, 426, -40, 648, 838, 647, 838, 419, 787, 420, 788, 600, 2, 600, 0, 420 ) + +[node name="DialogueCooldown" type="Timer" parent="."] +wait_time = 0.4 +one_shot = true diff --git a/addons/Wol/core/StandardLibrary.gd b/addons/Wol/core/StandardLibrary.gd index 9f5abf5..00adb96 100644 --- a/addons/Wol/core/StandardLibrary.gd +++ b/addons/Wol/core/StandardLibrary.gd @@ -79,7 +79,11 @@ func node_visit_count(node = virtual_machine.current_node.name): if node is Value: node = virtual_machine.program.strings[node.value()].text + var variable_storage = virtual_machine.dialogue.variable_storage var visited_node_count = variable_storage[virtual_machine.program.filename] + print('checking node "%s"' % node) + print(variable_storage) + return visited_node_count[node] if visited_node_count.has(node) else 0 diff --git a/addons/Wol/core/VirtualMachine.gd b/addons/Wol/core/VirtualMachine.gd index 96066c2..ad66224 100644 --- a/addons/Wol/core/VirtualMachine.gd +++ b/addons/Wol/core/VirtualMachine.gd @@ -68,7 +68,7 @@ func set_node(name): dialogue.variable_storage[program.filename] = {} if not dialogue.variable_storage[program.filename].has(name): - dialogue.variable_storage[program.filename][name] = 0 + dialogue.variable_storage[program.filename][name] = 1 else: dialogue.variable_storage[program.filename][name] += 1 return true @@ -287,7 +287,7 @@ func run_instruction(instruction): var line = option[0] var destination = option[1] choices.append(Program.Option.new(line, option_index, destination)) - + execution_state = Constants.ExecutionState.WaitingForOption options_handler.call_func(choices) diff --git a/addons/Wol/core/compiler/compiler.gd b/addons/Wol/core/compiler/compiler.gd index aafd6a7..617042d 100644 --- a/addons/Wol/core/compiler/compiler.gd +++ b/addons/Wol/core/compiler/compiler.gd @@ -1,5 +1,7 @@ extends Object +signal error(message, line_number, column) + const Constants = preload('res://addons/Wol/core/Constants.gd') const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') const Program = preload('res://addons/Wol/core/Program.gd') @@ -12,13 +14,15 @@ var filename = '' var current_node var has_implicit_string_tags = false +var soft_assert = false var string_count = 0 var string_table = {} var label_count = 0 -func _init(_filename, _source = null): +func _init(_filename, _source = null, _soft_assert = false): filename = _filename + soft_assert = _soft_assert if not _filename and _source: self.source = _source @@ -28,69 +32,119 @@ func _init(_filename, _source = null): self.source = file.get_as_text() file.close() -func compile(): - var header_sep = RegEx.new() - var header_property = RegEx.new() - header_sep.compile('---(\r\n|\r|\n)') - header_property.compile('(?.*): *(?.*)') - - assert(header_sep.search(source), 'No headers found!') - - var line_number = 0 - - var source_lines = source.split('\n', false) + var source_lines = source.split('\n') for i in range(source_lines.size()): source_lines[i] = source_lines[i].strip_edges(false, true) - var parsed_nodes = [] + source = source_lines.join('\n') + +func get_headers(offset = 0): + var header_property = RegEx.new() + var header_sep = RegEx.new() + + header_sep.compile('---(\r\n|\r|\n)') + header_property.compile('(?.*): *(?.*)') + + self.assert(header_sep.search(source), 'No headers found!') + + var title = '' + var position = Vector2.ZERO + + var source_lines = source.split('\n') + var line_number = offset while line_number < source_lines.size(): - var title = '' - var body = '' - - # Parse header - while true: - var line = source_lines[line_number] - line_number += 1 - - if not line.empty(): - var result = header_property.search(line) - if result != null: - var field = result.get_string('field') - var value = result.get_string('value') - - if field == 'title': - var regex = RegEx.new() - regex.compile(INVALID_TITLE) - assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)]) - - title = value - # TODO: Implement position, color and tags - - if line_number >= source_lines.size() or line == '---': - break - - # past header - var body_lines = [] + var line = source_lines[line_number] + line_number += 1 - while line_number < source_lines.size() and source_lines[line_number] != '===': - body_lines.append(source_lines[line_number]) - line_number += 1 + if not line.empty(): + var result = header_property.search(line) + if result != null: + var field = result.get_string('field') + var value = result.get_string('value') + + if field == 'title': + var regex = RegEx.new() + regex.compile(INVALID_TITLE) + self.assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)]) + + title = value + + if field == 'position': + var regex = RegEx.new() + regex.compile('^position:.*,.*\\d$') + self.assert(regex.search(line), 'Couldn\'t parse position property in the headers, got "%s" instead in node "%s"' % [value, title]) + + position = Vector2(int(value.split(',')[0].strip_edges()), int(value.split(',')[1].strip_edges())) + + # TODO: Implement color and tags + + if line == '---': + break + + return { + 'title': title, + 'position': position + } + +func get_body(offset = 0): + var body_lines = [] + + var source_lines = source.split('\n') + var recording = false + var line_number = offset + + while line_number < source_lines.size() and source_lines[line_number] != '===': + if recording: + body_lines.append(source_lines[line_number]) + + recording = recording or source_lines[line_number] == '---' line_number += 1 - body = PoolStringArray(body_lines).join('\n') - var lexer = Lexer.new(filename, title, body) - var tokens = lexer.tokenize() + line_number += 1 - var parser = Parser.new(title, tokens) - var parser_node = parser.parse_node() + return PoolStringArray(body_lines).join('\n') - parser_node.name = title - # parser_node.tags = title - parsed_nodes.append(parser_node) +func get_nodes(): + var nodes = [] + var line_number = 0 + var source_lines = source.split('\n') + while line_number < source_lines.size(): + var headers = get_headers(line_number) + var body = get_body(line_number) + headers.body = body + + nodes.append(headers) + + # Add +2 to the final line to skip the === from that node + line_number = Array(source_lines).find_last(body.split('\n')[-1]) + 2 while line_number < source_lines.size() and source_lines[line_number].empty(): line_number += 1 + + return nodes + +func assert(statement, message, line_number = -1, column = -1, _absolute_line_number = -1): + if not soft_assert: + assert(statement, message + ('; on line %d column %d' % [line_number, column])) + elif not statement: + emit_signal('error', message, line_number, column) + +func compile(): + var parsed_nodes = [] + for node in get_nodes(): + var lexer = Lexer.new(self, filename, node.title, node.body) + var tokens = lexer.tokenize() + + # In case of lexer error + if not tokens: + return + + var parser = Parser.new(self, node.title, tokens) + var parser_node = parser.parse_node() + + parser_node.name = node.title + parsed_nodes.append(parser_node) var program = Program.new() program.filename = filename @@ -104,7 +158,7 @@ func compile(): return program func compile_node(program, parsed_node): - assert(not program.nodes.has(parsed_node.name), 'Duplicate node in program: %s' % parsed_node.name) + self.assert(not program.nodes.has(parsed_node.name), 'Duplicate node in program: %s' % parsed_node.name) var node_compiled = Program.WolNode.new() @@ -205,7 +259,7 @@ func generate_statement(node, statement): Constants.StatementTypes.Line: generate_line(node, statement) _: - assert(false, 'Illegal statement type [%s]. Could not generate code.' % statement.type) + self.assert(false, statement.line_number, 'Illegal statement type [%s]. Could not generate code.' % statement.type) func generate_custom_command(node, command): # TODO: See if the first tree of this statement is being used @@ -234,7 +288,7 @@ func generate_shortcut_group(node, shortcut_group): var end = register_label('group_end') var labels = [] var option_count = 0 - + for option in shortcut_group.options: var endof_clause = '' var op_destination = register_label('option_%s' % [option_count + 1]) diff --git a/addons/Wol/core/compiler/lexer.gd b/addons/Wol/core/compiler/lexer.gd index 53faf40..b651b99 100644 --- a/addons/Wol/core/compiler/lexer.gd +++ b/addons/Wol/core/compiler/lexer.gd @@ -2,7 +2,7 @@ extends Object const Constants = preload('res://addons/Wol/core/Constants.gd') -const LINE_COMENT = '//' +const LINE_COMMENT = '//' const FORWARD_SLASH = '/' const LINE_SEPARATOR = '\n' @@ -22,6 +22,7 @@ const FORMAT_FUNCTION = 'format' var WHITESPACE = '\\s*' +var compiler var filename = '' var title = '' var text = '' @@ -33,14 +34,15 @@ var current_state var indent_stack = [] var should_track_indent = false -func _init(_filename, _title, _text): - createstates() +func _init(_compiler, _filename, _title, _text): + create_states() + compiler = _compiler filename = _filename title = _title text = _text -func createstates(): +func create_states(): var patterns = {} patterns[Constants.TokenType.Text] = ['.*', 'any text'] @@ -111,8 +113,8 @@ func createstates(): states[BASE].add_transition(Constants.TokenType.TagMarker, TAG, true) states[BASE].add_text_rule(Constants.TokenType.Text) - #TODO: FIXME - Tags are not being proccessed properly this way. We must look for the format #{identifier}:{value} - # Possible solution is to add more transitions + #FIXME - Tags are not being proccessed properly this way. We must look for the format #{identifier}:{value} + # Possible solution is to add more transitions states[TAG] = LexerState.new(patterns) states[TAG].add_transition(Constants.TokenType.Identifier, BASE) @@ -236,7 +238,11 @@ func tokenize(): lines.append('') for line in lines: - tokens += tokenize_line(line, line_number) + var line_tokens = tokenize_line(line, line_number) + if line_tokens == null: + return + + tokens.append_array(line_tokens) line_number += 1 var end_of_input = Token.new( @@ -252,12 +258,12 @@ func tokenize(): func tokenize_line(line, line_number): var token_stack = [] - var fresh_line = line.replace('\t',' ').replace('\r','') + var fresh_line = line.replace('\t', ' ').replace('\r', '') var indentation = line_indentation(line) var previous_indentation = indent_stack.front()[0] - if should_track_indent && indentation > previous_indentation: + if should_track_indent and indentation > previous_indentation: indent_stack.push_front([indentation, true]) var indent = Token.new( @@ -284,7 +290,7 @@ func tokenize_line(line, line_number): whitespace.compile(WHITESPACE) while column < fresh_line.length(): - if fresh_line.substr(column).begins_with(LINE_COMENT): + if fresh_line.substr(column).begins_with(LINE_COMMENT): break var matched = false @@ -367,12 +373,12 @@ func tokenize_line(line, line_number): line_number, column ] - assert(false, 'Expected %s in file %s in node "%s" on line #%d (column #%d)' % error_data) + compiler.assert(false, 'Expected %s in file %s in node "%s" on line #%d (column #%d)' % error_data, line_number, column) + return var last_whitespace = whitespace.search(line, column) if last_whitespace: column += last_whitespace.get_string().length() - token_stack.invert() diff --git a/addons/Wol/core/compiler/parser.gd b/addons/Wol/core/compiler/parser.gd index 884f9ba..b4385eb 100644 --- a/addons/Wol/core/compiler/parser.gd +++ b/addons/Wol/core/compiler/parser.gd @@ -1,13 +1,17 @@ extends Object +# warnings-disable + const Constants = preload('res://addons/Wol/core/Constants.gd') const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') const Value = preload('res://addons/Wol/core/Value.gd') -var tokens = [] +var compiler var title = '' +var tokens = [] -func _init(_title, _tokens): +func _init(_compiler, _title, _tokens): + compiler = _compiler title = _title tokens = _tokens @@ -21,13 +25,9 @@ func parse_node(): return WolNode.new('Start', null, self) func next_symbol_is(valid_types): - var type = self.tokens.front().type - for valid_type in valid_types: - if type == valid_type: - return true - return false + return tokens.front().type in valid_types -# NOTE:0 look ahead for `<<` and `else` +# NOTE: 0 look ahead for `<<` and `else` func next_symbols_are(valid_types): var temporary = [] + tokens for type in valid_types: @@ -40,7 +40,7 @@ func expect_symbol(token_types = []): if token_types.size() == 0: if token.type == Constants.TokenType.EndOfInput: - assert(false, 'Unexpected end of input') + compiler.assert(false, 'Unexpected end of input') return token for type in token_types: @@ -60,15 +60,12 @@ func expect_symbol(token_types = []): error_guess = '' var error_data = [ - token.filename, - title, - token.line_number, - token.column, PoolStringArray(token_names).join(', '), Constants.token_type_name(token.type), error_guess ] - assert(false, '[%s|%s:%d:%d]:\nExpected token "%s" but got "%s"%s' % error_data) + compiler.assert(false, 'Expected token "%s" but got "%s"%s' % error_data, token.line_number, token.column) + return token static func tab(indent_level, input, newline = true): return '%*s| %s%s' % [indent_level * 2, '', input, '' if not newline else '\n'] @@ -111,11 +108,21 @@ class WolNode extends ParseNode: var editor_node_tags = [] var statements = [] + var parser - func _init(name, parent, parser).(parent, parser): - self.name = name + func _init(_name, parent, _parser).(parent, _parser): + name = _name + parser = _parser while parser.tokens.size() > 0 \ and not parser.next_symbol_is([Constants.TokenType.Dedent, Constants.TokenType.EndOfInput]): + + parser.compiler.assert( + not parser.next_symbol_is([Constants.TokenType.Indent]), + 'Found a stray indentation!', + parser.tokens.front().line_number, + parser.tokens.front().column + ) + statements.append(Statement.new(self, parser)) func tree_string(indent_level): @@ -125,109 +132,6 @@ class WolNode extends ParseNode: return PoolStringArray(info).join('') -# TODO: Evaluate use -class Header extends ParseNode: - pass - -class InlineExpression extends ParseNode: - var expression - - func _init(parent, parser).(parent, parser): - parser.expect_symbol([Constants.TokenType.ExpressionFunctionStart]) - expression = ExpressionNode.parse(self, parser) - parser.expect_symbol([Constants.TokenType.ExpressionFunctionEnd]) - - static func can_parse(parser): - return parser.next_symbol_is([Constants.TokenType.ExpressionFunctionStart]) - - func tree_string(_indent_level): - return "InlineExpression:" - -# Returns a format_text string as [ name "{0}" key1="value1" key2="value2" ] -class FormatFunctionNode extends ParseNode: - var format_text = '' - var expression_value - - func _init(parent:ParseNode, parser, expressionCount:int).(parent, parser): - format_text="[" - parser.expect_symbol([Constants.TokenType.FormatFunctionStart]) - - while not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): - if parser.next_symbol_is([Constants.TokenType.Text]): - format_text += parser.expect_symbol().value - - if InlineExpression.can_parse(parser): - expression_value = InlineExpression.new(self, parser) - format_text +=" \"{%d}\" " % expressionCount - parser.expect_symbol() - format_text+="]" - - static func can_parse(parser): - return parser.next_symbol_is([Constants.TokenType.FormatFunctionStart]) - - # TODO: Make format prettier and add more information - func tree_string(_indent_level): - return "FormatFunction" - -class LineNode extends ParseNode: - var line_text = '' - # FIXME: Right now we are putting the formatfunctions and inline expressions in the same - # list but if at some point we want to stronly type our sub list we need to make a new - # parse node that can have either an InlineExpression or a FunctionFormat - # .. This is a consideration for Godot4.x - var substitutions = [] - var line_id = '' - var line_tags = [] - - # NOTE: If format function an inline functions are both present - # returns a line in the format "Some text {0} and some other {1}[format "{2}" key="value" key="value"]" - - func _init(parent, parser).(parent, parser): - while parser.next_symbol_is( - [ - Constants.TokenType.FormatFunctionStart, - Constants.TokenType.ExpressionFunctionStart, - Constants.TokenType.Text, - Constants.TokenType.TagMarker - ] - ): - - if FormatFunctionNode.can_parse(parser): - var format_function = FormatFunctionNode.new(self, parser, substitutions.size()) - if format_function.expression_value != null: - substitutions.append(format_function.expression_value) - - line_text += format_function.format_text - - elif InlineExpression.can_parse(parser): - var inline_expression = InlineExpression.new(self, parser) - line_text += '{%d}' % substitutions.size() - substitutions.append(inline_expression) - - elif parser.next_symbols_are([Constants.TokenType.TagMarker, Constants.TokenType.Identifier]): - parser.expect_symbol() - var tag_token = parser.expect_symbol([ Constants.TokenType.Identifier ]) - if tag_token.value.begins_with("line:"): - if line_id.empty(): - line_id = tag_token.value - else: - printerr("Too many line_tags @[%s:%d]" %[parser.currentNodeName, tag_token.line_number]) - return - else: - tags.append(tag_token.value) - - else: - var token = parser.expect_symbol() - if token.line_number == line_number and token.type != Constants.TokenType.BeginCommand: - line_text += token.value - else: - parser.tokens.push_front(token) - break - - - func tree_string(indent_level): - return tab(indent_level, 'Line: (%s)[%d]' % [line_text, substitutions.size()]) - class Statement extends ParseNode: var Type = Constants.StatementTypes @@ -270,7 +174,7 @@ class Statement extends ParseNode: type = Type.Line else: - printerr('Expected a statement but got %s instead. (probably an imbalanced if statement)' % parser.tokens.front()._to_string()) + parser.compiler.assert(false, 'Expected a statement but got %s instead. (probably an imbalanced if statement)' % parser.tokens.front()._to_string()) var tags = [] @@ -305,6 +209,107 @@ class Statement extends ParseNode: return PoolStringArray(info).join('') +class InlineExpression extends ParseNode: + var expression + + func _init(parent, parser).(parent, parser): + parser.expect_symbol([Constants.TokenType.ExpressionFunctionStart]) + expression = ExpressionNode.parse(self, parser) + parser.expect_symbol([Constants.TokenType.ExpressionFunctionEnd]) + + static func can_parse(parser): + return parser.next_symbol_is([Constants.TokenType.ExpressionFunctionStart]) + + func tree_string(_indent_level): + return "InlineExpression:" + +# Returns a format_text string as [ name "{0}" key1="value1" key2="value2" ] +class FormatFunctionNode extends ParseNode: + var format_text = '' + var expression_value + + func _init(parent:ParseNode, parser, expressionCount:int).(parent, parser): + format_text="[" + parser.expect_symbol([Constants.TokenType.FormatFunctionStart]) + + # FIXME: Add exit condition in case of failure + while not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): + if parser.next_symbol_is([Constants.TokenType.Text]): + format_text += parser.expect_symbol().value + + if InlineExpression.can_parse(parser): + expression_value = InlineExpression.new(self, parser) + format_text +=" \"{%d}\" " % expressionCount + parser.expect_symbol() + format_text+="]" + + static func can_parse(parser): + return parser.next_symbol_is([Constants.TokenType.FormatFunctionStart]) + + # TODO: Make format prettier and add more information + func tree_string(_indent_level): + return "FormatFunction" + +class LineNode extends ParseNode: + var line_text = '' + # FIXME: Right now we are putting the formatfunctions and inline expressions in the same + # list but if at some point we want to strongly type our sub list we need to make a new + # parse node that can have either an InlineExpression or a FunctionFormat + # .. This is a consideration for Godot4.x + var substitutions = [] + var line_id = '' + var line_tags = [] + + # NOTE: If format function an inline functions are both present + # returns a line in the format "Some text {0} and some other {1}[format "{2}" key="value" key="value"]" + + func _init(parent, parser).(parent, parser): + while parser.next_symbol_is( + [ + Constants.TokenType.FormatFunctionStart, + Constants.TokenType.ExpressionFunctionStart, + Constants.TokenType.Text, + Constants.TokenType.TagMarker + ] + ): + + if FormatFunctionNode.can_parse(parser): + var format_function = FormatFunctionNode.new(self, parser, substitutions.size()) + if format_function.expression_value != null: + substitutions.append(format_function.expression_value) + + line_text += format_function.format_text + + elif InlineExpression.can_parse(parser): + var inline_expression = InlineExpression.new(self, parser) + line_text += '{%d}' % substitutions.size() + substitutions.append(inline_expression) + + elif parser.next_symbols_are([Constants.TokenType.TagMarker, Constants.TokenType.Identifier]): + parser.expect_symbol() + var tag_token = parser.expect_symbol([ Constants.TokenType.Identifier ]) + if tag_token.value.begins_with("line:"): + if line_id.empty(): + line_id = tag_token.value + else: + printerr("Too many line_tags @[%s:%d]" % [parser.currentNodeName, tag_token.line_number]) + return + else: + tags.append(tag_token.value) + + else: + var token = parser.expect_symbol() + if token.line_number == line_number and token.type != Constants.TokenType.BeginCommand: + line_text += token.value + else: + parser.tokens.push_front(token) + break + + + func tree_string(indent_level): + return tab(indent_level, 'Line: (%s)[%d]' % [line_text, substitutions.size()]) + + class CustomCommand extends ParseNode: enum Type { @@ -322,6 +327,7 @@ class CustomCommand extends ParseNode: var command_tokens = [] command_tokens.append(parser.expect_symbol()) + # FIXME: add exit condition while not parser.next_symbol_is([Constants.TokenType.EndCommand]): command_tokens.append(parser.expect_symbol()) @@ -338,9 +344,9 @@ class CustomCommand extends ParseNode: type = Type.Expression else: - #otherwise evaluuate command + # otherwise evaluate command type = Type.ClientCommand - self.client_command = command_tokens[0].value + client_command = command_tokens[0].value func tree_string(indent_level): match type: @@ -362,9 +368,7 @@ class ShortcutOptionGroup extends ParseNode: # parse options until there is no more # expect one otherwise invalid - var index = 1 - options.append(ShortCutOption.new(index, self, parser)) - index += 1 + var index = 0 while parser.next_symbol_is([Constants.TokenType.ShortcutOption]): options.append(ShortCutOption.new(index, self, parser)) index += 1 @@ -393,7 +397,7 @@ class ShortCutOption extends ParseNode: parser.expect_symbol([Constants.TokenType.ShortcutOption]) label = parser.expect_symbol([Constants.TokenType.Text]).value - # NOTE: Parse the conditional << if $x >> when it exists + # FIXME: Parse the conditional << if $x >> when it exists var tags = [] while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) \ or parser.next_symbol_is([Constants.TokenType.TagMarker]): @@ -446,6 +450,7 @@ class Block extends ParseNode: parser.expect_symbol([Constants.TokenType.Indent]) #keep reading statements until we hit a dedent + # FIXME: find exit condition while not parser.next_symbol_is([Constants.TokenType.Dedent]): #parse all statements including nested blocks statements.append(Statement.new(self, parser)) @@ -669,6 +674,8 @@ class ExpressionNode extends ParseNode: # Using Djikstra's shunting-yard algorithm to convert stream of expresions into postfix notation, # & then build a tree of expressions + + # TODO: Rework expression parsing static func parse(parent, parser): var rpn = [] var op_stack = [] @@ -693,20 +700,30 @@ class ExpressionNode extends ParseNode: var last + print(parser.tokens.slice(0, 6)) #read expression content while parser.tokens.size() > 0 and parser.next_symbol_is(valid_types): + print(parser.tokens.front()) var next = parser.expect_symbol(valid_types) - if next.type == Constants.TokenType.Variable \ - or next.type == Constants.TokenType.Number \ - or next.type == Constants.TokenType.Str \ - or next.type == Constants.TokenType.FalseToken \ - or next.type == Constants.TokenType.TrueToken \ - or next.type == Constants.TokenType.NullToken: - - #output primitives - rpn.append(next) + if next.type in [ + Constants.TokenType.Variable, + Constants.TokenType.Number, + Constants.TokenType.Str, + Constants.TokenType.FalseToken, + Constants.TokenType.TrueToken, + Constants.TokenType.NullToken + ]: + + # Output primitives + print('adding value "%s" in expression' % next) + print(op_stack.size()) + if func_stack.size() != 0: + op_stack.append(next) + else: + rpn.append(next) elif next.type == Constants.TokenType.Identifier: + print('adding function') op_stack.push_back(next) func_stack.push_back(next) @@ -719,7 +736,7 @@ class ExpressionNode extends ParseNode: while op_stack.back().type != Constants.TokenType.LeftParen: var p = op_stack.pop_back() if p == null: - printerr('unbalanced parenthesis %s ' % next.name) + printerr('unbalanced parenthesis %s' % next.name) break rpn.append(p) @@ -732,7 +749,7 @@ class ExpressionNode extends ParseNode: #find the closest function on stack #increment parameters - func_stack.back().parameter_count+=1 + func_stack.back().parameter_count += 1 elif Operator.is_op(next.type): #this is an operator @@ -759,7 +776,7 @@ class ExpressionNode extends ParseNode: next.type = Constants.TokenType.EqualTo #operator precedence - while (ExpressionNode.is_apply_precedence(next.type, op_stack)): + while ExpressionNode.is_apply_precedence(next.type, op_stack, parser): var op = op_stack.pop_back() rpn.append(op) @@ -771,20 +788,28 @@ class ExpressionNode extends ParseNode: elif next.type == Constants.TokenType.RightParen: #leaving sub expression # resolve order of operations + var parameters = [] while op_stack.back().type != Constants.TokenType.LeftParen: - rpn.append(op_stack.pop_back()) - if op_stack.back() == null: - printerr('Unbalanced parenthasis #RightParen. Parser.ExpressionNode') + parameters.append(op_stack.pop_back()) + + parser.compiler.assert( + op_stack.back() != null, + 'Unbalanced parenthasis #RightParen. Parser.ExpressionNode' + ) + rpn.append_array(parameters) op_stack.pop_back() + # FIXME: Something is going on with parameter counting, fixed for now + # but needs a bigger rework if op_stack.back().type == Constants.TokenType.Identifier: #function call #last token == left paren this == no parameters #else #we have more than 1 param - if last.type != Constants.TokenType.LeftParen: - func_stack.back().parameter_count+=1 + # if last.type != Constants.TokenType.LeftParen: + # func_stack.back().parameter_count += 1 + func_stack.back().parameter_count = parameters.size() rpn.append(op_stack.pop_back()) func_stack.pop_back() @@ -804,6 +829,8 @@ class ExpressionNode extends ParseNode: var first = rpn.front() var eval_stack = []#ExpressionNode + print(rpn) + while rpn.size() > 0: var next = rpn.pop_front() if Operator.is_op(next.type): @@ -811,7 +838,14 @@ class ExpressionNode extends ParseNode: var info = Operator.op_info(next.type) if eval_stack.size() < info.arguments: - printerr('Error parsing : Not enough arguments for %s [ got %s expected - was %s]'%[Constants.token_type_name(next.type), eval_stack.size(), info.arguments]) + printerr( + 'Error parsing : Not enough arguments for %s [ got %s expected - was %s]' \ + % [ + Constants.token_type_name(next.type), + eval_stack.size(), + info.arguments + ] + ) var function_parameters = [] for _i in range(info.arguments): @@ -827,6 +861,7 @@ class ExpressionNode extends ParseNode: # A function call elif next.type == Constants.TokenType.Identifier: var function_name = next.value + prints(function_name, next.parameter_count) var function_parameters = [] for _i in range(next.parameter_count): @@ -845,10 +880,14 @@ class ExpressionNode extends ParseNode: eval_stack.append(expression) - #we should have a single root expression left - #if more then we failed ---- NANI - if eval_stack.size() != 1: - printerr('[%s] Error parsing expression (stack did not reduce correctly )' % first) + # NOTE: We should have a single root expression left + # if more then we failed + parser.compiler.assert( + eval_stack.size() == 1, + '[%s] Error parsing expression (stack did not reduce correctly)' % first, + first.line_number, + first.column + ) return eval_stack.pop_back() @@ -860,12 +899,13 @@ class ExpressionNode extends ParseNode: return key return string - static func is_apply_precedence(_type, operator_stack): + static func is_apply_precedence(_type, operator_stack, parser): if operator_stack.size() == 0: return false if not Operator.is_op(_type): - assert(false, 'Unable to parse expression!') + parser.compiler.assert(false, 'Unable to parse expression!') + return false var second = operator_stack.back().type diff --git a/addons/Wol/core/constants.gd b/addons/Wol/core/constants.gd index c44b85e..c700d85 100644 --- a/addons/Wol/core/constants.gd +++ b/addons/Wol/core/constants.gd @@ -211,3 +211,4 @@ static func token_name(type): if TokenType[key] == type: return key return '' + diff --git a/addons/Wol/core/program.gd b/addons/Wol/core/program.gd index fa94140..4b9d282 100644 --- a/addons/Wol/core/program.gd +++ b/addons/Wol/core/program.gd @@ -29,7 +29,6 @@ class Line: func _to_string(): return '%s:%d: "%s"' % [file_name.get_file(), line_number, text] - class Option: var line var id = -1 diff --git a/addons/Wol/editor/Editor.gd b/addons/Wol/editor/Editor.gd new file mode 100644 index 0000000..3b1a108 --- /dev/null +++ b/addons/Wol/editor/Editor.gd @@ -0,0 +1,56 @@ +tool +extends Panel + +var current_node +var current_graph_node + +func _ready(): + hide() + connect('visibility_changed', self, '_on_visibility_changed') + $Close.connect('pressed', self, 'close') + +func close(): + hide() + +func open_node(graph_node, node): + current_node = node + current_graph_node = graph_node + + var text_edit = graph_node.get_node('TextEdit') + text_edit.get_parent().remove_child(text_edit) + $Content.add_child(text_edit) + toggle_text_edit(text_edit) + + show() + + # window_title = node.title + +func toggle_text_edit(text_edit): + text_edit.anchor_left = 0 + text_edit.anchor_top = 0 + text_edit.anchor_bottom = 1 + text_edit.anchor_right = 1 + text_edit.margin_left = 0 + text_edit.margin_right = 0 + text_edit.margin_bottom = 0 + text_edit.margin_top = 0 + text_edit.mouse_filter = MOUSE_FILTER_STOP if text_edit.get_parent().name == 'Content' else MOUSE_FILTER_IGNORE + + text_edit.deselect() + + for property in [ + 'highlight_current_line', + 'show_line_numbers', + 'draw_tabs', + 'smooth_scrolling', + 'wrap_enabled', + 'minimap_draw' + ]: + text_edit.set(property, not text_edit.get(property)) + +func _on_visibility_changed(): + if not visible: + var text_edit = $Content/TextEdit + $Content.remove_child(text_edit) + current_graph_node.add_child(text_edit) + toggle_text_edit(text_edit) diff --git a/addons/Wol/editor/GraphNodeTemplate.tscn b/addons/Wol/editor/GraphNodeTemplate.tscn new file mode 100644 index 0000000..9f371a8 --- /dev/null +++ b/addons/Wol/editor/GraphNodeTemplate.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/Wol/editor/WolGraphNode.gd" type="Script" id=1] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_left = 4.0 +content_margin_right = 4.0 +content_margin_top = 26.0 +content_margin_bottom = 4.0 +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[node name="GraphNodeTemplate" type="GraphNode"] +margin_left = 500.0 +margin_top = 1068.0 +margin_right = 937.0 +margin_bottom = 1459.0 +mouse_filter = 1 +custom_styles/frame = SubResource( 1 ) +title = "Hello world" +offset = Vector2( 500, 500 ) +resizable = true +slot/0/left_enabled = false +slot/0/left_type = 0 +slot/0/left_color = Color( 1, 1, 1, 1 ) +slot/0/right_enabled = false +slot/0/right_type = 0 +slot/0/right_color = Color( 1, 1, 1, 1 ) +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TextEdit" type="TextEdit" parent="."] +margin_left = 4.0 +margin_top = 26.0 +margin_right = 433.0 +margin_bottom = 387.0 +mouse_filter = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +syntax_highlighting = true +fold_gutter = true +context_menu_enabled = false +virtual_keyboard_enabled = false +wrap_enabled = true diff --git a/addons/Wol/editor/WolEditor.gd b/addons/Wol/editor/WolEditor.gd new file mode 100644 index 0000000..1677b7c --- /dev/null +++ b/addons/Wol/editor/WolEditor.gd @@ -0,0 +1,103 @@ +tool +extends Control + +const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd') +onready var GraphNodeTemplate = $GraphNodeTemplate + +var path +var compiler + +func _ready(): + for menu_button in [$Menu/File]: + menu_button.get_popup().connect('index_pressed', self, '_on_menu_pressed', [menu_button.get_popup()]) + + # TODO: Conditionally load in theme based on Editor or standalone + + path = 'res://dialogue.yarn' + build_nodes() + +func build_nodes(): + compiler = Compiler.new(path) + + for node in compiler.get_nodes(): + var graph_node = GraphNodeTemplate.duplicate() + $GraphEdit.add_child(graph_node) + graph_node.node = node + graph_node.show() + graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node, node]) + +func serialize_to_file(): + var buffer = [] + for graph_node in $GraphEdit.get_children(): + if not graph_node is GraphNode: + continue + + var node = graph_node.node + buffer.append('title: %s' % node.title) + buffer.append('tags: ') + buffer.append('colorID: ') + buffer.append('position: %d, %d' % [node.position.x, node.position.y]) + buffer.append('---') + buffer.append(node.body) + buffer.append('===') + + return PoolStringArray(buffer).join('\n') + +func save_as(file_path = null): + if not file_path: + $FileDialog.mode = $FileDialog.MODE_SAVE_FILE + # TODO: Set up path based on context (Godot editor, standalone or web) + $FileDialog.popup_centered() + file_path = yield($FileDialog, 'file_selected') + + if not file_path: + return + + var file = File.new() + file.open(file_path, File.WRITE) + file.store_string(serialize_to_file()) + file.close() + print('saved file!') + +func open(): + $FileDialog.mode = $FileDialog.MODE_OPEN_FILE + # TODO: Set up path based on context (Godot editor, standalone or web) + $FileDialog.popup_centered() + path = yield($FileDialog, 'file_selected') + if not path: + return + + for node in $GraphEdit.get_children(): + if node is GraphNode: + $GraphEdit.remove_child(node) + node.queue_free() + + yield(get_tree(), 'idle_frame') + build_nodes() + +func new(): + # TODO: add dialog for maybe saving existing file + + for node in $GraphEdit.get_children(): + if node is GraphNode: + $GraphEdit.remove_child(node) + node.queue_free() + + path = null + +func _on_menu_pressed(index, node): + match(node.get_item_text(index)): + 'New': + new() + 'Save': + save_as(path) + 'Save as...': + save_as() + 'Open': + open() + +func _on_graph_node_input(event, graph_node, node): + if event is InputEventMouseButton \ + and event.doubleclick and event.button_index == BUTTON_LEFT: + $HBoxContainer/Editor.open_node(graph_node, node) + accept_event() diff --git a/addons/Wol/editor/WolEditor.tscn b/addons/Wol/editor/WolEditor.tscn new file mode 100644 index 0000000..6fb5aff --- /dev/null +++ b/addons/Wol/editor/WolEditor.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2c2ef9c83d968c8cc99aa62da40477e6f3660aa71f8621e360818cc0307bf0b +size 604323 diff --git a/addons/Wol/editor/WolGraphNode.gd b/addons/Wol/editor/WolGraphNode.gd new file mode 100644 index 0000000..dc584cf --- /dev/null +++ b/addons/Wol/editor/WolGraphNode.gd @@ -0,0 +1,46 @@ +tool +extends GraphNode + +const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd') + +var node setget set_node + +onready var text_edit = $TextEdit + +func _ready(): + connect('offset_changed', self, '_on_offset_changed') + text_edit.connect('text_changed', self, '_on_text_changed') + $TextDebounce.connect('timeout', self, '_on_debounce') + +func _on_text_changed(): + $TextDebounce.start(.3) + +func _on_debounce(): + text_edit.get_node('ErrorGutter').hide() + node.body = text_edit.text + compile() + +func _on_offset_changed(): + node.position = offset + +func _on_error(message, _line_number, _column): + var error_gutter = text_edit.get_node('ErrorGutter') + error_gutter.show() + error_gutter.text = message + + # TODO: Highlight line based on line number and column + +func set_node(_node): + node = _node + title = node.title + text_edit.text = node.body + text_edit.clear_undo_history() + offset = node.position + + compile() + +func compile(): + var text = '---\n%s\n===' % text_edit.text + var compiler = Compiler.new(null, text, true) + compiler.connect('error', self, '_on_error') + compiler.compile() diff --git a/addons/Wol/plugin.gd b/addons/Wol/plugin.gd index fb9984d..ed5a6cb 100644 --- a/addons/Wol/plugin.gd +++ b/addons/Wol/plugin.gd @@ -1,6 +1,10 @@ tool extends EditorPlugin +const WolEditor = preload('res://addons/Wol/editor/WolEditor.tscn') + +var wol_editor_instance + func _enter_tree(): add_custom_type( 'Wol', @@ -9,5 +13,27 @@ func _enter_tree(): load('res://addons/Wol/icon-white.svg') ) + wol_editor_instance = WolEditor.instance() + get_editor_interface().get_editor_viewport().add_child(wol_editor_instance) + + make_visible(false) + +func make_visible(visible): + if wol_editor_instance: + wol_editor_instance.visible = visible + func _exit_tree(): remove_custom_type('Wol') + + if wol_editor_instance: + wol_editor_instance.queue_free() + +func has_main_screen(): + return true + +func get_plugin_name(): + return 'Wol' + +func get_plugin_icon(): + print(get_editor_interface().get_base_control().get_icon('Node', 'EditorIcons')) + return get_editor_interface().get_base_control().get_icon('Node', 'EditorIcons') diff --git a/dialogue.yarn b/dialogue.yarn index d34b431..4faa15e 100644 --- a/dialogue.yarn +++ b/dialogue.yarn @@ -1,54 +1,54 @@ title: Start tags: -colorID: 0 +colorID: position: 0, 0 --- <> <> // remove "to" to trigger error -<> +<> <> -// Implement inline expressions +// Implement inline expressions <> -Narrator: You, {$direction} way! +Narrator: You, {$direction} way! <> Narrator: Do you know you've been here {visit_count()} times? You: Did you know one + one equals {$one + $one}? Narrator: You wanna go somewhere? - -> Go to the store - [[TheStore]] -// -> Wait, how many times have I been to the store? -// Narrator: You've been to the store {visit_count('TheStore')} times. -// [[Start]] +-> Go to the store + [[TheStore]] +-> How much did I visit the store? + Narrator: You've been to the store { visit_count("TheStore") } times. + [[Start]] -> Lets stay here and talk [[Talk]] === title: TheStore tags: -colorID: 0 -position: 0, 200 +colorID: +position: 400, 200 --- Guy: Hey what's up I need your help can you come here? You: Well I can't I'm buying clothes. -All right well hurry up and come over here. +Guy: All right well hurry up and come over here. You: I can't find them. Guy: What do you mean you can't find them? You: I can't find them there's only soup. -Guy: What do you mean there's only soup?! +Guy: What do you mean there's only soup?! You: It means there's only soup. Guy: WELL THEN GET OUT OF THE SOUP ISLE!! You: Alright you dont have to shout at me! You: There's more soup. -Guy: What do you mean there's more soup? +Guy: What do you mean there's more soup? You: There's just more soup. Guy: Then go to the next aisle! -You: There's still soup! +You: There's still soup! Guy: Where are you right now?! You: I'm at soup! -Guy: What do you mean you're at soup?! +Guy: What do you mean you're at soup?! You: I mean I'm at soup. Guy: WHAT STORE ARE YOU IN?! You: IM AT THE SOUP STORE!! @@ -58,8 +58,8 @@ You: FUCK YOU! === title: Talk tags: -colorID: 0 -position: 0, 400 +colorID: +position: 800, 400 --- Narrator: So how are you really? You: I'm good! diff --git a/project.godot b/project.godot index a351bd7..e6a9e01 100644 --- a/project.godot +++ b/project.godot @@ -24,6 +24,11 @@ gdscript/warnings/return_value_discarded=false enabled=PoolStringArray( "res://addons/Wol/plugin.cfg" ) +[network] + +limits/debugger_stdout/max_chars_per_second=8096 +limits/debugger_stdout/max_messages_per_frame=200 + [physics] common/enable_pause_aware_picking=true