From 0ba6048ad3c945e2bd1d0114b5095535c22103ce Mon Sep 17 00:00:00 2001 From: George Marques Date: Wed, 19 Apr 2023 11:10:35 -0300 Subject: [PATCH] Add support for static variables in GDScript Which allows editable data associated with a particular class instead of the instance. Scripts with static variables are kept in memory indefinitely unless the `@static_unload` annotation is used or the `static_unload()` method is called on the GDScript. If the custom function `_static_init()` exists it will be called when the class is loaded, after the static variables are set. --- doc/classes/ProjectSettings.xml | 3 + modules/gdscript/doc_classes/@GDScript.xml | 6 + modules/gdscript/gdscript.cpp | 127 +++++++++- modules/gdscript/gdscript.h | 14 ++ modules/gdscript/gdscript_analyzer.cpp | 85 +++++-- modules/gdscript/gdscript_analyzer.h | 1 + modules/gdscript/gdscript_byte_codegen.h | 2 + modules/gdscript/gdscript_cache.cpp | 10 + modules/gdscript/gdscript_cache.h | 3 + modules/gdscript/gdscript_codegen.h | 1 + modules/gdscript/gdscript_compiler.cpp | 228 ++++++++++++++++-- modules/gdscript/gdscript_compiler.h | 3 + modules/gdscript/gdscript_editor.cpp | 9 + modules/gdscript/gdscript_function.h | 3 +- modules/gdscript/gdscript_parser.cpp | 86 ++++--- modules/gdscript/gdscript_parser.h | 21 +- modules/gdscript/gdscript_vm.cpp | 4 +- modules/gdscript/gdscript_warning.cpp | 4 + modules/gdscript/gdscript_warning.h | 2 + .../gdscript/tests/gdscript_test_runner.cpp | 16 +- .../static_constructor_with_return_type.gd | 5 + .../static_constructor_with_return_type.out | 2 + .../errors/static_var_init_non_static_call.gd | 9 + .../static_var_init_non_static_call.out | 2 + .../errors/static_constructor_not_static.gd | 5 + .../errors/static_constructor_not_static.out | 2 + .../static_constructor_returning_something.gd | 6 + ...static_constructor_returning_something.out | 2 + .../runtime/features/static_constructor.gd | 13 + .../runtime/features/static_constructor.out | 4 + .../runtime/features/static_variables.gd | 56 +++++ .../runtime/features/static_variables.out | 16 ++ .../runtime/features/static_variables_load.gd | 10 + .../features/static_variables_load.out | 2 + .../features/static_variables_other.gd | 11 + .../features/static_variables_other.out | 2 + 36 files changed, 689 insertions(+), 86 deletions(-) create mode 100644 modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.gd create mode 100644 modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.out create mode 100644 modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.gd create mode 100644 modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out create mode 100644 modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.out create mode 100644 modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.out create mode 100644 modules/gdscript/tests/scripts/runtime/features/static_constructor.gd create mode 100644 modules/gdscript/tests/scripts/runtime/features/static_constructor.out create mode 100644 modules/gdscript/tests/scripts/runtime/features/static_variables.gd create mode 100644 modules/gdscript/tests/scripts/runtime/features/static_variables.out create mode 100644 modules/gdscript/tests/scripts/runtime/features/static_variables_load.gd create mode 100644 modules/gdscript/tests/scripts/runtime/features/static_variables_load.out create mode 100644 modules/gdscript/tests/scripts/runtime/features/static_variables_other.gd create mode 100644 modules/gdscript/tests/scripts/runtime/features/static_variables_other.out diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 4a6368ca6c7..949253c9989 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -456,6 +456,9 @@ When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a function that is not a coroutine is called with await. + + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when the [code]@static_unload[/code] annotation is used in a script without any static variables. + When enabled, using a property, enum, or function that was renamed since Godot 3 will produce a hint if an error occurs. diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 08a48830545..d8f12f72329 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -622,6 +622,12 @@ [/codeblock] + + + + Make a script with static variables to not persist after all references are lost. If the script is loaded again the static variables will revert to their default values. + + diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index e74c2866e63..3bc11c69e9d 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -651,6 +651,49 @@ String GDScript::_get_debug_path() const { } } +Error GDScript::_static_init() { + if (static_initializer) { + Callable::CallError call_err; + static_initializer->call(nullptr, nullptr, 0, call_err); + if (call_err.error != Callable::CallError::CALL_OK) { + return ERR_CANT_CREATE; + } + } + Error err = OK; + for (KeyValue> &inner : subclasses) { + err = inner.value->_static_init(); + if (err) { + break; + } + } + return err; +} + +#ifdef TOOLS_ENABLED + +void GDScript::_save_old_static_data() { + old_static_variables_indices = static_variables_indices; + old_static_variables = static_variables; + for (KeyValue> &inner : subclasses) { + inner.value->_save_old_static_data(); + } +} + +void GDScript::_restore_old_static_data() { + for (KeyValue &E : old_static_variables_indices) { + if (static_variables_indices.has(E.key)) { + static_variables.write[static_variables_indices[E.key].index] = old_static_variables[E.value.index]; + } + } + old_static_variables_indices.clear(); + old_static_variables.clear(); + for (KeyValue> &inner : subclasses) { + inner.value->_restore_old_static_data(); + } +} + +#endif + Error GDScript::reload(bool p_keep_state) { if (reloading) { return OK; @@ -696,6 +739,14 @@ Error GDScript::reload(bool p_keep_state) { } } + bool can_run = ScriptServer::is_scripting_enabled() || is_tool(); + +#ifdef DEBUG_ENABLED + if (p_keep_state && can_run && is_valid()) { + _save_old_static_data(); + } +#endif + valid = false; GDScriptParser parser; Error err = parser.parse(source, path, false); @@ -726,7 +777,7 @@ Error GDScript::reload(bool p_keep_state) { return ERR_PARSE_ERROR; } - bool can_run = ScriptServer::is_scripting_enabled() || parser.is_tool(); + can_run = ScriptServer::is_scripting_enabled() || parser.is_tool(); GDScriptCompiler compiler; err = compiler.compile(&parser, this, p_keep_state); @@ -760,6 +811,19 @@ Error GDScript::reload(bool p_keep_state) { } #endif + if (can_run) { + err = _static_init(); + if (err) { + return err; + } + } + +#ifdef DEBUG_ENABLED + if (can_run && p_keep_state) { + _restore_old_static_data(); + } +#endif + reloading = false; return OK; } @@ -788,6 +852,10 @@ const Variant GDScript::get_rpc_config() const { return rpc_config; } +void GDScript::unload_static() const { + GDScriptCache::remove_script(fully_qualified_name); +} + Variant GDScript::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { GDScript *top = this; while (top) { @@ -824,6 +892,19 @@ bool GDScript::_get(const StringName &p_name, Variant &r_ret) const { return true; } } + + { + HashMap::ConstIterator E = static_variables_indices.find(p_name); + if (E) { + if (E->value.getter) { + Callable::CallError ce; + r_ret = const_cast(this)->callp(E->value.getter, nullptr, 0, ce); + return true; + } + r_ret = static_variables[E->value.index]; + return true; + } + } top = top->_base; } @@ -841,7 +922,32 @@ bool GDScript::_set(const StringName &p_name, const Variant &p_value) { set_source_code(p_value); reload(); } else { - return false; + const GDScript *top = this; + while (top) { + HashMap::ConstIterator E = static_variables_indices.find(p_name); + if (E) { + const GDScript::MemberInfo *member = &E->value; + Variant value = p_value; + if (member->data_type.has_type && !member->data_type.is_type(value)) { + const Variant *args = &p_value; + Callable::CallError err; + Variant::construct(member->data_type.builtin_type, value, &args, 1, err); + if (err.error != Callable::CallError::CALL_OK || !member->data_type.is_type(value)) { + return false; + } + } + if (member->setter) { + const Variant *args = &value; + Callable::CallError err; + callp(member->setter, &args, 1, err); + return err.error == Callable::CallError::CALL_OK; + } else { + static_variables.write[member->index] = value; + return true; + } + } + top = top->_base; + } } return true; @@ -1293,6 +1399,13 @@ void GDScript::clear(GDScript::ClearData *p_clear_data) { E.value.data_type.script_type_ref = Ref