diff options
Diffstat (limited to 'addons/escoria-core/game/core-scripts/esc/compiler')
13 files changed, 548 insertions, 147 deletions
diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_break_counter.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_break_counter.gd index df14c0b..53d8927 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_break_counter.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_break_counter.gd @@ -1,20 +1,21 @@ -## Used by the intpreter to track the depth of a `break` statement invoked in a dialog. -extends Object +## Used by the intpreter to track dialog `break` propagation state. class_name ESCBreakCounter +extends Object var _levels_left: int = 0: set = set_levels_left, get = get_levels_left +var _resume_parent_dialog: bool = false -## Sets the depth from the root of the dialog tree, with `levels == 0` at the root.[br] +## Sets the remaining number of dialog levels to unwind before control should return to a parent dialog, where `levels == 0` means the unwind has reached the parent dialog that should resume.[br] ## [br] ## #### Parameters[br] ## [br] ## | Name | Type | Description | Required? |[br] ## |:-----|:-----|:------------|:----------|[br] -## |levels|`int`|Number of dialog nesting levels remaining (0 represents the root).|yes|[br] +## |levels|`int`|Number of dialog nesting levels still to exit before resuming a parent dialog.|yes|[br] ## [br] ## #### Returns[br] ## [br] @@ -23,7 +24,7 @@ func set_levels_left(levels: int) -> void: _levels_left = levels -## Gets the depth from the root of the dialog tree, with `levels == 0` at the root.[br] +## Gets the remaining number of dialog levels to unwind before control should return to a parent dialog, where `levels == 0` means the unwind has reached the parent dialog that should resume.[br] ## [br] ## #### Parameters[br] ## [br] @@ -31,12 +32,68 @@ func set_levels_left(levels: int) -> void: ## [br] ## #### Returns[br] ## [br] -## Returns a `int` value. (`int`) +## Returns an `int` value. func get_levels_left() -> int: return _levels_left -## Decrements by one the depth from the root of the dialog tree, with `levels == 0` at the root.[br] +## Returns whether more dialog levels still need to be exited before reaching the parent dialog that should resume.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns `true` if additional dialog levels must still be unwound, otherwise `false`. (`bool`) +func has_levels_left() -> bool: + return _levels_left > 0 + + +## Advances the dialog-break state upward by one dialog level.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func advance_up_one_level() -> void: + if _levels_left > 0: + _levels_left -= 1 + + if _levels_left == 0: + _resume_parent_dialog = true + + +## Marks the dialog-break state so the receiving dialog frame resumes its own option loop instead of concluding.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func mark_resume_parent_dialog() -> void: + _resume_parent_dialog = true + + +## Returns whether the receiving dialog frame should resume its option loop.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns `true` if the parent dialog should resume, otherwise `false`. (`bool`) +func should_resume_parent_dialog() -> bool: + return _resume_parent_dialog + + +## Clears the resume-parent-dialog state after it has been consumed by a dialog frame.[br] ## [br] ## #### Parameters[br] ## [br] @@ -45,5 +102,5 @@ func get_levels_left() -> int: ## #### Returns[br] ## [br] ## Returns nothing. -func dec_levels_left() -> void: - _levels_left = _levels_left - 1 +func consume_parent_resume() -> void: + _resume_parent_dialog = false diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd index d63c52b..f89bed9 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd @@ -2,8 +2,8 @@ ## ## Scoping is implemented using this class, including traditional scoping rules. ## Scope binding and resolution are handled in this class. -extends Object class_name ESCEnvironment +extends Object var _enclosing: # ESCEnvironment diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_expr.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_expr.gd index 0e8bda8..ee94810 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_expr.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_expr.gd @@ -3,8 +3,8 @@ ## All inheriting classes must implement an `accept` method to invoke the visitor. ## For more information on the "Visitor" pattern, see: ## (Visitor Pattern)[https://en.wikipedia.org/wiki/Visitor_pattern] -extends RefCounted class_name ESCGrammarExpr +extends RefCounted ## Method that invokes another method in the visitor against this, the implementing class.[br] @@ -18,5 +18,5 @@ class_name ESCGrammarExpr ## #### Returns[br] ## [br] ## Returns nothing. -func accept(visitor): +func accept(_visitor): pass diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmt.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmt.gd index 1793f0e..126e8cc 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmt.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmt.gd @@ -3,8 +3,8 @@ ## All inheriting classes must implement an `accept` method to invoke the visitor. ## For more information on the "Visitor" pattern, see: ## (Visitor Pattern)[https://en.wikipedia.org/wiki/Visitor_pattern] -extends RefCounted class_name ESCGrammarStmt +extends RefCounted ## Method that invokes another method in the visitor against this, the implementing class.[br] @@ -18,5 +18,5 @@ class_name ESCGrammarStmt ## #### Returns[br] ## [br] ## Returns nothing. -func accept(visitor): +func accept(_visitor): pass diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd index 2e9c575..07c197c 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd @@ -154,6 +154,27 @@ class Event extends ESCGrammarStmt: ## [br] signal interrupted + ## Valid event flags[br] + ##[br] + ## * TK: stands for "telekinetic". It means the player won't walk over to + ## the item to say the line.[br] + ## * NO_TT: stands for "No tooltip". It hides the tooltip for the duration of + ## the event. Probably not very useful, because events having multiple + ## say commands in them are automatically hidden.[br] + ## * NO_UI: stands for "No User Inteface". It hides the UI for the duration of + ## the event. Useful when you want something to look like a cut scene but not + ## disable input for skipping dialog.[br] + ## * NO_SAVE: disables saving. Use this in cut scenes and anywhere a + ## badly-timed autosave would leave your game in a messed-up state. + enum { + FLAG_TK = 1, + FLAG_NO_TT = 2, + FLAG_NO_UI = 4, + FLAG_NO_SAVE = 8 + } + + var source: String = "" # TODO: Make proper use of this + var _name: ESCToken: get = get_name var _target: ESCGrammarExprs.Literal: @@ -175,28 +196,6 @@ class Event extends ESCGrammarStmt: var _is_interrupted: bool = false: get = is_interrupted - var source: String = "" # TODO: Make proper use of this - - - ## Valid event flags[br] - ##[br] - ## * TK: stands for "telekinetic". It means the player won't walk over to - ## the item to say the line.[br] - ## * NO_TT: stands for "No tooltip". It hides the tooltip for the duration of - ## the event. Probably not very useful, because events having multiple - ## say commands in them are automatically hidden.[br] - ## * NO_UI: stands for "No User Inteface". It hides the UI for the duration of - ## the event. Useful when you want something to look like a cut scene but not - ## disable input for skipping dialog.[br] - ## * NO_SAVE: disables saving. Use this in cut scenes and anywhere a - ## badly-timed autosave would leave your game in a messed-up state. - enum { - FLAG_TK = 1, - FLAG_NO_TT = 2, - FLAG_NO_UI = 4, - FLAG_NO_SAVE = 8 - } - ## Initialization method. Must be called after instantiation.[br] ## [br] @@ -784,6 +783,8 @@ class Stop extends ESCGrammarStmt: ## say("worker", "Because I work here.")[br] ##``` class DialogOption extends ESCGrammarStmt: + var _translation_key: String: + get = get_translation_key var _option: ESCGrammarExpr: get = get_option var _condition: ESCGrammarExpr: @@ -798,6 +799,7 @@ class DialogOption extends ESCGrammarStmt: ## [br] ## | Name | Type | Description | Required? |[br] ## |:-----|:-----|:------------|:----------|[br] + ## |translation_key|`String`|an optional translation key for the option text|yes|[br] ## |option|`ESCGrammarExpr`|the expression whose results will be displayed on screen as an option to be selected alongside any other options at the current level|yes|[br] ## |condition|`ESCGrammarExpr`|an optional condition to be evaluated that is used to determine whether the option should be displayed at all|yes|[br] ## |body|`ESCGrammarStmt`|the block of statements to be executed should this dialog option be selected|yes|[br] @@ -805,12 +807,26 @@ class DialogOption extends ESCGrammarStmt: ## #### Returns[br] ## [br] ## Returns nothing. - func init(option: ESCGrammarExpr, condition: ESCGrammarExpr, body: ESCGrammarStmt): + func init(translation_key: String, option: ESCGrammarExpr, condition: ESCGrammarExpr, body: ESCGrammarStmt): + _translation_key = translation_key _option = option _condition = condition _body = body + ## The translation key for this option, if one exists.[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## None. + ## [br] + ## #### Returns[br] + ## [br] + ## Returns the translation key for this option, if one exists. (`String`) + func get_translation_key() -> String: + return _translation_key + + ## The option expression.[br] ## [br] ## #### Parameters[br] diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd index 0944486..2097890 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd @@ -1,6 +1,6 @@ ## The actual interpreter that processes a parsed ASHES script. -extends RefCounted class_name ESCInterpreter +extends RefCounted ## Used to represent the current player character in scripts. @@ -26,6 +26,7 @@ var _locals: Dictionary = {} # Still, this should be tested extensively, and, if at all possible, with multiple # concurrent events running. var _current_event: ESCGrammarStmts.Event +var _dialog_depth: int = 0 # While most of the time we only run a single event at a time, it is possible for # multiple events to @@ -34,10 +35,12 @@ var _event_stack: Array = [] var _builtin_functions: Array = [ "print" ] +var _channel_name: String = "" -func _init(callables: Array, globals: Dictionary): +func _init(callables: Array, globals: Dictionary, channel_name: String = ""): _globals = ESCEnvironment.new() + _channel_name = channel_name for callable in callables: _globals.define(callable.get_command_name(), callable) @@ -77,6 +80,19 @@ func get_global_values() -> Dictionary: return _globals.get_values() +## Gets the event-manager channel this interpreter is executing on.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns the channel name associated with this interpreter run. (`String`) +func get_channel_name() -> String: + return _channel_name + + ## Resets the interpreter, specifically any locally-scoped variables.[br] ## [br] ## #### Parameters[br] @@ -253,13 +269,15 @@ func visit_call_expr(expr: ESCGrammarExprs.Call): self, "Can only call valid commands." ) - else: - return _handle_builtin_function(callee, args) + return ESCExecution.RC_ERROR + + return _handle_builtin_function(callee, args) var command = ESCCommand.new() command.parameters = args command.name = callee.get_command_name() command.parser_token = expr.get_paren_token() + command.channel_name = _channel_name var rc = ESCExecution.RC_OK @@ -282,11 +300,12 @@ func _handle_builtin_function(fn_name: String, args: Array) -> int: match fn_name: 'print': - if args.size() > 1: + if args.size() != 1: escoria.logger.error( self, - "'print' only takes one argument" + "'print' expects exactly one argument" ) + return rc _print(args) rc = ESCExecution.RC_OK @@ -315,13 +334,13 @@ func _print(value): func visit_if_stmt(stmt: ESCGrammarStmts.If): if _is_truthy(await _evaluate(stmt.get_condition())): return await _execute(stmt.get_then_branch()) - else: - for branch in stmt.get_elif_branches(): - if _is_truthy(await _evaluate(branch.get_condition())): - return await _execute(branch) - if stmt.get_else_branch(): - return await _execute(stmt.get_else_branch()) + for branch in stmt.get_elif_branches(): + if _is_truthy(await _evaluate(branch.get_condition())): + return await _execute(branch) + + if stmt.get_else_branch(): + return await _execute(stmt.get_else_branch()) return null @@ -344,6 +363,9 @@ func visit_while_stmt(stmt: ESCGrammarStmts.While): if ret is ESCGrammarStmts.Break: break + if _is_terminal_control_flow(ret): + return ret + return null @@ -358,10 +380,23 @@ func visit_while_stmt(stmt: ESCGrammarStmts.While): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_pass_stmt(stmt: ESCGrammarStmts.Pass): +func visit_pass_stmt(_stmt: ESCGrammarStmts.Pass): pass +func _is_terminal_control_flow(ret) -> bool: + if ret is ESCGrammarStmts.Stop \ + or ret is ESCGrammarStmts.Done \ + or ret is ESCBreakCounter: + return true + + if typeof(ret) == TYPE_INT: + return ret == ESCExecution.RC_ERROR \ + or ret == ESCExecution.RC_INTERRUPTED + + return false + + ## Executes code relevant to interpreting a `stop` statement. Relevant only to dialogs.[br] ## [br] ## #### Parameters[br] @@ -438,14 +473,76 @@ func visit_global_stmt(stmt: ESCGrammarStmts.Global): func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): var dialog: ESCDialog = ESCDialog.new() var rc = ESCExecution.RC_OK + var dialog_args := stmt.get_args() + var dialog_options := stmt.get_options() + + _dialog_depth += 1 + dialog.authored_options_count = dialog_options.size() + + if dialog_args.size() > 0: + var avatar = await _evaluate(dialog_args[0]) + + if avatar != null and not avatar is String and not avatar is StringName: + escoria.logger.error( + self, + "Dialog avatar must evaluate to a String." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + dialog.avatar = str(avatar) + + if dialog_args.size() > 1: + var timeout = await _evaluate(dialog_args[1]) + + if not timeout is int and not timeout is float: + escoria.logger.error( + self, + "Dialog timeout must evaluate to a number." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + if timeout < 0: + escoria.logger.error( + self, + "Dialog timeout must be greater than or equal to 0." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + dialog.timeout = int(timeout) + + if dialog_args.size() > 2: + var timeout_option = await _evaluate(dialog_args[2]) + + if not timeout_option is int and not timeout_option is float: + escoria.logger.error( + self, + "Dialog timeout option must evaluate to a number." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + if timeout_option < 0: + escoria.logger.error( + self, + "Dialog timeout option must be greater than or equal to 0." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + dialog.timeout_option = int(timeout_option) while true: dialog.options = [] - for dialog_option in stmt.get_options(): + for index in range(dialog_options.size()): + var dialog_option = dialog_options[index] var option: ESCDialogOption = ESCDialogOption.new() - # TODO: Translation keys option.source_option = dialog_option + option.source_option_index = index + 1 + option.translation_key = dialog_option.get_translation_key() option.option = await _evaluate(dialog_option.get_option()) if dialog_option.get_condition(): @@ -453,12 +550,23 @@ func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): else: option.set_is_valid(true) - dialog.options.append(option) + # Dialog execution should stop once no valid options remain. Keep only + # currently valid options in the presented list so an exhausted dialog + # concludes instead of re-running forever with unselectable entries. + if option.is_valid(): + dialog.options.append(option) if dialog.options.size() == 0: break - if dialog.is_valid() and not _current_event.is_interrupted(): + # If the event was interrupted while a nested command or dialog body was + # active, stop the dialog frame immediately instead of looping and + # presenting options again. + if _current_event and _current_event.is_interrupted(): + _dialog_depth -= 1 + return ESCExecution.RC_INTERRUPTED + + if dialog.is_valid(): var chosen_option = await dialog.run() if chosen_option: @@ -468,22 +576,73 @@ func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): "Chosen dialog option (%s) was completed." % chosen_option ) - if execute_ret is ESCGrammarStmts.Break: - var break_tracker: ESCBreakCounter = ESCBreakCounter.new() + # Dialog frames must propagate terminal runtime errors the same + # way normal blocks do. Otherwise an invalid command or similar + # failure inside an option body is treated like normal completion + # and the dialog loop re-displays the same options indefinitely. + if typeof(execute_ret) == TYPE_INT and execute_ret == ESCExecution.RC_ERROR: + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + # An interruption can happen while the chosen option body is still + # running, so check again after it returns before processing normal + # dialog control flow like `break`, `done`, or `stop`. + if _current_event and _current_event.is_interrupted(): + _dialog_depth -= 1 + return ESCExecution.RC_INTERRUPTED + + if execute_ret is ESCGrammarStmts.Break: + var levels_left := 0 if execute_ret.get_levels(): - break_tracker.set_levels_left(await _evaluate(execute_ret.get_levels()) - 1) + levels_left = await _evaluate(execute_ret.get_levels()) - 1 + + # A top-level dialog consumes `break` by concluding the current dialog. + if _dialog_depth == 1: + break + + # Nested dialogs propagate explicit break state upward so parent + # dialog frames can decide whether to keep unwinding or resume. + var break_tracker: ESCBreakCounter = ESCBreakCounter.new() + if levels_left > 0: + break_tracker.set_levels_left(levels_left) else: - break_tracker.set_levels_left(0) + break_tracker.mark_resume_parent_dialog() + _dialog_depth -= 1 return break_tracker - elif execute_ret is ESCGrammarStmts.Done: + if execute_ret is ESCGrammarStmts.Done: + _dialog_depth -= 1 + + if _dialog_depth == 0: + return rc + return execute_ret - elif execute_ret is ESCBreakCounter: - if execute_ret.get_levels_left() > 0: - execute_ret.dec_levels_left() + + if execute_ret is ESCGrammarStmts.Stop: + # `stop` aborts the entire event, so dialog frames must pass it through. + _dialog_depth -= 1 + return execute_ret + + if execute_ret is ESCBreakCounter: + if execute_ret.has_levels_left(): + # Once the unwind reaches the top-most dialog, the remaining + # levels are consumed by concluding that dialog entirely. + if _dialog_depth == 1: + break + + execute_ret.advance_up_one_level() + _dialog_depth -= 1 return execute_ret + if execute_ret.should_resume_parent_dialog(): + # The requested dialog levels have been exited, so this frame + # resumes presenting its own options instead of concluding. + execute_ret.consume_parent_resume() + continue + + break + + _dialog_depth -= 1 return rc @@ -498,7 +657,7 @@ func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_dialog_option_stmt(stmt: ESCGrammarStmts.DialogOption): +func visit_dialog_option_stmt(_stmt: ESCGrammarStmts.DialogOption): pass @@ -623,18 +782,28 @@ func visit_binary_expr(expr: ESCGrammarExprs.Binary): return not _is_equal(left_part, right_part) ESCTokenType.TokenType.GREATER: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part > right_part ESCTokenType.TokenType.GREATER_EQUAL: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part >= right_part ESCTokenType.TokenType.LESS: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part < right_part ESCTokenType.TokenType.LESS_EQUAL: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part <= right_part ESCTokenType.TokenType.MINUS: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part - right_part ESCTokenType.TokenType.PLUS: var check = _check_are_numbers(left_part, expr.get_operator(), right_part, false) @@ -653,9 +822,13 @@ func visit_binary_expr(expr: ESCGrammarExprs.Binary): ) ESCTokenType.TokenType.SLASH: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part / right_part ESCTokenType.TokenType.STAR: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part * right_part return null @@ -680,6 +853,8 @@ func visit_unary_expr(expr: ESCGrammarExprs.Unary): return not _is_truthy(right_part) ESCTokenType.TokenType.MINUS: var check = _check_is_number(right_part, expr.get_operator()) + if not check: + return null return -right_part return null @@ -793,8 +968,8 @@ func look_up_variable(name: ESCToken, expr: ESCGrammarExpr): if distance == -1: return _globals.get_value(name) - else: - return _environment.get_at(distance, name.get_lexeme()) + + return _environment.get_at(distance, name.get_lexeme()) # Private methods @@ -860,10 +1035,18 @@ func _execute_block(statements: Array, env: ESCEnvironment): for stmt in statements: ret = await _execute(stmt) - if ret is ESCGrammarStmts.Break \ - or ret is ESCGrammarStmts.Done \ - or ret is ESCGrammarStmts.Stop: + # If the running event was interrupted while this statement was active, + # stop the block immediately so later statements do not continue running. + if _current_event and _current_event.is_interrupted(): + _environment = previous_env + return ESCExecution.RC_INTERRUPTED + if ret is ESCGrammarStmts.Break: + _environment = previous_env + return ret + + if _is_terminal_control_flow(ret): + _environment = previous_env return ret # TODO: Proper error handling per statement? @@ -925,12 +1108,12 @@ func _check_at_least_one_string(value_1, value_2): return typeof(value_1) == TYPE_STRING || typeof(value_2) == TYPE_STRING -func _on_global_changed(key: String, old_value, new_value) -> void: +func _on_global_changed(key: String, _old_value, new_value) -> void: # Shoehorn this in as an adapter var token: ESCToken = ESCToken.new() token.init(ESCTokenType.TokenType.IDENTIFIER, key, null, "", -1, "") if _globals.get_values().has(key): _globals.assign(token, new_value) - elif escoria.save_manager.is_loading_game: + else: _globals.define(key, new_value) diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter_factory.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter_factory.gd index 4205f92..24f6c66 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter_factory.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter_factory.gd @@ -1,6 +1,10 @@ ## Factory class for producing an ASHES interpreter. -extends RefCounted class_name ESCInterpreterFactory +extends RefCounted + +const ESC_INTERPRETER_SCRIPT := preload( + "res://addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd" +) static var _interpreter: ESCInterpreter = null @@ -15,16 +19,64 @@ static var _interpreter: ESCInterpreter = null ## #### Returns[br] ## [br] ## Returns a `ESCInterpreter` value. (`ESCInterpreter`) -static func create_interpreter() -> ESCInterpreter: - if not _interpreter: - _interpreter = load("res://addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd").new(ESCCompiler.load_commands(), ESCCompiler.load_globals()) - return _interpreter +static func create_interpreter(channel_name: String = "") -> ESCInterpreter: + return create_utility_interpreter(channel_name) + - _interpreter = load("res://addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd").new([], _interpreter.get_global_values()) +## Produces an interpreter for non-runtime utility work such as resolving or +## evaluating statements outside the event manager.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |channel_name|`String`|Optional channel name to assign to the created interpreter.|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns a cached-or-reseeded `ESCInterpreter` instance for utility usage. (`ESCInterpreter`) +static func create_utility_interpreter(channel_name: String = "") -> ESCInterpreter: + if not _interpreter: + _interpreter = ESC_INTERPRETER_SCRIPT.new( + ESCCompiler.load_commands(), + ESCCompiler.load_globals(), + channel_name + ) + else: + _interpreter = ESC_INTERPRETER_SCRIPT.new( + [], + _interpreter.get_global_values(), + channel_name + ) return _interpreter +## Produces a fresh interpreter intended for event-manager runtime execution.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |channel_name|`String`|Optional channel name to assign to the created interpreter.|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns a newly-created `ESCInterpreter` instance for runtime usage. (`ESCInterpreter`) +static func create_runtime_interpreter(channel_name: String = "") -> ESCInterpreter: + var runtime_globals := ESCCompiler.load_globals() + + if not Engine.is_editor_hint() and escoria and escoria.globals_manager: + runtime_globals.merge(escoria.globals_manager.get_globals(), true) + + var interpreter := ESC_INTERPRETER_SCRIPT.new( + ESCCompiler.load_commands(), + runtime_globals, + channel_name + ) + return interpreter + + static func reset_interpreter() -> void: if is_instance_valid(_interpreter): _interpreter.cleanup() diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_parse_error.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_parse_error.gd index 5fd01a3..1bb5b19 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_parse_error.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_parse_error.gd @@ -1,3 +1,3 @@ ## Class to represent an error that occured during parsing. -extends Object class_name ESCParseError +extends Object diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_parser.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_parser.gd index b88adcb..6229e34 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_parser.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_parser.gd @@ -4,8 +4,8 @@ ## Note that the vast majority of this class consists of (effectively) private methods ## in order to facilitate encapsulation, but if you're interested in how the parser ## is implemented, [check out the class in GitHub](https://github.com/godot-escoria/escoria-demo-game/blob/main/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd) -extends RefCounted class_name ESCParser +extends RefCounted var _tokens: Array @@ -52,48 +52,56 @@ func parse() -> Array: var statements: Array = [] while not _at_end(): - statements.append(_declaration()) + var declaration = _declaration() + if declaration != null: + statements.append(declaration) return statements func _declaration() -> ESCGrammarStmt: - var retStmt + while _match(ESCTokenType.TokenType.NEWLINE) or _match(ESCTokenType.TokenType.DEDENT): + pass + + if _at_end(): + return null + + var ret_stmt if _match(ESCTokenType.TokenType.COLON): - retStmt = _event_declaration() + ret_stmt = _event_declaration() - if retStmt is ESCParseError: + if ret_stmt is ESCParseError: _synchronize() return null - else: - return retStmt + + return ret_stmt if _match(ESCTokenType.TokenType.VAR): - retStmt = _var_declaration() + ret_stmt = _var_declaration() - if retStmt is ESCParseError: + if ret_stmt is ESCParseError: _synchronize() return null - else: - return retStmt + + return ret_stmt if _match(ESCTokenType.TokenType.GLOBAL): - retStmt = _global_declaration() + ret_stmt = _global_declaration() - if retStmt is ESCParseError: + if ret_stmt is ESCParseError: _synchronize() return null - else: - return retStmt - retStmt = _statement() + return ret_stmt + + ret_stmt = _statement() - if retStmt is ESCParseError: + if ret_stmt is ESCParseError: _synchronize() return null - return retStmt + return ret_stmt func _event_declaration(): @@ -157,8 +165,8 @@ func _event_declaration(): ret.init(name, target, flags, body, _associated_object_global_id) return ret - else: - return _error(_peek(), "Expected block after event declaration for '%s'. Code blocks require tab(s) at the start of a line." % name.get_lexeme()) + + return _error(_peek(), "Expected block after event declaration for '%s'. Code blocks require tab(s) at the start of a line." % name.get_lexeme()) func _literal_from_identifier(expr: ESCGrammarExprs.Variable) -> ESCGrammarExprs.Literal: @@ -314,21 +322,25 @@ func _while_statement(): var condition = _expression() if condition is ESCParseError: + _loop_level -= 1 return condition var colon_token = _consume(ESCTokenType.TokenType.COLON, "Expect ':' after condition.") if colon_token is ESCParseError: + _loop_level -= 1 return colon_token var consume = _consume_new_block_start("start of while loop") if consume: + _loop_level -= 1 return consume var block = _block() if block is ESCParseError: + _loop_level -= 1 return block var body = ESCGrammarStmts.Block.new() @@ -372,6 +384,7 @@ func _dialog_statement(): var arg = _expression() if arg is ESCParseError: + _dialog_level -= 1 return arg args.append(arg) @@ -382,14 +395,32 @@ func _dialog_statement(): var consume = _consume(ESCTokenType.TokenType.RIGHT_PAREN, "Expect ')' after start dialog arguments.") if consume is ESCParseError: + _dialog_level -= 1 return consume + elif not _check(ESCTokenType.TokenType.NEWLINE): + while true: + var arg = _expression() + + if arg is ESCParseError: + _dialog_level -= 1 + return arg + + args.append(arg) + + if _check(ESCTokenType.TokenType.NEWLINE): + break + + if _match(ESCTokenType.TokenType.COMMA): + continue if args.size() > 3: + _dialog_level -= 1 return _error(_peek(), "Start dialog cannot have more than 3 arguments.") var consume = _consume_new_block_start("dialog start") if consume is ESCParseError: + _dialog_level -= 1 return consume var options: Array = [] @@ -398,6 +429,7 @@ func _dialog_statement(): var dialog_option = _dialog_option_statement() if dialog_option is ESCParseError: + _dialog_level -= 1 return dialog_option options.append(dialog_option) @@ -433,6 +465,14 @@ func _dialog_option_statement(): if consume is ESCParseError: return consume + var translation_key := "" + + if _match_in_order([ + ESCTokenType.TokenType.IDENTIFIER, + ESCTokenType.TokenType.COLON + ]): + translation_key = _tokens[_current - 2].get_lexeme() + var expr = _expression() if expr is ESCParseError: @@ -448,8 +488,14 @@ func _dialog_option_statement(): consume = _consume(ESCTokenType.TokenType.RIGHT_SQUARE, "Expect ']' after dialog option condition") + if consume is ESCParseError: + return consume + consume = _consume_new_block_start("dialog option") + if consume is ESCParseError: + return consume + var block_stmts = _block() if block_stmts is ESCParseError: @@ -459,7 +505,7 @@ func _dialog_option_statement(): block.init(block_stmts) var option: ESCGrammarStmts.DialogOption = ESCGrammarStmts.DialogOption.new() - option.init(expr, condition, block) + option.init(translation_key, expr, condition, block) return option @@ -512,7 +558,8 @@ func _block(): if decl is ESCParseError: return decl - statements.append(decl) + if decl != null: + statements.append(decl) _consume(ESCTokenType.TokenType.DEDENT, "Expected DEDENT after block.") return statements @@ -536,7 +583,8 @@ func _assignment(): var ret = ESCGrammarExprs.Assign.new() ret.init(name, value) return ret - elif expr is ESCGrammarExprs.Get: + + if expr is ESCGrammarExprs.Get: var ret = ESCGrammarExprs.Set.new() ret.init(expr.get_object(), expr.get_name(), value) return ret @@ -723,15 +771,15 @@ func _is_checking(): var ret = ESCGrammarExprs.Is.new() ret.init(expr, null, _previous()) return ret - else: - var state_expr = _expression() - if state_expr is ESCParseError: - return state_expr + var state_expr = _expression() - var ret = ESCGrammarExprs.Is.new() - ret.init(expr, state_expr, null) - return ret + if state_expr is ESCParseError: + return state_expr + + var ret = ESCGrammarExprs.Is.new() + ret.init(expr, state_expr, null) + return ret return expr @@ -777,6 +825,9 @@ func _finish_call(callee: ESCGrammarExpr): var paren = _consume(ESCTokenType.TokenType.RIGHT_PAREN, "Expect ')' after arguments.") + if paren is ESCParseError: + return paren + #if paren.get_type() != ESCTokenType.TokenType.NEWLINE: # return _error(ESCTokenType.TokenType.NEWLINE, "Expect NEWLINE after arguments.") @@ -817,7 +868,13 @@ func _primary(): if expr is ESCParseError: return expr - _consume(ESCTokenType.TokenType.RIGHT_PAREN, "Expect ')' after expression.") + var paren = _consume( + ESCTokenType.TokenType.RIGHT_PAREN, + "Expect ')' after expression." + ) + + if paren is ESCParseError: + return paren var ret = ESCGrammarExprs.Grouping.new() ret.init(expr) @@ -826,30 +883,52 @@ func _primary(): return _error(_peek(), "Expect expression.") -func _var_declaration() -> ESCGrammarStmt: +func _var_declaration(): var name = _consume(ESCTokenType.TokenType.IDENTIFIER, "Expect variable name.") var initializer: ESCGrammarExpr = null if _match(ESCTokenType.TokenType.EQUAL): - initializer = _expression() + var initializer_expr = _expression() + + if initializer_expr is ESCParseError: + return initializer_expr + + initializer = initializer_expr + + var newline = _consume( + ESCTokenType.TokenType.NEWLINE, + "Expect newline after variable declaration." + ) - _consume(ESCTokenType.TokenType.NEWLINE, "Expect newline after variable declaration.") + if newline is ESCParseError: + return newline var ret = ESCGrammarStmts.Var.new() ret.init(name, initializer) return ret -func _global_declaration() -> ESCGrammarStmt: +func _global_declaration(): var name = _consume(ESCTokenType.TokenType.IDENTIFIER, "Expect global variable name.") var initializer: ESCGrammarExpr = null if _match(ESCTokenType.TokenType.EQUAL): - initializer = _expression() + var initializer_expr = _expression() - _consume(ESCTokenType.TokenType.NEWLINE, "Expect newline after global variable declaration.") + if initializer_expr is ESCParseError: + return initializer_expr + + initializer = initializer_expr + + var newline = _consume( + ESCTokenType.TokenType.NEWLINE, + "Expect newline after global variable declaration." + ) + + if newline is ESCParseError: + return newline var ret = ESCGrammarStmts.Global.new() ret.init(name, initializer) @@ -864,11 +943,11 @@ func _peek() -> ESCToken: return _tokens[_current] -func _match(tokenTypes) -> bool: - if not tokenTypes is Array: - tokenTypes = [tokenTypes] +func _match(token_types) -> bool: + if not token_types is Array: + token_types = [token_types] - for type in tokenTypes: + for type in token_types: if _check(type): _advance() return true @@ -876,12 +955,15 @@ func _match(tokenTypes) -> bool: return false -func _match_in_order(tokenTypes) -> bool: - if not tokenTypes is Array: - tokenTypes = [tokenTypes] +func _match_in_order(token_types) -> bool: + if not token_types is Array: + token_types = [token_types] - for type in tokenTypes: + var start := _current + + for type in token_types: if not _check(type): + _current = start return false _advance() @@ -889,21 +971,21 @@ func _match_in_order(tokenTypes) -> bool: return true -func _consume(tokenType, message: String): - if _check(tokenType): +func _consume(token_type, message: String): + if _check(token_type): return _advance() return _error(_peek(), message) -func _check(tokenTypes) -> bool: - if not tokenTypes is Array: - tokenTypes = [tokenTypes] +func _check(token_types) -> bool: + if not token_types is Array: + token_types = [token_types] if _at_end(): return false - for type in tokenTypes: + for type in token_types: if _peek().get_type() == type: return true @@ -911,9 +993,9 @@ func _check(tokenTypes) -> bool: # This turns the parser into an LL(2). -func _check_next(tokenTypes) -> bool: - if not tokenTypes is Array: - tokenTypes = [tokenTypes] +func _check_next(token_types) -> bool: + if not token_types is Array: + token_types = [token_types] if _at_end(): return false @@ -924,7 +1006,7 @@ func _check_next(tokenTypes) -> bool: _current -= 1 return false - for type in tokenTypes: + for type in token_types: if _peek().get_type() == type: _current -= 1 return true @@ -959,16 +1041,27 @@ func _error(token: ESCToken, message: String) -> ESCParseError: func _synchronize() -> void: + if _check(ESCTokenType.TokenType.DEDENT): + return + _advance() while not _at_end(): - if _previous().get_type() == ESCTokenType.TokenType.NEWLINE: + if _previous().get_type() == ESCTokenType.TokenType.NEWLINE \ + or _previous().get_type() == ESCTokenType.TokenType.DEDENT: return match _peek().get_type(): + ESCTokenType.TokenType.COLON,\ + ESCTokenType.TokenType.GLOBAL,\ ESCTokenType.TokenType.VAR,\ ESCTokenType.TokenType.IF,\ ESCTokenType.TokenType.WHILE,\ + ESCTokenType.TokenType.PASS,\ + ESCTokenType.TokenType.STOP,\ + ESCTokenType.TokenType.QUESTION_BANG,\ + ESCTokenType.TokenType.BREAK,\ + ESCTokenType.TokenType.DONE,\ ESCTokenType.TokenType.RETURN: return diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_resolver.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_resolver.gd index 2b32b35..d356b85 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_resolver.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_resolver.gd @@ -1,8 +1,8 @@ ## Resolves script variables to ensure that scoping rules are obeyed. ## ## The class will throw errors if scoping rules are broken or otherwise not followed. -extends RefCounted class_name ESCResolver +extends RefCounted var _interpreter @@ -136,7 +136,7 @@ func visit_while_stmt(stmt: ESCGrammarStmts.While): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_pass_stmt(stmt: ESCGrammarStmts.Pass): +func visit_pass_stmt(_stmt: ESCGrammarStmts.Pass): pass @@ -151,7 +151,7 @@ func visit_pass_stmt(stmt: ESCGrammarStmts.Pass): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_stop_stmt(stmt: ESCGrammarStmts.Stop): +func visit_stop_stmt(_stmt: ESCGrammarStmts.Stop): pass @@ -186,7 +186,7 @@ func visit_var_stmt(stmt: ESCGrammarStmts.Var): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_global_stmt(stmt: ESCGrammarStmts.Global): +func visit_global_stmt(_stmt: ESCGrammarStmts.Global): pass @@ -237,7 +237,7 @@ func visit_break_stmt(stmt: ESCGrammarStmts.Break): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_done_stmt(stmt: ESCGrammarStmts.Done): +func visit_done_stmt(_stmt: ESCGrammarStmts.Done): pass @@ -270,7 +270,7 @@ func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_literal_expr(expr: ESCGrammarExprs.Literal): +func visit_literal_expr(_expr: ESCGrammarExprs.Literal): pass diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_scanner.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_scanner.gd index 08642a9..6591c48 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_scanner.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_scanner.gd @@ -3,8 +3,8 @@ ## This is the first link in the ASHES compiler toolchain, and takes in ASHES ## script as text in order to provide a list of tokens to be handled by the ## ASHES parser. -extends RefCounted class_name ESCScanner +extends RefCounted ## In ASHES, an identifier starting with `$` is interpreted as a lookup in diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd index 46ba408..26dacce 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd @@ -1,7 +1,7 @@ ## Convenience class to help build scripts in code for use with the ASHES compiler ## toolchain. Loosely follows the Builder pattern. -extends RefCounted class_name ESCScriptBuilder +extends RefCounted # Not really a "Builder" pattern per se, but helps with readability. diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_token.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_token.gd index ec95d98..bd99d73 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_token.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_token.gd @@ -3,11 +3,11 @@ ## ## This class contains any information required by the parser but also carries ## with it associated information useful for debugging and tracing purposes. -extends RefCounted class_name ESCToken +extends RefCounted -var _tokenType: int: # ESCScanner.TokenType enum +var _token_type: int: # ESCScanner.TokenType enum get = get_type var _lexeme: String: get = get_lexeme @@ -38,8 +38,8 @@ var _filename: String: ## #### Returns[br] ## [br] ## Returns nothing. -func init(tokenType: int, lexeme: String, literal, source: String, line: int, filename: String) -> void: - _tokenType = tokenType +func init(token_type: int, lexeme: String, literal, source: String, line: int, filename: String) -> void: + _token_type = token_type _lexeme = lexeme _literal = literal _source = source @@ -85,7 +85,7 @@ func get_source() -> String: ## [br] ## Returns the type of token this is; will be a value from `ESCTokenType.TokenType`. (`int`) func get_type() -> int: - return _tokenType + return _token_type ## The literal value of the token, if it exists.[br] @@ -141,4 +141,4 @@ func get_filename() -> String: func _to_string() -> String: - return ESCTokenType.get_token_type_name(_tokenType) + " " + str(_lexeme) + " " + (str(_literal) if _literal else "") + return ESCTokenType.get_token_type_name(_token_type) + " " + str(_lexeme) + " " + (str(_literal) if _literal else "") |
