made several fixes and started work on editor

This commit is contained in:
Bram Dingelstad 2021-12-06 17:31:55 +01:00
parent 0fd71a9940
commit 4cb93851b1
18 changed files with 640 additions and 241 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
/addons/Wol/editor/WolEditor.tscn filter=lfs diff=lfs merge=lfs -text

View file

@ -8,17 +8,20 @@ func _ready():
$Ship/DialogueStarter.connect('body_entered', self, '_on_player_near_dialogue', [$Ship, true]) $Ship/DialogueStarter.connect('body_entered', self, '_on_player_near_dialogue', [$Ship, true])
$Ship/DialogueStarter.connect('body_exited', self, '_on_player_near_dialogue', [$Ship, false]) $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): func _on_player_near_dialogue(_player, node, entered):
print('body entered?', entered)
if entered: if entered:
current_dialogue = node.name current_dialogue = node.name
else: else:
current_dialogue = null current_dialogue = null
func _on_finished():
$DialogueCooldown.start()
func _process(_delta): func _process(_delta):
if Input.is_action_just_released('ui_accept') and current_dialogue and not $Dialogue/Wol.running: if Input.is_action_just_released('ui_accept') \
print(current_dialogue) and current_dialogue and not $Dialogue/Wol.running and $DialogueCooldown.time_left == 0:
$Dialogue/Wol.starting_node = current_dialogue $Dialogue/Wol.starting_node = current_dialogue
$Dialogue/Wol.path = 'res://ExampleDialogue/%s.yarn' % current_dialogue $Dialogue/Wol.path = 'res://ExampleDialogue/%s.yarn' % current_dialogue
$Dialogue/Wol.start() $Dialogue/Wol.start()
print($Dialogue/Wol.variable_storage)

View file

@ -230,3 +230,7 @@ __meta__ = {
[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"] [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 ) 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

View file

@ -79,7 +79,11 @@ func node_visit_count(node = virtual_machine.current_node.name):
if node is Value: if node is Value:
node = virtual_machine.program.strings[node.value()].text node = virtual_machine.program.strings[node.value()].text
var variable_storage = virtual_machine.dialogue.variable_storage var variable_storage = virtual_machine.dialogue.variable_storage
var visited_node_count = variable_storage[virtual_machine.program.filename] 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 return visited_node_count[node] if visited_node_count.has(node) else 0

View file

@ -68,7 +68,7 @@ func set_node(name):
dialogue.variable_storage[program.filename] = {} dialogue.variable_storage[program.filename] = {}
if not dialogue.variable_storage[program.filename].has(name): if not dialogue.variable_storage[program.filename].has(name):
dialogue.variable_storage[program.filename][name] = 0 dialogue.variable_storage[program.filename][name] = 1
else: else:
dialogue.variable_storage[program.filename][name] += 1 dialogue.variable_storage[program.filename][name] += 1
return true return true

View file

@ -1,5 +1,7 @@
extends Object extends Object
signal error(message, line_number, column)
const Constants = preload('res://addons/Wol/core/Constants.gd') const Constants = preload('res://addons/Wol/core/Constants.gd')
const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd')
const Program = preload('res://addons/Wol/core/Program.gd') const Program = preload('res://addons/Wol/core/Program.gd')
@ -12,13 +14,15 @@ var filename = ''
var current_node var current_node
var has_implicit_string_tags = false var has_implicit_string_tags = false
var soft_assert = false
var string_count = 0 var string_count = 0
var string_table = {} var string_table = {}
var label_count = 0 var label_count = 0
func _init(_filename, _source = null): func _init(_filename, _source = null, _soft_assert = false):
filename = _filename filename = _filename
soft_assert = _soft_assert
if not _filename and _source: if not _filename and _source:
self.source = _source self.source = _source
@ -28,32 +32,33 @@ func _init(_filename, _source = null):
self.source = file.get_as_text() self.source = file.get_as_text()
file.close() file.close()
func compile(): var source_lines = source.split('\n')
var header_sep = RegEx.new()
var header_property = RegEx.new()
header_sep.compile('---(\r\n|\r|\n)')
header_property.compile('(?<field>.*): *(?<value>.*)')
assert(header_sep.search(source), 'No headers found!')
var line_number = 0
var source_lines = source.split('\n', false)
for i in range(source_lines.size()): for i in range(source_lines.size()):
source_lines[i] = source_lines[i].strip_edges(false, true) source_lines[i] = source_lines[i].strip_edges(false, true)
var parsed_nodes = [] source = source_lines.join('\n')
while line_number < source_lines.size():
var title = ''
var body = ''
# Parse header func get_headers(offset = 0):
while true: var header_property = RegEx.new()
var header_sep = RegEx.new()
header_sep.compile('---(\r\n|\r|\n)')
header_property.compile('(?<field>.*): *(?<value>.*)')
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 line = source_lines[line_number] var line = source_lines[line_number]
line_number += 1 line_number += 1
if not line.empty(): if not line.empty():
var result = header_property.search(line) var result = header_property.search(line)
if result != null: if result != null:
var field = result.get_string('field') var field = result.get_string('field')
var value = result.get_string('value') var value = result.get_string('value')
@ -61,37 +66,86 @@ func compile():
if field == 'title': if field == 'title':
var regex = RegEx.new() var regex = RegEx.new()
regex.compile(INVALID_TITLE) regex.compile(INVALID_TITLE)
assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)]) self.assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)])
title = value title = value
# TODO: Implement position, color and tags
if line_number >= source_lines.size() or line == '---': 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 break
# past header return {
'title': title,
'position': position
}
func get_body(offset = 0):
var body_lines = [] 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] != '===': while line_number < source_lines.size() and source_lines[line_number] != '===':
if recording:
body_lines.append(source_lines[line_number]) body_lines.append(source_lines[line_number])
recording = recording or source_lines[line_number] == '---'
line_number += 1 line_number += 1
line_number += 1 line_number += 1
body = PoolStringArray(body_lines).join('\n') return PoolStringArray(body_lines).join('\n')
var lexer = Lexer.new(filename, title, body)
var tokens = lexer.tokenize()
var parser = Parser.new(title, tokens) func get_nodes():
var parser_node = parser.parse_node() 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
parser_node.name = title nodes.append(headers)
# parser_node.tags = title
parsed_nodes.append(parser_node) # 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(): while line_number < source_lines.size() and source_lines[line_number].empty():
line_number += 1 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() var program = Program.new()
program.filename = filename program.filename = filename
@ -104,7 +158,7 @@ func compile():
return program return program
func compile_node(program, parsed_node): 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() var node_compiled = Program.WolNode.new()
@ -205,7 +259,7 @@ func generate_statement(node, statement):
Constants.StatementTypes.Line: Constants.StatementTypes.Line:
generate_line(node, statement) 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): func generate_custom_command(node, command):
# TODO: See if the first tree of this statement is being used # TODO: See if the first tree of this statement is being used

View file

@ -2,7 +2,7 @@ extends Object
const Constants = preload('res://addons/Wol/core/Constants.gd') const Constants = preload('res://addons/Wol/core/Constants.gd')
const LINE_COMENT = '//' const LINE_COMMENT = '//'
const FORWARD_SLASH = '/' const FORWARD_SLASH = '/'
const LINE_SEPARATOR = '\n' const LINE_SEPARATOR = '\n'
@ -22,6 +22,7 @@ const FORMAT_FUNCTION = 'format'
var WHITESPACE = '\\s*' var WHITESPACE = '\\s*'
var compiler
var filename = '' var filename = ''
var title = '' var title = ''
var text = '' var text = ''
@ -33,14 +34,15 @@ var current_state
var indent_stack = [] var indent_stack = []
var should_track_indent = false var should_track_indent = false
func _init(_filename, _title, _text): func _init(_compiler, _filename, _title, _text):
createstates() create_states()
compiler = _compiler
filename = _filename filename = _filename
title = _title title = _title
text = _text text = _text
func createstates(): func create_states():
var patterns = {} var patterns = {}
patterns[Constants.TokenType.Text] = ['.*', 'any text'] patterns[Constants.TokenType.Text] = ['.*', 'any text']
@ -111,7 +113,7 @@ func createstates():
states[BASE].add_transition(Constants.TokenType.TagMarker, TAG, true) states[BASE].add_transition(Constants.TokenType.TagMarker, TAG, true)
states[BASE].add_text_rule(Constants.TokenType.Text) 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} #FIXME - Tags are not being proccessed properly this way. We must look for the format #{identifier}:{value}
# Possible solution is to add more transitions # Possible solution is to add more transitions
states[TAG] = LexerState.new(patterns) states[TAG] = LexerState.new(patterns)
states[TAG].add_transition(Constants.TokenType.Identifier, BASE) states[TAG].add_transition(Constants.TokenType.Identifier, BASE)
@ -236,7 +238,11 @@ func tokenize():
lines.append('') lines.append('')
for line in lines: 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 line_number += 1
var end_of_input = Token.new( var end_of_input = Token.new(
@ -252,12 +258,12 @@ func tokenize():
func tokenize_line(line, line_number): func tokenize_line(line, line_number):
var token_stack = [] 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 indentation = line_indentation(line)
var previous_indentation = indent_stack.front()[0] 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]) indent_stack.push_front([indentation, true])
var indent = Token.new( var indent = Token.new(
@ -284,7 +290,7 @@ func tokenize_line(line, line_number):
whitespace.compile(WHITESPACE) whitespace.compile(WHITESPACE)
while column < fresh_line.length(): while column < fresh_line.length():
if fresh_line.substr(column).begins_with(LINE_COMENT): if fresh_line.substr(column).begins_with(LINE_COMMENT):
break break
var matched = false var matched = false
@ -367,13 +373,13 @@ func tokenize_line(line, line_number):
line_number, line_number,
column 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) var last_whitespace = whitespace.search(line, column)
if last_whitespace: if last_whitespace:
column += last_whitespace.get_string().length() column += last_whitespace.get_string().length()
token_stack.invert() token_stack.invert()
return token_stack return token_stack

View file

@ -1,13 +1,17 @@
extends Object extends Object
# warnings-disable
const Constants = preload('res://addons/Wol/core/Constants.gd') const Constants = preload('res://addons/Wol/core/Constants.gd')
const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd')
const Value = preload('res://addons/Wol/core/Value.gd') const Value = preload('res://addons/Wol/core/Value.gd')
var tokens = [] var compiler
var title = '' var title = ''
var tokens = []
func _init(_title, _tokens): func _init(_compiler, _title, _tokens):
compiler = _compiler
title = _title title = _title
tokens = _tokens tokens = _tokens
@ -21,13 +25,9 @@ func parse_node():
return WolNode.new('Start', null, self) return WolNode.new('Start', null, self)
func next_symbol_is(valid_types): func next_symbol_is(valid_types):
var type = self.tokens.front().type return tokens.front().type in valid_types
for valid_type in valid_types:
if type == valid_type:
return true
return false
# NOTE:0 look ahead for `<<` and `else` # NOTE: 0 look ahead for `<<` and `else`
func next_symbols_are(valid_types): func next_symbols_are(valid_types):
var temporary = [] + tokens var temporary = [] + tokens
for type in valid_types: for type in valid_types:
@ -40,7 +40,7 @@ func expect_symbol(token_types = []):
if token_types.size() == 0: if token_types.size() == 0:
if token.type == Constants.TokenType.EndOfInput: if token.type == Constants.TokenType.EndOfInput:
assert(false, 'Unexpected end of input') compiler.assert(false, 'Unexpected end of input')
return token return token
for type in token_types: for type in token_types:
@ -60,15 +60,12 @@ func expect_symbol(token_types = []):
error_guess = '' error_guess = ''
var error_data = [ var error_data = [
token.filename,
title,
token.line_number,
token.column,
PoolStringArray(token_names).join(', '), PoolStringArray(token_names).join(', '),
Constants.token_type_name(token.type), Constants.token_type_name(token.type),
error_guess 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): static func tab(indent_level, input, newline = true):
return '%*s| %s%s' % [indent_level * 2, '', input, '' if not newline else '\n'] 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 editor_node_tags = []
var statements = [] var statements = []
var parser
func _init(name, parent, parser).(parent, parser): func _init(_name, parent, _parser).(parent, _parser):
self.name = name name = _name
parser = _parser
while parser.tokens.size() > 0 \ while parser.tokens.size() > 0 \
and not parser.next_symbol_is([Constants.TokenType.Dedent, Constants.TokenType.EndOfInput]): 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)) statements.append(Statement.new(self, parser))
func tree_string(indent_level): func tree_string(indent_level):
@ -125,109 +132,6 @@ class WolNode extends ParseNode:
return PoolStringArray(info).join('') 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: class Statement extends ParseNode:
var Type = Constants.StatementTypes var Type = Constants.StatementTypes
@ -270,7 +174,7 @@ class Statement extends ParseNode:
type = Type.Line type = Type.Line
else: 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 = [] var tags = []
@ -305,6 +209,107 @@ class Statement extends ParseNode:
return PoolStringArray(info).join('') 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: class CustomCommand extends ParseNode:
enum Type { enum Type {
@ -322,6 +327,7 @@ class CustomCommand extends ParseNode:
var command_tokens = [] var command_tokens = []
command_tokens.append(parser.expect_symbol()) command_tokens.append(parser.expect_symbol())
# FIXME: add exit condition
while not parser.next_symbol_is([Constants.TokenType.EndCommand]): while not parser.next_symbol_is([Constants.TokenType.EndCommand]):
command_tokens.append(parser.expect_symbol()) command_tokens.append(parser.expect_symbol())
@ -338,9 +344,9 @@ class CustomCommand extends ParseNode:
type = Type.Expression type = Type.Expression
else: else:
#otherwise evaluuate command # otherwise evaluate command
type = Type.ClientCommand type = Type.ClientCommand
self.client_command = command_tokens[0].value client_command = command_tokens[0].value
func tree_string(indent_level): func tree_string(indent_level):
match type: match type:
@ -362,9 +368,7 @@ class ShortcutOptionGroup extends ParseNode:
# parse options until there is no more # parse options until there is no more
# expect one otherwise invalid # expect one otherwise invalid
var index = 1 var index = 0
options.append(ShortCutOption.new(index, self, parser))
index += 1
while parser.next_symbol_is([Constants.TokenType.ShortcutOption]): while parser.next_symbol_is([Constants.TokenType.ShortcutOption]):
options.append(ShortCutOption.new(index, self, parser)) options.append(ShortCutOption.new(index, self, parser))
index += 1 index += 1
@ -393,7 +397,7 @@ class ShortCutOption extends ParseNode:
parser.expect_symbol([Constants.TokenType.ShortcutOption]) parser.expect_symbol([Constants.TokenType.ShortcutOption])
label = parser.expect_symbol([Constants.TokenType.Text]).value 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 = [] var tags = []
while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) \ while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) \
or parser.next_symbol_is([Constants.TokenType.TagMarker]): or parser.next_symbol_is([Constants.TokenType.TagMarker]):
@ -446,6 +450,7 @@ class Block extends ParseNode:
parser.expect_symbol([Constants.TokenType.Indent]) parser.expect_symbol([Constants.TokenType.Indent])
#keep reading statements until we hit a dedent #keep reading statements until we hit a dedent
# FIXME: find exit condition
while not parser.next_symbol_is([Constants.TokenType.Dedent]): while not parser.next_symbol_is([Constants.TokenType.Dedent]):
#parse all statements including nested blocks #parse all statements including nested blocks
statements.append(Statement.new(self, parser)) 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, # Using Djikstra's shunting-yard algorithm to convert stream of expresions into postfix notation,
# & then build a tree of expressions # & then build a tree of expressions
# TODO: Rework expression parsing
static func parse(parent, parser): static func parse(parent, parser):
var rpn = [] var rpn = []
var op_stack = [] var op_stack = []
@ -693,20 +700,30 @@ class ExpressionNode extends ParseNode:
var last var last
print(parser.tokens.slice(0, 6))
#read expression content #read expression content
while parser.tokens.size() > 0 and parser.next_symbol_is(valid_types): while parser.tokens.size() > 0 and parser.next_symbol_is(valid_types):
print(parser.tokens.front())
var next = parser.expect_symbol(valid_types) var next = parser.expect_symbol(valid_types)
if next.type == Constants.TokenType.Variable \ if next.type in [
or next.type == Constants.TokenType.Number \ Constants.TokenType.Variable,
or next.type == Constants.TokenType.Str \ Constants.TokenType.Number,
or next.type == Constants.TokenType.FalseToken \ Constants.TokenType.Str,
or next.type == Constants.TokenType.TrueToken \ Constants.TokenType.FalseToken,
or next.type == Constants.TokenType.NullToken: Constants.TokenType.TrueToken,
Constants.TokenType.NullToken
]:
#output primitives # 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) rpn.append(next)
elif next.type == Constants.TokenType.Identifier: elif next.type == Constants.TokenType.Identifier:
print('adding function')
op_stack.push_back(next) op_stack.push_back(next)
func_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: while op_stack.back().type != Constants.TokenType.LeftParen:
var p = op_stack.pop_back() var p = op_stack.pop_back()
if p == null: if p == null:
printerr('unbalanced parenthesis %s ' % next.name) printerr('unbalanced parenthesis %s' % next.name)
break break
rpn.append(p) rpn.append(p)
@ -732,7 +749,7 @@ class ExpressionNode extends ParseNode:
#find the closest function on stack #find the closest function on stack
#increment parameters #increment parameters
func_stack.back().parameter_count+=1 func_stack.back().parameter_count += 1
elif Operator.is_op(next.type): elif Operator.is_op(next.type):
#this is an operator #this is an operator
@ -759,7 +776,7 @@ class ExpressionNode extends ParseNode:
next.type = Constants.TokenType.EqualTo next.type = Constants.TokenType.EqualTo
#operator precedence #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() var op = op_stack.pop_back()
rpn.append(op) rpn.append(op)
@ -771,20 +788,28 @@ class ExpressionNode extends ParseNode:
elif next.type == Constants.TokenType.RightParen: elif next.type == Constants.TokenType.RightParen:
#leaving sub expression #leaving sub expression
# resolve order of operations # resolve order of operations
var parameters = []
while op_stack.back().type != Constants.TokenType.LeftParen: while op_stack.back().type != Constants.TokenType.LeftParen:
rpn.append(op_stack.pop_back()) parameters.append(op_stack.pop_back())
if op_stack.back() == null:
printerr('Unbalanced parenthasis #RightParen. Parser.ExpressionNode') parser.compiler.assert(
op_stack.back() != null,
'Unbalanced parenthasis #RightParen. Parser.ExpressionNode'
)
rpn.append_array(parameters)
op_stack.pop_back() 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: if op_stack.back().type == Constants.TokenType.Identifier:
#function call #function call
#last token == left paren this == no parameters #last token == left paren this == no parameters
#else #else
#we have more than 1 param #we have more than 1 param
if last.type != Constants.TokenType.LeftParen: # if last.type != Constants.TokenType.LeftParen:
func_stack.back().parameter_count+=1 # func_stack.back().parameter_count += 1
func_stack.back().parameter_count = parameters.size()
rpn.append(op_stack.pop_back()) rpn.append(op_stack.pop_back())
func_stack.pop_back() func_stack.pop_back()
@ -804,6 +829,8 @@ class ExpressionNode extends ParseNode:
var first = rpn.front() var first = rpn.front()
var eval_stack = []#ExpressionNode var eval_stack = []#ExpressionNode
print(rpn)
while rpn.size() > 0: while rpn.size() > 0:
var next = rpn.pop_front() var next = rpn.pop_front()
if Operator.is_op(next.type): if Operator.is_op(next.type):
@ -811,7 +838,14 @@ class ExpressionNode extends ParseNode:
var info = Operator.op_info(next.type) var info = Operator.op_info(next.type)
if eval_stack.size() < info.arguments: 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 = [] var function_parameters = []
for _i in range(info.arguments): for _i in range(info.arguments):
@ -827,6 +861,7 @@ class ExpressionNode extends ParseNode:
# A function call # A function call
elif next.type == Constants.TokenType.Identifier: elif next.type == Constants.TokenType.Identifier:
var function_name = next.value var function_name = next.value
prints(function_name, next.parameter_count)
var function_parameters = [] var function_parameters = []
for _i in range(next.parameter_count): for _i in range(next.parameter_count):
@ -845,10 +880,14 @@ class ExpressionNode extends ParseNode:
eval_stack.append(expression) eval_stack.append(expression)
#we should have a single root expression left # NOTE: We should have a single root expression left
#if more then we failed ---- NANI # if more then we failed
if eval_stack.size() != 1: parser.compiler.assert(
printerr('[%s] Error parsing expression (stack did not reduce correctly )' % first) eval_stack.size() == 1,
'[%s] Error parsing expression (stack did not reduce correctly)' % first,
first.line_number,
first.column
)
return eval_stack.pop_back() return eval_stack.pop_back()
@ -860,12 +899,13 @@ class ExpressionNode extends ParseNode:
return key return key
return string return string
static func is_apply_precedence(_type, operator_stack): static func is_apply_precedence(_type, operator_stack, parser):
if operator_stack.size() == 0: if operator_stack.size() == 0:
return false return false
if not Operator.is_op(_type): 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 var second = operator_stack.back().type

View file

@ -211,3 +211,4 @@ static func token_name(type):
if TokenType[key] == type: if TokenType[key] == type:
return key return key
return '' return ''

View file

@ -29,7 +29,6 @@ class Line:
func _to_string(): func _to_string():
return '%s:%d: "%s"' % [file_name.get_file(), line_number, text] return '%s:%d: "%s"' % [file_name.get_file(), line_number, text]
class Option: class Option:
var line var line
var id = -1 var id = -1

View file

@ -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)

View file

@ -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

View file

@ -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()

BIN
addons/Wol/editor/WolEditor.tscn (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -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()

View file

@ -1,6 +1,10 @@
tool tool
extends EditorPlugin extends EditorPlugin
const WolEditor = preload('res://addons/Wol/editor/WolEditor.tscn')
var wol_editor_instance
func _enter_tree(): func _enter_tree():
add_custom_type( add_custom_type(
'Wol', 'Wol',
@ -9,5 +13,27 @@ func _enter_tree():
load('res://addons/Wol/icon-white.svg') 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(): func _exit_tree():
remove_custom_type('Wol') 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')

View file

@ -1,13 +1,13 @@
title: Start title: Start
tags: tags:
colorID: 0 colorID:
position: 0, 0 position: 0, 0
--- ---
<<a_custom_command>> <<a_custom_command>>
<<command_with multiple arguments>> <<command_with multiple arguments>>
// remove "to" to trigger error // remove "to" to trigger error
<<set $direction to 'that'>> <<set $direction to "that">>
<<set $one to 1>> <<set $one to 1>>
// Implement inline expressions // Implement inline expressions
@ -18,22 +18,22 @@ Narrator: Do you know you've been here {visit_count()} times?
You: Did you know one + one equals {$one + $one}? You: Did you know one + one equals {$one + $one}?
Narrator: You wanna go somewhere? Narrator: You wanna go somewhere?
-> Go to the store -> Go to the store
[[TheStore]] [[TheStore]]
// -> Wait, how many times have I been to the store? -> How much did I visit the store?
// Narrator: You've been to the store {visit_count('TheStore')} times. Narrator: You've been to the store { visit_count("TheStore") } times.
// [[Start]] [[Start]]
-> Lets stay here and talk -> Lets stay here and talk
[[Talk]] [[Talk]]
=== ===
title: TheStore title: TheStore
tags: tags:
colorID: 0 colorID:
position: 0, 200 position: 400, 200
--- ---
Guy: Hey what's up I need your help can you come here? Guy: Hey what's up I need your help can you come here?
You: Well I can't I'm buying clothes. 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. You: I can't find them.
Guy: What do you mean you can't find them? Guy: What do you mean you can't find them?
You: I can't find them there's only soup. You: I can't find them there's only soup.
@ -58,8 +58,8 @@ You: FUCK YOU!
=== ===
title: Talk title: Talk
tags: tags:
colorID: 0 colorID:
position: 0, 400 position: 800, 400
--- ---
Narrator: So how are you really? Narrator: So how are you really?
You: I'm good! You: I'm good!

View file

@ -24,6 +24,11 @@ gdscript/warnings/return_value_discarded=false
enabled=PoolStringArray( "res://addons/Wol/plugin.cfg" ) 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] [physics]
common/enable_pause_aware_picking=true common/enable_pause_aware_picking=true