diff options
| author | Radio <radiohotline@disroot.org> | 2026-06-04 15:15:11 +0300 |
|---|---|---|
| committer | Radio <radiohotline@disroot.org> | 2026-06-04 15:15:11 +0300 |
| commit | 9d74b49ab62908d4acf53dde444413886b8b28e5 (patch) | |
| tree | 0cb785267ea7e239b19d88684c0d64b9bd5d52f0 /addons/escoria-core/tools/script_analysis/analyzers | |
| parent | 2c85d452ad02a5f89cd43bad7f9de31d0e4fa0c1 (diff) | |
escoria setup and old branch archives
Diffstat (limited to 'addons/escoria-core/tools/script_analysis/analyzers')
4 files changed, 540 insertions, 0 deletions
diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd new file mode 100644 index 0000000..3ed0025 --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd @@ -0,0 +1,151 @@ +extends ESCScriptAnalyzer +class_name ESCExitSceneScriptAnalyzer + + +const EXIT_SCENE_EVENT_NAME = "exit_scene" +const ACCEPT_INPUT_DISABLE_ARGS = ["none", "skip"] + +const CHANGE_SCENE_MISSING_MESSAGE = "Event ':exit_scene' is missing 'change_scene' command. Scene might not change as expected. Ignore this warning if the command was omitted on purpose." + +const COMMANDS_AFTER_CHANGE_SCENE_MESSAGE = "Event ':exit_scene' may have commands that are expected to be run after a call to 'change_scene'. Such commands will not be executed." + +const MISSING_ACCEPT_INPUT_MESSAGE = \ + "Event ':exit_scene' may allow for the player character to move while exiting and changing the scene." \ + + " To prevent this, ensure a call to 'accept_input' with an argument evaluating to 'NONE' or 'SKIP' is" \ + + " made before any calls to 'transition' and/or 'change_scene'." + +const BULLET_CHARACTER = "- " + + +# These are vars because they have to be, but should be treated as consts +var TRANSITION_COMMAND_NAME = TransitionCommand.new().get_command_name() +var CHANGE_SCENE_COMMAND_NAME = ChangeSceneCommand.new().get_command_name() +var ACCEPT_INPUT_COMMAND_NAME = AcceptInputCommand.new().get_command_name() + +var _has_change_scene_command: bool = false + +var _has_commands_after_change_scene_command: bool = false + +var _change_scene_token: ESCToken = null + +var _accept_input_disable_missing: bool = false + +var _accept_input_token: ESCToken = null + + +func analyze(statements: Array) -> void: + if not statements is Array: + statements = [statements] + + for statement in statements: + if statement.get_event_name().to_lower() == EXIT_SCENE_EVENT_NAME: + _execute(statement) + + _add_warning_messages() + + break + + +func _add_warning_messages() -> void: + var messages: Array[String] = [] + + if not _has_change_scene_command: + messages.append(BULLET_CHARACTER + CHANGE_SCENE_MISSING_MESSAGE) + + if _accept_input_disable_missing: + messages.append(BULLET_CHARACTER + MISSING_ACCEPT_INPUT_MESSAGE) + + if _has_commands_after_change_scene_command: + messages.append(BULLET_CHARACTER + COMMANDS_AFTER_CHANGE_SCENE_MESSAGE) + + if not messages.is_empty(): + _rich_messages.append(ESCSafeLogging.log_warn.bind(self, "\n".join(messages))) + + +func _execute_block(statements: Array, env: ESCEnvironment): + var previous_env = _environment + var ret = null + + _environment = env + + for stmt in statements: + ret = _execute(stmt) + + _environment = previous_env + + return ret + + +# Visitor overriding +func visit_call_expr(expr: ESCGrammarExprs.Call): + var callee = _evaluate(expr.get_callee()) + + var args: Array = [] + + for arg in expr.get_arguments(): + arg = _evaluate(arg) + + # "Adapter" for current ESC commands since they don't currently take + # ESCObject's (or ESCRoom's) as arguments. + if arg is ESCObject or arg is ESCRoom: + arg = arg.global_id + + args.append(arg) + + var command_name: String = _get_command_name(callee) + + _has_commands_after_change_scene_command = _has_change_scene_command_in_scope() + + if command_name == CHANGE_SCENE_COMMAND_NAME: + _has_change_scene_command = true + + _change_scene_token = expr.get_paren_token() + _environment.define(_change_scene_token.get_lexeme(), true) + + # First arg is always the path of the scene to transition to. + # Second arg determines whether the transition is automatic, and defaults to true + if args.size() > 1: + var is_auto_transition = args[1] as bool + + if is_auto_transition != null and not is_auto_transition: + _check_for_missing_accept_input_disable() + elif command_name == TRANSITION_COMMAND_NAME: + _check_for_missing_accept_input_disable() + elif command_name == ACCEPT_INPUT_COMMAND_NAME: + var first_arg = args[0] as String + + if first_arg != null and first_arg.to_lower() in ACCEPT_INPUT_DISABLE_ARGS: + _accept_input_token = expr.get_paren_token() + _environment.define(_accept_input_token.get_lexeme(), true) + + return ESCExecution.RC_OK + + +# as a key in the environment/scope to serve as a marker to check concerning whether `accept_input` +# has previously been called with the appropriate argument +func _check_for_missing_accept_input_disable() -> void: + if _accept_input_token == null: + _accept_input_disable_missing = true + return + + _accept_input_disable_missing = \ + not _environment.is_valid_key(_accept_input_token) \ + or not _environment.get_value(_accept_input_token) + + +func _has_change_scene_command_in_scope() -> bool: + if _change_scene_token == null: + return false + + return \ + _environment.is_valid_key(_change_scene_token) \ + and _environment.get_value(_change_scene_token) + + +func _get_command_name(callee) -> String: + # if we don't have a command to run, check against any built-in functions and, + # if one is found and is executed, we're done + if callee is ESCBaseCommand: + return callee.get_command_name() + + return callee diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd.uid b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd.uid new file mode 100644 index 0000000..902e168 --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd.uid @@ -0,0 +1 @@ +uid://0jmaf8d0mjpe diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd new file mode 100644 index 0000000..874ffa1 --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd @@ -0,0 +1,387 @@ +extends RefCounted +class_name ESCScriptAnalyzer + + +const CURRENT_PLAYER_KEYWORD = "CURRENT_PLAYER" + + +var _rich_messages: Array[Callable] = [] + +var _globals: ESCEnvironment +var _environment: ESCEnvironment = _globals + +var _locals: Dictionary = {} + +var _builtin_functions: Array = [ + "print" +] + + +# This must be implemented in child class. +func analyze(statements: Array) -> void: + pass + + +func print_messages() -> void: + for print_method in _rich_messages: + print_method.call() + + +func _init(): + var globals: Dictionary = ESCCompiler.load_globals() + + _globals = ESCEnvironment.new() + + for callable in ESCCompiler.load_commands(): + _globals.define(callable.get_command_name(), callable) + + for key in ESCCompiler.load_globals().keys(): + _globals.define(key, globals[key]) + + +# Visitor implementations +func visit_block_stmt(stmt: ESCGrammarStmts.Block): + var env: ESCEnvironment = ESCEnvironment.new() + env.init(_environment) + + return _execute_block(stmt.get_statements(), env) + + +func visit_event_stmt(stmt: ESCGrammarStmts.Event): + _execute(stmt.get_body()) + + +func visit_expression_stmt(stmt: ESCGrammarStmts.ESCExpression): + return _evaluate(stmt.get_expression()) + + +func visit_call_expr(expr: ESCGrammarExprs.Call): + var callee = _evaluate(expr.get_callee()) + + var args: Array = [] + + for arg in expr.get_arguments(): + arg = _evaluate(arg) + + # "Adapter" for current ESC commands since they don't currently take + # ESCObject's (or ESCRoom's) as arguments. + if arg is ESCObject or arg is ESCRoom: + arg = arg.global_id + + args.append(arg) + + # if we don't have a command to run, check against any built-in functions and, + # if one is found and is executed, we're done + if not callee is ESCBaseCommand: + if not callee in _builtin_functions: + return 0 + else: + return _handle_builtin_function(callee, args) + + return ESCExecution.RC_OK + + +func _handle_builtin_function(fn_name: String, args: Array) -> int: + var rc = ESCExecution.RC_ERROR + + match fn_name: + 'print': + if args.size() > 1: + ESCSafeLogging.log_warn( + self, + "'print' only takes one argument" + ) + + rc = ESCExecution.RC_OK + + return rc + + +func visit_if_stmt(stmt: ESCGrammarStmts.If): + _execute(stmt.get_then_branch()) + + for branch in stmt.get_elif_branches(): + _execute(branch) + + if stmt.get_else_branch(): + _execute(stmt.get_else_branch()) + + return null + + +func visit_while_stmt(stmt: ESCGrammarStmts.While): + _execute(stmt.get_body()) + + return null + + +func visit_pass_stmt(stmt: ESCGrammarStmts.Pass): + pass + + +func visit_stop_stmt(stmt: ESCGrammarStmts.Stop): + return stmt + + +func visit_var_stmt(stmt: ESCGrammarStmts.Var): + var value = null + + if stmt.get_initializer(): + value = _evaluate(stmt.get_initializer()) + + _environment.define(stmt.get_name().get_lexeme(), value) + return null + + +func visit_global_stmt(stmt: ESCGrammarStmts.Global): + var value = null + + if stmt.get_initializer(): + value = _evaluate(stmt.get_initializer()) + + # Only define the global if we haven't already done so; otherwise, just + # ignore it + if not _globals.get_values().has(stmt.get_name().get_lexeme()): + _globals.define(stmt.get_name().get_lexeme(), value) + + return null + + +func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): + return null + + +func visit_dialog_option_stmt(stmt: ESCGrammarStmts.DialogOption): + pass + + +func visit_break_stmt(stmt: ESCGrammarStmts.Break): + return stmt + + +func visit_done_stmt(stmt: ESCGrammarStmts.Done): + return stmt + + +func visit_assign_expr(expr: ESCGrammarExprs.Assign): + var value = _evaluate(expr.get_value()) + + var distance: int = _locals.get(expr, -1) + + return value + + +func visit_in_inventory_expr(expr: ESCGrammarExprs.InInventory): + var arg = _evaluate(expr.get_identifier()) + + if arg is ESCObject: + arg = arg.global_id + + return null + + +func visit_is_expr(expr: ESCGrammarExprs.Is): + var arg = _evaluate(expr.get_identifier()) + + return true + + +func visit_binary_expr(expr: ESCGrammarExprs.Binary): + var left_part = _evaluate(expr.get_left()) + var right_part = _evaluate(expr.get_right()) + + match expr.get_operator().get_type(): + ESCTokenType.TokenType.EQUAL_EQUAL: + return _is_equal(left_part, right_part) + ESCTokenType.TokenType.BANG_EQUAL: + return not _is_equal(left_part, right_part) + ESCTokenType.TokenType.GREATER: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part > right_part + ESCTokenType.TokenType.GREATER_EQUAL: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part >= right_part + ESCTokenType.TokenType.LESS: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part < right_part + ESCTokenType.TokenType.LESS_EQUAL: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part <= right_part + ESCTokenType.TokenType.MINUS: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part - right_part + ESCTokenType.TokenType.PLUS: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part, false) + + if check: + return left_part + right_part + + check = _check_at_least_one_string(left_part, right_part) + + if check: + return str(left_part) + str(right_part) + + ESCSafeLogging.log_warn( + self, + "%s: Operands must be numbers or strings." % expr.get_operator().get_lexeme() + ) + ESCTokenType.TokenType.SLASH: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part / right_part + ESCTokenType.TokenType.STAR: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part * right_part + + return null + + +func visit_unary_expr(expr: ESCGrammarExprs.Unary): + var right_part = _evaluate(expr.get_right()) + + match expr.get_operator().get_type(): + ESCTokenType.TokenType.BANG, ESCTokenType.TokenType.NOT: + return not _is_truthy(right_part) + ESCTokenType.TokenType.MINUS: + var check = _check_is_number(right_part, expr.get_operator()) + return -right_part + + return null + + +func visit_variable_expr(expr: ESCGrammarExprs.Variable): + if expr.get_name().get_lexeme().begins_with("$"): + return _look_up_object(expr.get_name()) + + if expr.get_name().get_lexeme() in _builtin_functions: + return expr.get_name().get_lexeme() + + return look_up_variable(expr.get_name(), expr) + + +func visit_literal_expr(expr: ESCGrammarExprs.Literal): + return expr.get_value() + + +func visit_logical_expr(expr: ESCGrammarExprs.Logical): + var left = _evaluate(expr.get_left()) + + if expr.get_operator().get_type() == ESCTokenType.TokenType.OR: + if _is_truthy(left): + return left + else: + if not _is_truthy(left): + return left + + return _evaluate(expr.get_right()) + + +func visit_grouping_expr(expr: ESCGrammarExprs.Grouping): + return _evaluate(expr.get_expression()) + + +func resolve(expr: ESCGrammarExpr, depth: int): + _locals[expr] = depth + + +# Private methods +func _look_up_object(name: ESCToken): + var global_id: String = name.get_lexeme().substr(1) + + if global_id.to_upper() == CURRENT_PLAYER_KEYWORD: + pass + + return _look_up_object_by_global_id(global_id) + + +# We have no way of knowing what the value will actually be since this is called during static +# analysis. As such, we'll just return null. +func _look_up_object_by_global_id(global_id: String): + return null + + +# We don't care about the values of variables for static analysis purposes. +func look_up_variable(name: ESCToken, expr: ESCGrammarExpr): + var distance: int = _locals[expr] if _locals.has(expr) else -1 + + if distance == -1: + return _globals.get_value(name) + else: + return _environment.get_at(distance, name.get_lexeme()) + + +func _evaluate(expr: ESCGrammarExpr): + var ret = expr.accept(self) + + # TODO: Error handling + return ret + + +func _execute(stmt: ESCGrammarStmt): + var ret = stmt.accept(self) + + # TODO: Error handling + return ret + + +func _execute_block(statements: Array, env: ESCEnvironment): + var previous_env = _environment + var ret = null + + _environment = env + + for stmt in statements: + ret = _execute(stmt) + + _environment = previous_env + + return ret + + +func _is_truthy(value) -> bool: + if value == null: + return false + + if typeof(value) in [TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_BOOL]: + return bool(value) == true + + return false + + +func _is_equal(left_part, right_part) -> bool: + if not left_part and not right_part: + return true + + if not left_part: + return false + + return left_part == right_part + + +func _check_are_numbers(value_1, operator: ESCToken, value_2, strict: bool = true): + if typeof(value_1) in [TYPE_INT, TYPE_FLOAT] and typeof(value_2) in [TYPE_INT, TYPE_FLOAT]: + return true + + if strict: + ESCSafeLogging.log_warn( + self, + "%s: Operands must be numbers." % operator.get_lexeme() + ) + + return false + + +func _check_is_number(value, operator: ESCToken, strict: bool = true): + if typeof(value) in [TYPE_INT, TYPE_FLOAT]: + return true + + if strict: + ESCSafeLogging.log_warn( + self, + "%s: Operand must be number." % operator.get_lexeme() + ) + + return false + + +func _check_at_least_one_string(value_1, value_2): + return typeof(value_1) == TYPE_STRING || typeof(value_2) == TYPE_STRING diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd.uid b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd.uid new file mode 100644 index 0000000..4ee126f --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd.uid @@ -0,0 +1 @@ +uid://cv8ptpeiod6g2 |
