diff --git a/.gitignore b/.gitignore index 99333856f14..753d2477caa 100644 --- a/.gitignore +++ b/.gitignore @@ -390,3 +390,5 @@ $RECYCLE.BIN/ *.msp *.lnk *.generated.props +.gdbinit +CLAUDE.md diff --git a/core/extension/extension_api_dump.cpp b/core/extension/extension_api_dump.cpp index d2763771291..d18489c003e 100644 --- a/core/extension/extension_api_dump.cpp +++ b/core/extension/extension_api_dump.cpp @@ -40,6 +40,9 @@ #include "core/templates/pair.h" #include "core/version.h" +// Forward declaration for struct type +class GDScriptStructInstance; + #ifdef TOOLS_ENABLED #include "editor/doc/editor_help.h" @@ -210,6 +213,7 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { { Variant::NODE_PATH, ptrsize_32, ptrsize_64, ptrsize_32, ptrsize_64 }, { Variant::RID, sizeof(uint64_t), sizeof(uint64_t), sizeof(uint64_t), sizeof(uint64_t) }, { Variant::OBJECT, ptrsize_32, ptrsize_64, ptrsize_32, ptrsize_64 }, + { Variant::STRUCT, ptrsize_32, ptrsize_64, ptrsize_32, ptrsize_64 }, { Variant::CALLABLE, sizeof(Callable), sizeof(Callable), sizeof(Callable), sizeof(Callable) }, // Hardcoded align. { Variant::SIGNAL, sizeof(Signal), sizeof(Signal), sizeof(Signal), sizeof(Signal) }, // Hardcoded align. { Variant::DICTIONARY, ptrsize_32, ptrsize_64, ptrsize_32, ptrsize_64 }, @@ -252,6 +256,7 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { static_assert(type_size_array[Variant::NODE_PATH][sizeof(void *)] == sizeof(NodePath), "Size of NodePath mismatch"); static_assert(type_size_array[Variant::RID][sizeof(void *)] == sizeof(RID), "Size of RID mismatch"); static_assert(type_size_array[Variant::OBJECT][sizeof(void *)] == sizeof(Object *), "Size of Object mismatch"); + static_assert(type_size_array[Variant::STRUCT][sizeof(void *)] == sizeof(GDScriptStructInstance *), "Size of Struct mismatch"); static_assert(type_size_array[Variant::CALLABLE][sizeof(void *)] == sizeof(Callable), "Size of Callable mismatch"); static_assert(type_size_array[Variant::SIGNAL][sizeof(void *)] == sizeof(Signal), "Size of Signal mismatch"); static_assert(type_size_array[Variant::DICTIONARY][sizeof(void *)] == sizeof(Dictionary), "Size of Dictionary mismatch"); diff --git a/core/extension/gdextension_interface.cpp b/core/extension/gdextension_interface.cpp index 988446e4cdf..3f8c176e79d 100644 --- a/core/extension/gdextension_interface.cpp +++ b/core/extension/gdextension_interface.cpp @@ -817,6 +817,11 @@ static GDExtensionPtrOperatorEvaluator gdextension_variant_get_ptr_operator_eval return (GDExtensionPtrOperatorEvaluator)Variant::get_ptr_operator_evaluator(Variant::Operator(p_operator), Variant::Type(p_type_a), Variant::Type(p_type_b)); } static GDExtensionPtrBuiltInMethod gdextension_variant_get_ptr_builtin_method(GDExtensionVariantType p_type, GDExtensionConstStringNamePtr p_method, GDExtensionInt p_hash) { + // STRUCT type doesn't support built-in methods + if (p_type == (int)Variant::STRUCT) { + return nullptr; + } + const StringName method = *reinterpret_cast(p_method); uint32_t hash = Variant::get_builtin_method_hash(Variant::Type(p_type), method); if (hash != p_hash) { @@ -827,6 +832,11 @@ static GDExtensionPtrBuiltInMethod gdextension_variant_get_ptr_builtin_method(GD return (GDExtensionPtrBuiltInMethod)Variant::get_ptr_builtin_method(Variant::Type(p_type), method); } static GDExtensionPtrConstructor gdextension_variant_get_ptr_constructor(GDExtensionVariantType p_type, int32_t p_constructor) { + // STRUCT type doesn't support constructors (yet) + if (p_type == (int)Variant::STRUCT) { + return nullptr; + } + return (GDExtensionPtrConstructor)Variant::get_ptr_constructor(Variant::Type(p_type), p_constructor); } static GDExtensionPtrDestructor gdextension_variant_get_ptr_destructor(GDExtensionVariantType p_type) { diff --git a/core/io/json.cpp b/core/io/json.cpp index 96ecc9f8a7b..4afa866eb09 100644 --- a/core/io/json.cpp +++ b/core/io/json.cpp @@ -35,6 +35,40 @@ #include "core/config/engine.h" #include "core/object/script_language.h" #include "core/variant/container_type_validate.h" +#include "core/variant/variant_internal.h" + +// GDScript struct serialization support +// Guarded with MODULE_GDSCRIPT_ENABLED since core/io can't depend on modules/gdscript +#ifdef MODULE_GDSCRIPT_ENABLED +// Forward declaration for GDScript struct serialization +class GDScriptStructInstance; +class GDScriptStruct; + +// We need to declare these methods without including the GDScript headers +// This is a bit of a hack, but necessary due to layering (core/io can't include modules/gdscript) +// In the future, this should be replaced with a proper Variant-level API + +extern "C" { +// These will be defined in gdscript.cpp with C linkage to avoid name mangling issues +void gdscript_struct_instance_serialize(const GDScriptStructInstance *p_instance, Dictionary &r_dict); +bool gdscript_struct_instance_get_type_name(const GDScriptStructInstance *p_instance, String &r_name); +} + +// Helper function to serialize a struct +static Dictionary _serialize_struct(const GDScriptStructInstance *p_instance) { + ERR_FAIL_NULL_V(p_instance, Dictionary()); + + // Call the GDScript helper function + Dictionary dict; + gdscript_struct_instance_serialize(p_instance, dict); + return dict; +} +#else +// Stub implementation when GDScript module is disabled +static Dictionary _serialize_struct(const void *p_instance) { + ERR_FAIL_V_MSG(Dictionary(), "STRUCT serialization requires MODULE_GDSCRIPT_ENABLED."); +} +#endif // MODULE_GDSCRIPT_ENABLED const char *JSON::tk_name[TK_MAX] = { "'{'", @@ -824,6 +858,50 @@ Variant JSON::_from_native(const Variant &p_variant, bool p_full_objects, int p_ return ret; } break; +#ifdef MODULE_GDSCRIPT_ENABLED + case Variant::STRUCT: { + // Serialize struct as tagged dictionary with __type__ metadata + // This allows round-trip deserialization + const GDScriptStructInstance *struct_instance = reinterpret_cast(VariantInternal::get_struct(&p_variant)); + ERR_FAIL_NULL_V(struct_instance, Variant()); + + Dictionary ret; + ret[TYPE] = Variant::get_type_name(p_variant.get_type()); + + // Get the serialized data from the struct instance using helper + Dictionary struct_data = _serialize_struct(struct_instance); + + // Ensure __type__ field exists (it should from serialize()) + if (!struct_data.has("__type__")) { + ERR_FAIL_V_MSG(Variant(), "Struct serialization failed: missing __type__ field."); + } + + // Encode field values using _from_native to ensure proper JSON encoding + // This handles non-JSON-native types (nested structs, objects, typed arrays, etc.) + Dictionary encoded_struct_data; + encoded_struct_data["__type__"] = struct_data["__type__"]; // Copy type identifier as-is + + for (const KeyValue &kv : struct_data) { + if (kv.key == "__type__") { + continue; // Already copied above + } + // Encode the value using _from_native, keys are strings (field names) and don't need encoding + encoded_struct_data[kv.key] = _from_native(kv.value, p_full_objects, p_depth + 1); + } + + // Wrap the encoded serialized data in the expected format + Array args; + args.push_back(encoded_struct_data); + ret[ARGS] = args; + + return ret; + } break; +#else + case Variant::STRUCT: { + ERR_FAIL_V_MSG(Variant(), "STRUCT serialization requires MODULE_GDSCRIPT_ENABLED."); + } break; +#endif // MODULE_GDSCRIPT_ENABLED + case Variant::DICTIONARY: { const Dictionary dict = p_variant; @@ -1297,6 +1375,44 @@ Variant JSON::_to_native(const Variant &p_json, bool p_allow_objects, int p_dept // Nothing to do at this stage. `Object` should be treated as a class, not as a built-in type. } break; +#ifdef MODULE_GDSCRIPT_ENABLED + case Variant::STRUCT: { + // Deserialize struct from tagged dictionary + LOAD_ARGS(); + + ERR_FAIL_COND_V_MSG(args.size() != 1, Variant(), "Invalid struct data: expected single dictionary argument."); + + Dictionary encoded_data = args[0]; + ERR_FAIL_COND_V_MSG(!encoded_data.has("__type__"), Variant(), "Invalid struct data: missing __type__ field."); + + String type_name = encoded_data["__type__"]; + ERR_FAIL_COND_V_MSG(type_name.is_empty(), Variant(), "Invalid struct data: empty __type__ field."); + + // Decode field values using _to_native to properly reconstruct non-JSON-native types + // This handles nested structs, objects, typed arrays, etc. + Dictionary decoded_struct_data; + decoded_struct_data["__type__"] = encoded_data["__type__"]; // Copy type identifier as-is + + for (const KeyValue &kv : encoded_data) { + if (kv.key == "__type__") { + continue; // Already copied above + } + // Decode the value using _to_native, keys are strings (field names) and don't need decoding + decoded_struct_data[kv.key] = _to_native(kv.value, p_allow_objects, p_depth + 1); + } + + // Use ScriptServer to create the struct instance with decoded data + Variant struct_instance = ScriptServer::create_struct_instance(type_name, decoded_struct_data); + ERR_FAIL_COND_V_MSG(struct_instance.get_type() != Variant::STRUCT, Variant(), vformat("Failed to create struct instance for type '%s'.", type_name)); + + return struct_instance; + } break; +#else + case Variant::STRUCT: { + ERR_FAIL_V_MSG(Variant(), "STRUCT deserialization requires MODULE_GDSCRIPT_ENABLED."); + } break; +#endif // MODULE_GDSCRIPT_ENABLED + case Variant::DICTIONARY: { LOAD_ARGS_CHECK_FACTOR(2); diff --git a/core/io/marshalls.cpp b/core/io/marshalls.cpp index 689ea4d6812..ac4c74620c1 100644 --- a/core/io/marshalls.cpp +++ b/core/io/marshalls.cpp @@ -40,6 +40,18 @@ #include #include +// GDScript struct serialization support +// Guarded with MODULE_GDSCRIPT_ENABLED since core/io can't depend on modules/gdscript +#ifdef MODULE_GDSCRIPT_ENABLED + +extern "C" { +// These functions are defined in modules/gdscript/gdscript.cpp with C linkage +Error gdscript_variant_encode_struct(const Variant &p_variant, uint8_t *r_buffer, int &r_len); +Error gdscript_variant_decode_struct(const uint8_t *p_buffer, int p_len, int *r_len, Variant &r_variant); +} + +#endif // MODULE_GDSCRIPT_ENABLED + void EncodedObjectAsID::_bind_methods() { ClassDB::bind_method(D_METHOD("set_object_id", "id"), &EncodedObjectAsID::set_object_id); ClassDB::bind_method(D_METHOD("get_object_id"), &EncodedObjectAsID::get_object_id); @@ -1296,6 +1308,17 @@ Error decode_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int r_variant = varray; } break; +#ifdef MODULE_GDSCRIPT_ENABLED + case Variant::STRUCT: { + // Decode struct via GDScript helper function + Error err = gdscript_variant_decode_struct(buf, len, r_len, r_variant); + ERR_FAIL_COND_V(err, err); + } break; +#else + case Variant::STRUCT: { + ERR_FAIL_V(ERR_BUG); + } break; +#endif default: { ERR_FAIL_V(ERR_BUG); } @@ -2124,6 +2147,17 @@ Error encode_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len, bo r_len += sizeof(real_t) * 4 * len; } break; +#ifdef MODULE_GDSCRIPT_ENABLED + case Variant::STRUCT: { + // Encode struct via GDScript helper function + Error err = gdscript_variant_encode_struct(p_variant, buf, r_len); + ERR_FAIL_COND_V(err, err); + } break; +#else + case Variant::STRUCT: { + ERR_FAIL_V(ERR_BUG); + } break; +#endif default: { ERR_FAIL_V(ERR_BUG); } diff --git a/core/object/object.cpp b/core/object/object.cpp index 42c54cb0791..be35f1b718f 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -848,6 +848,20 @@ Variant Object::callp(const StringName &p_method, const Variant **p_args, int p_ Variant ret; OBJ_DEBUG_LOCK +#ifdef MODULE_GDSCRIPT_ENABLED + // Special case for GDScriptStructClass to support struct constructors + // This is needed because GDScriptStructClass::callp needs to be called directly, + // but callp is not virtual so Object::callp doesn't dispatch to it. + if (get_class_name() == StringName("GDScriptStructClass")) { + // Forward to GDScriptStructClass::callp by using a Callable + Callable callable = Callable(this, p_method); + if (callable.is_valid()) { + callable.callp(p_args, p_argcount, ret, r_error); + return ret; + } + } +#endif + if (script_instance) { ret = script_instance->callp(p_method, p_args, p_argcount, r_error); // Force jump table. diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index 63f4e38ff16..43444ec1a47 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -550,6 +550,48 @@ void ScriptServer::save_global_classes() { ProjectSettings::get_singleton()->store_global_class_list(gcarr); } +/***************** STRUCT SERIALIZATION *****************/ + +Variant ScriptServer::create_struct_instance(const String &p_fully_qualified_name, const Dictionary &p_data) { + MutexLock lock(languages_mutex); + if (!languages_ready) { + ERR_FAIL_V_MSG(Variant(), "Cannot create struct instance: languages not initialized."); + } + + // Try each language to see if it can create the struct + for (int i = 0; i < _language_count; i++) { + ScriptLanguage *lang = _languages[i]; + if (lang && lang->can_create_struct_by_name()) { + Variant result = lang->create_struct_by_name(p_fully_qualified_name, p_data); + if (result.get_type() != Variant::NIL) { + return result; + } + } + } + + ERR_FAIL_V_MSG(Variant(), vformat("Cannot create struct instance: no language supports struct '%s'.", p_fully_qualified_name)); +} + +bool ScriptServer::global_struct_exists(const String &p_fully_qualified_name) { + MutexLock lock(languages_mutex); + if (!languages_ready) { + return false; + } + + // Check if any language can create this struct + for (int i = 0; i < _language_count; i++) { + ScriptLanguage *lang = _languages[i]; + if (lang && lang->can_create_struct_by_name()) { + // Try to create with empty data to check if it exists + Variant result = lang->create_struct_by_name(p_fully_qualified_name, Dictionary()); + if (result.get_type() != Variant::NIL) { + return true; + } + } + } + return false; +} + Vector> ScriptServer::capture_script_backtraces(bool p_include_variables) { if (is_program_exiting) { return Vector>(); diff --git a/core/object/script_language.h b/core/object/script_language.h index 17ddd5679ed..1726650c8e5 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -104,6 +104,10 @@ class ScriptServer { static void get_inheriters_list(const StringName &p_base_type, List *r_classes); static void save_global_classes(); + // Struct serialization utilities (for GDScript) + static Variant create_struct_instance(const String &p_fully_qualified_name, const Dictionary &p_data); + static bool global_struct_exists(const String &p_fully_qualified_name); + static Vector> capture_script_backtraces(bool p_include_variables = false); static void init_languages(); @@ -446,6 +450,13 @@ class ScriptLanguage : public Object { virtual bool handles_global_class_type(const String &p_type) const { return false; } virtual String get_global_class_name(const String &p_path, String *r_base_type = nullptr, String *r_icon_path = nullptr, bool *r_is_abstract = nullptr, bool *r_is_tool = nullptr) const { return String(); } + /* STRUCT SERIALIZATION */ + // These methods allow serialization systems to create struct instances without knowing about GDScript + virtual bool can_create_struct_by_name() const { return false; } + virtual Variant create_struct_by_name(const String &p_fully_qualified_name, const Dictionary &p_data) { return Variant(); } + virtual Dictionary struct_to_dict(const Variant &p_struct) const { return Dictionary(); } + virtual void get_struct_property_list(const Variant &p_struct, List *p_list) const {} + virtual ~ScriptLanguage() {} }; diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp index 5504104074e..4d391ff7217 100644 --- a/core/variant/variant.cpp +++ b/core/variant/variant.cpp @@ -37,6 +37,9 @@ #include "core/io/resource.h" #include "core/math/math_funcs.h" #include "core/variant/variant_parser.h" +#ifdef MODULE_GDSCRIPT_ENABLED +#include "modules/gdscript/gdscript_struct.h" +#endif PagedAllocator Variant::Pools::_bucket_small; PagedAllocator Variant::Pools::_bucket_medium; @@ -119,6 +122,9 @@ String Variant::get_type_name(Variant::Type p_type) { case OBJECT: { return "Object"; } + case STRUCT: { + return "Struct"; + } case CALLABLE: { return "Callable"; } @@ -1311,6 +1317,15 @@ void Variant::reference(const Variant &p_variant) { _data.packed_array = PackedArrayRef::create(); } } break; +#ifdef MODULE_GDSCRIPT_ENABLED + case STRUCT: { + // COW: Copy the wrapper by value. + // The Ref<> inside handles reference counting automatically. + // Placement new to copy the wrapper into _mem. + const GDScriptStructInstance *src_wrapper = reinterpret_cast(p_variant._data._mem); + new (_data._mem) GDScriptStructInstance(*src_wrapper); + } break; +#endif default: { } } @@ -1480,6 +1495,16 @@ void Variant::_clear_internal() { case PACKED_VECTOR4_ARRAY: { PackedArrayRefBase::destroy(_data.packed_array); } break; +#ifdef MODULE_GDSCRIPT_ENABLED + case STRUCT: { + // COW: Destroy the wrapper by calling its destructor. + // The Ref<> destructor handles reference counting automatically. + GDScriptStructInstance *wrapper = reinterpret_cast(_data._mem); + wrapper->~GDScriptStructInstance(); + // Clear the memory + memset(_data._mem, 0, sizeof(_data._mem)); + } break; +#endif default: { // Not needed, there is no point. The following do not allocate memory: // VECTOR2, VECTOR3, VECTOR4, RECT2, PLANE, QUATERNION, COLOR. @@ -1737,6 +1762,18 @@ String Variant::stringify(int recursion_count) const { const ::RID &s = *reinterpret_cast(_data._mem); return "RID(" + itos(s.get_id()) + ")"; } +#ifdef MODULE_GDSCRIPT_ENABLED + case STRUCT: { + // Get the struct wrapper and convert to string + const GDScriptStructInstance *wrapper = reinterpret_cast(_data._mem); + if (wrapper && wrapper->is_valid()) { + // Simple string representation for now to avoid crashes + // TODO: Add proper field serialization + return String("<") + String(wrapper->get_struct_name()) + " struct>"; + } + return ""; + } +#endif default: { return "<" + get_type_name(type) + ">"; } @@ -2790,6 +2827,16 @@ void Variant::operator=(const Variant &p_variant) { case PACKED_VECTOR4_ARRAY: { _data.packed_array = PackedArrayRef::reference_from(_data.packed_array, p_variant._data.packed_array); } break; +#ifdef MODULE_GDSCRIPT_ENABLED + case STRUCT: { + // COW: Destroy old wrapper, copy new wrapper by value + GDScriptStructInstance *old_wrapper = reinterpret_cast(_data._mem); + old_wrapper->~GDScriptStructInstance(); + + const GDScriptStructInstance *new_wrapper = reinterpret_cast(p_variant._data._mem); + new (_data._mem) GDScriptStructInstance(*new_wrapper); + } break; +#endif default: { } } @@ -3490,7 +3537,7 @@ void Variant::construct_from_string(const String &p_string, Variant &r_value, Ob String Variant::get_construct_string() const { String vars; - VariantWriter::write_to_string(*this, vars, nullptr, nullptr, true, true); + VariantWriter::write_to_string(*this, vars); return vars; } diff --git a/core/variant/variant.h b/core/variant/variant.h index b7e84054e4f..df6dafcb110 100644 --- a/core/variant/variant.h +++ b/core/variant/variant.h @@ -67,6 +67,7 @@ class Object; class RefCounted; +class GDScriptStructInstance; template class Ref; @@ -123,6 +124,7 @@ class Variant { NODE_PATH, RID, OBJECT, + STRUCT, CALLABLE, SIGNAL, DICTIONARY, @@ -175,6 +177,12 @@ class Variant { friend struct _VariantCall; friend class VariantInternal; + friend class GDScriptStructClass; // Needed for proper struct construction + friend class GDScriptStruct; // Needed for struct instance creation + friend class GDScriptLanguage; // Needed for struct serialization and property list +#ifdef MODULE_GDSCRIPT_ENABLED + friend struct VariantKeyedSetGetStruct; // Needed for struct keyed getter/setter +#endif // Variant takes 24 bytes when real_t is float, and 40 bytes if double. // It only allocates extra memory for AABB/Transform2D (24, 48 if double), // Basis/Transform3D (48, 96 if double), Projection (64, 128 if double), @@ -310,6 +318,7 @@ class Variant { true, //NODE_PATH, false, //RID, true, //OBJECT, + true, //STRUCT, true, //CALLABLE, true, //SIGNAL, true, //DICTIONARY, diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index 0a7bb7d5988..8158519f763 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -1376,6 +1376,12 @@ void Variant::callp(const StringName &p_method, const Variant **p_args, int p_ar } else { r_error.error = Callable::CallError::CALL_OK; + // STRUCT type doesn't have built-in methods + if (type == STRUCT) { + r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; + return; + } + const VariantBuiltInMethodInfo *imf = builtin_method_info[type].getptr(p_method); if (!imf) { @@ -1408,6 +1414,12 @@ void Variant::call_const(const StringName &p_method, const Variant **p_args, int } else { r_error.error = Callable::CallError::CALL_OK; + // STRUCT type doesn't have built-in methods + if (type == STRUCT) { + r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; + return; + } + const VariantBuiltInMethodInfo *imf = builtin_method_info[type].getptr(p_method); if (!imf) { @@ -1427,6 +1439,12 @@ void Variant::call_const(const StringName &p_method, const Variant **p_args, int void Variant::call_static(Variant::Type p_type, const StringName &p_method, const Variant **p_args, int p_argcount, Variant &r_ret, Callable::CallError &r_error) { r_error.error = Callable::CallError::CALL_OK; + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; + return; + } + const VariantBuiltInMethodInfo *imf = builtin_method_info[p_type].getptr(p_method); if (!imf) { @@ -1452,16 +1470,33 @@ bool Variant::has_method(const StringName &p_method) const { return obj->has_method(p_method); } + // STRUCT type doesn't have built-in methods + if (type == STRUCT) { + return false; + } + return builtin_method_info[type].has(p_method); } bool Variant::has_builtin_method(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, false); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return false; + } + return builtin_method_info[p_type].has(p_method); } Variant::ValidatedBuiltInMethod Variant::get_validated_builtin_method(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, nullptr); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return nullptr; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, nullptr); return method->validated_call; @@ -1469,6 +1504,12 @@ Variant::ValidatedBuiltInMethod Variant::get_validated_builtin_method(Variant::T Variant::PTRBuiltInMethod Variant::get_ptr_builtin_method(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, nullptr); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return nullptr; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, nullptr); return method->ptrcall; @@ -1476,6 +1517,12 @@ Variant::PTRBuiltInMethod Variant::get_ptr_builtin_method(Variant::Type p_type, MethodInfo Variant::get_builtin_method_info(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, MethodInfo()); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return MethodInfo(); + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, MethodInfo()); return method->get_method_info(p_method); @@ -1483,6 +1530,12 @@ MethodInfo Variant::get_builtin_method_info(Variant::Type p_type, const StringNa int Variant::get_builtin_method_argument_count(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, 0); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return 0; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, 0); return method->argument_count; @@ -1490,6 +1543,12 @@ int Variant::get_builtin_method_argument_count(Variant::Type p_type, const Strin Variant::Type Variant::get_builtin_method_argument_type(Variant::Type p_type, const StringName &p_method, int p_argument) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, Variant::NIL); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return Variant::NIL; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, Variant::NIL); ERR_FAIL_INDEX_V(p_argument, method->argument_count, Variant::NIL); @@ -1498,6 +1557,11 @@ Variant::Type Variant::get_builtin_method_argument_type(Variant::Type p_type, co String Variant::get_builtin_method_argument_name(Variant::Type p_type, const StringName &p_method, int p_argument) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, String()); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return String(); + } const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, String()); #ifdef DEBUG_ENABLED @@ -1510,6 +1574,12 @@ String Variant::get_builtin_method_argument_name(Variant::Type p_type, const Str Vector Variant::get_builtin_method_default_arguments(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, Vector()); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return Vector(); + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, Vector()); return method->default_arguments; @@ -1517,6 +1587,12 @@ Vector Variant::get_builtin_method_default_arguments(Variant::Type p_ty bool Variant::has_builtin_method_return_value(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, false); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return false; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, false); return method->has_return_type; @@ -1524,6 +1600,12 @@ bool Variant::has_builtin_method_return_value(Variant::Type p_type, const String void Variant::get_builtin_method_list(Variant::Type p_type, List *p_list) { ERR_FAIL_INDEX(p_type, Variant::VARIANT_MAX); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return; + } + for (const StringName &E : builtin_method_names[p_type]) { p_list->push_back(E); } @@ -1531,11 +1613,23 @@ void Variant::get_builtin_method_list(Variant::Type p_type, List *p_ int Variant::get_builtin_method_count(Variant::Type p_type) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, -1); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return 0; + } + return builtin_method_names[p_type].size(); } Variant::Type Variant::get_builtin_method_return_type(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, Variant::NIL); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return Variant::NIL; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, Variant::NIL); return method->return_type; @@ -1543,6 +1637,12 @@ Variant::Type Variant::get_builtin_method_return_type(Variant::Type p_type, cons bool Variant::is_builtin_method_const(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, false); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return false; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, false); return method->is_const; @@ -1550,6 +1650,12 @@ bool Variant::is_builtin_method_const(Variant::Type p_type, const StringName &p_ bool Variant::is_builtin_method_static(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, false); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return false; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, false); return method->is_static; @@ -1557,6 +1663,12 @@ bool Variant::is_builtin_method_static(Variant::Type p_type, const StringName &p bool Variant::is_builtin_method_vararg(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, false); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return false; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, false); return method->is_vararg; @@ -1564,6 +1676,12 @@ bool Variant::is_builtin_method_vararg(Variant::Type p_type, const StringName &p uint32_t Variant::get_builtin_method_hash(Variant::Type p_type, const StringName &p_method) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, 0); + + // STRUCT type doesn't have built-in methods + if (p_type == STRUCT) { + return 0; + } + const VariantBuiltInMethodInfo *method = builtin_method_info[p_type].getptr(p_method); ERR_FAIL_NULL_V(method, 0); uint32_t hash = hash_murmur3_one_32(method->is_const); @@ -1588,6 +1706,11 @@ void Variant::get_method_list(List *p_list) const { obj->get_method_list(p_list); } } else { + // STRUCT type doesn't have built-in methods + if (type == STRUCT) { + return; + } + for (const StringName &E : builtin_method_names[type]) { const VariantBuiltInMethodInfo *method = builtin_method_info[type].getptr(E); ERR_CONTINUE(!method); diff --git a/core/variant/variant_construct.cpp b/core/variant/variant_construct.cpp index 5d684c96a23..50345034cef 100644 --- a/core/variant/variant_construct.cpp +++ b/core/variant/variant_construct.cpp @@ -265,6 +265,18 @@ void Variant::_unregister_variant_constructors() { void Variant::construct(Variant::Type p_type, Variant &base, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { ERR_FAIL_INDEX(p_type, Variant::VARIANT_MAX); + + // STRUCT type - special handling since structs are dynamically defined + if (p_type == STRUCT) { + // Actual struct construction happens through GDScriptStructClass::new() + // Return a NIL variant and set error + base = Variant(); + r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; + r_error.argument = 0; + r_error.expected = 0; + return; + } + uint32_t s = construct_data[p_type].size(); for (uint32_t i = 0; i < s; i++) { int argc = construct_data[p_type][i].argument_count; @@ -297,30 +309,59 @@ int Variant::get_constructor_count(Variant::Type p_type) { Variant::ValidatedConstructor Variant::get_validated_constructor(Variant::Type p_type, int p_constructor) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, nullptr); + + // STRUCT type doesn't have constructors (yet) + if (p_type == STRUCT) { + return nullptr; + } + ERR_FAIL_INDEX_V(p_constructor, (int)construct_data[p_type].size(), nullptr); return construct_data[p_type][p_constructor].validated_construct; } Variant::PTRConstructor Variant::get_ptr_constructor(Variant::Type p_type, int p_constructor) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, nullptr); + + // STRUCT type doesn't have constructors (yet) + if (p_type == STRUCT) { + return nullptr; + } + ERR_FAIL_INDEX_V(p_constructor, (int)construct_data[p_type].size(), nullptr); return construct_data[p_type][p_constructor].ptr_construct; } int Variant::get_constructor_argument_count(Variant::Type p_type, int p_constructor) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, -1); + + // STRUCT type doesn't have constructors (yet) + if (p_type == STRUCT) { + return -1; + } + ERR_FAIL_INDEX_V(p_constructor, (int)construct_data[p_type].size(), -1); return construct_data[p_type][p_constructor].argument_count; } Variant::Type Variant::get_constructor_argument_type(Variant::Type p_type, int p_constructor, int p_argument) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, Variant::VARIANT_MAX); + + // STRUCT type doesn't have constructors (yet) + if (p_type == STRUCT) { + return Variant::NIL; + } + ERR_FAIL_INDEX_V(p_constructor, (int)construct_data[p_type].size(), Variant::VARIANT_MAX); return construct_data[p_type][p_constructor].get_argument_type(p_argument); } String Variant::get_constructor_argument_name(Variant::Type p_type, int p_constructor, int p_argument) { ERR_FAIL_INDEX_V(p_type, Variant::VARIANT_MAX, String()); + + // STRUCT type doesn't have constructors (yet) + if (p_type == STRUCT) { + return String(); + } ERR_FAIL_INDEX_V(p_constructor, (int)construct_data[p_type].size(), String()); return construct_data[p_type][p_constructor].arg_names[p_argument]; } @@ -328,6 +369,11 @@ String Variant::get_constructor_argument_name(Variant::Type p_type, int p_constr void Variant::get_constructor_list(Type p_type, List *r_list) { ERR_FAIL_INDEX(p_type, Variant::VARIANT_MAX); + // STRUCT type doesn't have constructors (yet) + if (p_type == STRUCT) { + return; + } + MethodInfo mi; mi.return_val.type = p_type; mi.name = get_type_name(p_type); diff --git a/core/variant/variant_internal.h b/core/variant/variant_internal.h index f19f2e2a6d9..d58ec5f9ace 100644 --- a/core/variant/variant_internal.h +++ b/core/variant/variant_internal.h @@ -123,6 +123,9 @@ class VariantInternal { case Variant::OBJECT: init_object(v); break; + case Variant::STRUCT: + init_struct(v); + break; default: break; } @@ -215,6 +218,10 @@ class VariantInternal { _FORCE_INLINE_ static const ObjectID get_object_id(const Variant *v) { return v->_get_obj().id; } + // Struct access - stores wrapper by value in _mem + _FORCE_INLINE_ static void *get_struct(Variant *v) { return v->_data._mem; } + _FORCE_INLINE_ static const void *get_struct(const Variant *v) { return v->_data._mem; } + template _FORCE_INLINE_ static void init_generic(Variant *v) { v->type = GetTypeInfo::VARIANT_TYPE; @@ -325,6 +332,12 @@ class VariantInternal { object_reset_data(v); v->type = Variant::OBJECT; } + _FORCE_INLINE_ static void init_struct(Variant *v) { + // Zero-initialize the wrapper memory + // The default constructor will be called via placement new when the struct is actually constructed + memset(v->_data._mem, 0, sizeof(v->_data._mem)); + v->type = Variant::STRUCT; + } _FORCE_INLINE_ static void clear(Variant *v) { v->clear(); @@ -438,6 +451,8 @@ class VariantInternal { return get_vector4_array(v); case Variant::OBJECT: return get_object(v); + case Variant::STRUCT: + return get_struct(v); case Variant::VARIANT_MAX: ERR_FAIL_V(nullptr); } @@ -524,6 +539,8 @@ class VariantInternal { return get_vector4_array(v); case Variant::OBJECT: return get_object(v); + case Variant::STRUCT: + return get_struct(v); case Variant::VARIANT_MAX: ERR_FAIL_V(nullptr); } @@ -778,6 +795,22 @@ struct VariantGetInternalPtr { static const PackedVector4Array *get_ptr(const Variant *v) { return VariantInternal::get_vector4_array(v); } }; +// Forward declaration for GDScript struct template specialization +// The actual type is defined in modules/gdscript/gdscript_struct.h +class GDScriptStructInstance; + +template <> +struct VariantGetInternalPtr { + static GDScriptStructInstance *get_ptr(Variant *v) { + // STRUCT type stores the wrapper by value in _data._mem + return reinterpret_cast(VariantInternal::get_struct(v)); + } + static const GDScriptStructInstance *get_ptr(const Variant *v) { + // STRUCT type stores the wrapper by value in _data._mem + return reinterpret_cast(VariantInternal::get_struct(v)); + } +}; + template struct VariantInternalAccessor; diff --git a/core/variant/variant_parser.cpp b/core/variant/variant_parser.cpp index 7f6a1c772e3..6f3d1d98a8a 100644 --- a/core/variant/variant_parser.cpp +++ b/core/variant/variant_parser.cpp @@ -38,6 +38,9 @@ #include "core/io/resource_uid.h" #include "core/object/script_language.h" #include "core/string/string_buffer.h" +#ifdef MODULE_GDSCRIPT_ENABLED +#include "modules/gdscript/gdscript_struct.h" +#endif char32_t VariantParser::Stream::get_char() { // is within buffer? @@ -2172,6 +2175,31 @@ Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_str } break; // Misc types. +#ifdef MODULE_GDSCRIPT_ENABLED + case Variant::STRUCT: { + // Serialize struct as a Dictionary with __type__ metadata + // JSON format supports round-trip, binary format does not yet + const GDScriptStructInstance *struct_instance = VariantGetInternalPtr::get_ptr(&p_variant); + if (struct_instance) { + if (unlikely(p_recursion_count > MAX_RECURSION)) { + ERR_PRINT("Max recursion reached"); + p_store_string_func(p_store_string_ud, "null"); + } else { + p_recursion_count++; + Dictionary dict = struct_instance->serialize(); + // Recursively write the dictionary with incremented recursion count + write(dict, p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, p_recursion_count, p_compat, p_full_objects); + } + } else { + p_store_string_func(p_store_string_ud, "null"); + } + } break; +#else + case Variant::STRUCT: { + // Structs not available without GDScript module + p_store_string_func(p_store_string_ud, "null"); + } break; +#endif case Variant::COLOR: { Color c = p_variant; p_store_string_func(p_store_string_ud, "Color(" + rtos_fix(c.r, p_compat) + ", " + rtos_fix(c.g, p_compat) + ", " + rtos_fix(c.b, p_compat) + ", " + rtos_fix(c.a, p_compat) + ")"); diff --git a/core/variant/variant_setget.cpp b/core/variant/variant_setget.cpp index b39ac7defa0..e20eac4fb65 100644 --- a/core/variant/variant_setget.cpp +++ b/core/variant/variant_setget.cpp @@ -34,6 +34,10 @@ #include "variant_callable.h" #include "core/io/resource.h" +#include "core/object/script_language.h" +#ifdef MODULE_GDSCRIPT_ENABLED +#include "modules/gdscript/gdscript_struct.h" +#endif struct VariantSetterGetterInfo { void (*setter)(Variant *base, const Variant *value, bool &valid); @@ -257,6 +261,16 @@ void Variant::set_named(const StringName &p_member, const Variant &p_value, bool obj->set(p_member, p_value, &r_valid); return; } +#ifdef MODULE_GDSCRIPT_ENABLED + } else if (type == Variant::STRUCT) { + // _data._mem contains the struct wrapper by value + GDScriptStructInstance *wrapper = reinterpret_cast(_data._mem); + if (wrapper) { + r_valid = wrapper->set(p_member, p_value); + return; + } + r_valid = false; +#endif } else if (type == Variant::DICTIONARY) { Dictionary &dict = *VariantGetInternalPtr::get_ptr(this); r_valid = dict.set(p_member, p_value); @@ -288,6 +302,21 @@ Variant Variant::get_named(const StringName &p_member, bool &r_valid) const { return obj->get(p_member, &r_valid); } } break; +#ifdef MODULE_GDSCRIPT_ENABLED + case Variant::STRUCT: { + // _data._mem contains the struct wrapper by value + const GDScriptStructInstance *wrapper = reinterpret_cast(_data._mem); + if (wrapper && wrapper->is_valid()) { + Variant ret; + if (wrapper->get(p_member, ret)) { + r_valid = true; + return ret; + } + } + r_valid = false; + return Variant(); + } break; +#endif case Variant::DICTIONARY: { const Variant *v = VariantGetInternalPtr::get_ptr(this)->getptr(p_member); if (v) { @@ -1092,6 +1121,167 @@ struct VariantKeyedSetGetObject { } }; +#ifdef MODULE_GDSCRIPT_ENABLED +struct VariantKeyedSetGetStruct { + static void get(const Variant *base, const Variant *key, Variant *value, bool *r_valid) { + // Get the struct wrapper from the Variant (stored by value) + const GDScriptStructInstance *wrapper = reinterpret_cast(base->_data._mem); + if (!wrapper || !wrapper->is_valid()) { + *r_valid = false; + *value = Variant(); + return; + } + + // Convert key to StringName + StringName member_name; + if (key->get_type() == Variant::STRING_NAME) { + member_name = *VariantGetInternalPtr::get_ptr(key); + } else if (key->get_type() == Variant::STRING) { + member_name = *VariantGetInternalPtr::get_ptr(key); + } else { + *r_valid = false; + *value = Variant(); + return; + } + + // Use wrapper->get to retrieve the member value + if (wrapper->get(member_name, *value)) { + *r_valid = true; + } else { + *r_valid = false; + *value = Variant(); + } + } + + static void ptr_get(const void *base, const void *key, void *value) { + // Avoid ptrconvert for performance + const Variant &base_var = *reinterpret_cast(base); + const GDScriptStructInstance *wrapper = reinterpret_cast(base_var._data._mem); + ERR_FAIL_NULL(wrapper); + ERR_FAIL_COND(!wrapper->is_valid()); + + // Convert key to Variant first, then to StringName + Variant key_var = PtrToArg::convert(key); + StringName member_name; + if (key_var.get_type() == Variant::STRING_NAME) { + member_name = *VariantGetInternalPtr::get_ptr(&key_var); + } else if (key_var.get_type() == Variant::STRING) { + member_name = *VariantGetInternalPtr::get_ptr(&key_var); + } else { + ERR_FAIL(); + } + + // Get the member value + Variant result; + if (wrapper->get(member_name, result)) { + PtrToArg::encode(result, value); + } else { + ERR_FAIL(); + } + } + + static void set(Variant *base, const Variant *key, const Variant *value, bool *r_valid) { + // Get the struct wrapper from the Variant (stored by value) + GDScriptStructInstance *wrapper = reinterpret_cast(base->_data._mem); + if (!wrapper || !wrapper->is_valid()) { + *r_valid = false; + return; + } + + // Convert key to StringName + StringName member_name; + if (key->get_type() == Variant::STRING_NAME) { + member_name = *VariantGetInternalPtr::get_ptr(key); + } else if (key->get_type() == Variant::STRING) { + member_name = *VariantGetInternalPtr::get_ptr(key); + } else { + *r_valid = false; + return; + } + + // Use wrapper->set to set the member value (COW happens inside) + *r_valid = wrapper->set(member_name, *value); + } + + static void ptr_set(void *base, const void *key, const void *value) { + // base points directly to the struct instance (stored by value in Variant) + GDScriptStructInstance *wrapper = reinterpret_cast(base); + ERR_FAIL_NULL(wrapper); + ERR_FAIL_COND(!wrapper->is_valid()); + + // Convert key to Variant first, then to StringName + Variant key_var = PtrToArg::convert(key); + StringName member_name; + if (key_var.get_type() == Variant::STRING_NAME) { + member_name = *VariantGetInternalPtr::get_ptr(&key_var); + } else if (key_var.get_type() == Variant::STRING) { + member_name = *VariantGetInternalPtr::get_ptr(&key_var); + } else { + ERR_FAIL(); + } + + // Set the member value + Variant value_var = PtrToArg::convert(value); + bool valid = wrapper->set(member_name, value_var); + ERR_FAIL_COND(!valid); + } + + static bool has(const Variant *base, const Variant *key, bool *r_valid) { + // Get the struct wrapper from the Variant (stored by value) + const GDScriptStructInstance *wrapper = reinterpret_cast(base->_data._mem); + if (!wrapper || !wrapper->is_valid()) { + *r_valid = false; + return false; + } + + // Convert key to StringName + StringName member_name; + if (key->get_type() == Variant::STRING_NAME) { + member_name = *VariantGetInternalPtr::get_ptr(key); + } else if (key->get_type() == Variant::STRING) { + member_name = *VariantGetInternalPtr::get_ptr(key); + } else { + *r_valid = false; + return false; + } + + // Check if the member exists by trying to get its index + *r_valid = true; + Ref struct_type = wrapper->get_struct_type(); + if (struct_type.is_null()) { + return false; + } + return struct_type->get_member_index(member_name) >= 0; + } + + static uint32_t ptr_has(const void *base, const void *key) { + // Convert base to Variant first to match ptr_get/ptr_set calling convention + Variant base_var = PtrToArg::convert(base); + const GDScriptStructInstance *wrapper = reinterpret_cast(base_var._data._mem); + ERR_FAIL_NULL_V(wrapper, false); + ERR_FAIL_COND_V(!wrapper->is_valid(), false); + + // Convert key to Variant first, then to StringName + Variant key_var = PtrToArg::convert(key); + StringName member_name; + if (key_var.get_type() == Variant::STRING_NAME) { + member_name = *VariantGetInternalPtr::get_ptr(&key_var); + } else if (key_var.get_type() == Variant::STRING) { + member_name = *VariantGetInternalPtr::get_ptr(&key_var); + } else { + return false; + } + + // Check if the member exists + Ref struct_type = wrapper->get_struct_type(); + if (struct_type.is_null()) { + return false; + } + return struct_type->get_member_index(member_name) >= 0; + } +}; +#endif + struct VariantKeyedSetterGetterInfo { Variant::ValidatedKeyedSetter validated_setter = nullptr; Variant::ValidatedKeyedGetter validated_getter = nullptr; @@ -1125,6 +1315,9 @@ static void register_keyed_member(Variant::Type p_type) { static void register_keyed_setters_getters() { register_keyed_member(Variant::DICTIONARY); register_keyed_member(Variant::OBJECT); +#ifdef MODULE_GDSCRIPT_ENABLED + register_keyed_member(Variant::STRUCT); +#endif } bool Variant::is_keyed(Variant::Type p_type) { ERR_FAIL_INDEX_V(p_type, VARIANT_MAX, false); @@ -1299,6 +1492,22 @@ void Variant::get_property_list(List *p_list) const { ERR_FAIL_NULL(obj); obj->get_property_list(p_list); +#ifdef MODULE_GDSCRIPT_ENABLED + } else if (type == STRUCT) { + // Use ScriptLanguage to get struct properties + ScriptLanguage *lang = nullptr; + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + ScriptLanguage *l = ScriptServer::get_language(i); + if (l && String(l->get_name()) == "GDScript") { + lang = l; + break; + } + } + if (lang) { + lang->get_struct_property_list(*this, p_list); + } + return; +#endif } else { List members; get_member_list(type, &members); diff --git a/core/variant/variant_utility.cpp b/core/variant/variant_utility.cpp index 6f0be32e038..1efc66235e6 100644 --- a/core/variant/variant_utility.cpp +++ b/core/variant/variant_utility.cpp @@ -917,6 +917,9 @@ Variant VariantUtilityFunctions::type_convert(const Variant &p_variant, const Va return p_variant.operator ::RID(); case Variant::Type::OBJECT: return p_variant.operator Object *(); + case Variant::Type::STRUCT: + // TODO: Implement struct conversion + return p_variant; case Variant::Type::CALLABLE: return p_variant.operator Callable(); case Variant::Type::SIGNAL: @@ -1055,7 +1058,7 @@ void VariantUtilityFunctions::push_warning(const Variant **p_args, int p_arg_cou String VariantUtilityFunctions::var_to_str(const Variant &p_var) { String vars; - VariantWriter::write_to_string(p_var, vars, nullptr, nullptr, true, false); + VariantWriter::write_to_string(p_var, vars); return vars; } @@ -1070,12 +1073,9 @@ Variant VariantUtilityFunctions::str_to_var(const String &p_var) { ss.s = p_var; String errs; - int line = 1; + int line; Variant ret; - Error err = VariantParser::parse(&ss, ret, errs, line, nullptr, false); - if (err != OK) { - ERR_PRINT("Parse error at line " + itos(line) + ": " + errs + "."); - } + (void)VariantParser::parse(&ss, ret, errs, line); return ret; } @@ -1085,12 +1085,9 @@ Variant VariantUtilityFunctions::str_to_var_with_objects(const String &p_var) { ss.s = p_var; String errs; - int line = 1; + int line; Variant ret; - Error err = VariantParser::parse(&ss, ret, errs, line, nullptr, true); - if (err != OK) { - ERR_PRINT("Parse error at line " + itos(line) + ": " + errs + "."); - } + (void)VariantParser::parse(&ss, ret, errs, line, nullptr, true); return ret; } @@ -1810,7 +1807,6 @@ void Variant::_register_variant_utility_functions() { FUNCBINDR(var_to_str, sarray("variable"), Variant::UTILITY_FUNC_TYPE_GENERAL); FUNCBINDR(str_to_var, sarray("string"), Variant::UTILITY_FUNC_TYPE_GENERAL); - FUNCBINDR(var_to_str_with_objects, sarray("variable"), Variant::UTILITY_FUNC_TYPE_GENERAL); FUNCBINDR(str_to_var_with_objects, sarray("string"), Variant::UTILITY_FUNC_TYPE_GENERAL); diff --git a/modules/gdscript/config.py b/modules/gdscript/config.py index ecd33a5dacc..a572eb89673 100644 --- a/modules/gdscript/config.py +++ b/modules/gdscript/config.py @@ -4,7 +4,7 @@ def can_build(env, platform): def configure(env): - pass + env.Append(CPPDEFINES=["MODULE_GDSCRIPT_ENABLED"]) def get_doc_classes(): diff --git a/modules/gdscript/editor/gdscript_docgen.cpp b/modules/gdscript/editor/gdscript_docgen.cpp index 5004a56adb0..a9ccfad3037 100644 --- a/modules/gdscript/editor/gdscript_docgen.cpp +++ b/modules/gdscript/editor/gdscript_docgen.cpp @@ -149,6 +149,14 @@ void GDScriptDocGen::_doctype_from_gdtype(const GDType &p_gdtype, String &r_type } } return; + case GDType::STRUCT: + // For now, treat structs as their struct type name + if (p_gdtype.struct_type && p_gdtype.struct_type->identifier) { + r_type = String(p_gdtype.struct_type->identifier->name); + } else { + r_type = "Struct"; + } + return; case GDType::VARIANT: case GDType::RESOLVING: case GDType::UNRESOLVED: diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 1035758076f..946d756db81 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -37,6 +37,7 @@ #include "gdscript_compiler.h" #include "gdscript_parser.h" #include "gdscript_rpc_callable.h" +#include "gdscript_struct.h" #include "gdscript_tokenizer_buffer.h" #include "gdscript_warning.h" @@ -52,6 +53,8 @@ #include "core/config/project_settings.h" #include "core/core_constants.h" #include "core/io/file_access.h" +#include "core/io/marshalls.h" +#include "core/variant/variant_internal.h" #include "scene/resources/packed_scene.h" #include "scene/scene_string_names.h" @@ -121,6 +124,96 @@ Variant GDScriptNativeClass::callp(const StringName &p_method, const Variant **p return Variant(); } +void GDScriptStructClass::_bind_methods() { + // Bind the vararg "new" constructor method + // This allows Object::callp to find it via ClassDB::get_method + ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "new", &GDScriptStructClass::_new_bind); +} + +// Constructor: takes ownership of the given struct. +// null is allowed - the struct_type can be set later via set_struct_type(). +GDScriptStructClass::GDScriptStructClass(const Ref &p_struct) : + struct_type(p_struct) { + // Ref<> automatically handles reference counting +} + +// Destructor is defaulted - Ref<> automatically handles cleanup + +Ref GDScriptStructClass::get_struct_type() const { + return struct_type; +} + +void GDScriptStructClass::set_struct_type(const Ref &p_struct) { + // Ref<> assignment automatically handles reference counting + struct_type = p_struct; +} + +// Temporary helper to create a STRUCT Variant from a GDScriptStructInstance +// TODO[PERFORMANCE]: Replace with proper Variant API once Variant exposes a safe constructor +// +// RECOMMENDED API ADDITIONS (in order of preference): +// 1. static Variant Variant::from_struct(GDScriptStructInstance *p_instance) +// - Cleanest API, consistent with other Variant constructors +// - Allows Variant to handle all validation internally +// - Can be made friend if needed for private access +// +// 2. void Variant::set_struct(GDScriptStructInstance *p_instance) +// - Similar to set_object(), set_ref(), etc. +// - Requires Variant to already exist, slightly less efficient +// +// 3. friend class GDScriptStructInstance; in Variant +// - Gives GDScript direct access to Variant internals +// - Most flexible but breaks encapsulation +// +// ARCHITECTURAL NOTES: +// - Variant::_data._mem is a byte array sized to fit any Variant type (sizeof(_mem) >= sizeof(void*)) +// - STRUCT type stores a single pointer to GDScriptStructInstance (reference-counted) +// - This pattern mimics how OBJECT and other pointer-based types are stored internally +// - The struct instance MUST already be reference-counted before calling this (see _new below) +// +// SAFETY ASSERTIONS: +// - Null check: p_instance must not be null +// - Size check: sizeof(void*) must fit in Variant::_data._mem +// - Enum check: Variant::STRUCT must be within valid enum range +// +// COW: Create a Variant from a struct instance wrapper +// The wrapper is copied by value, and the Ref<> inside handles reference counting +Variant GDScriptStructClass::_variant_from_struct_instance(GDScriptStructInstance *p_instance) { + ERR_FAIL_NULL_V(p_instance, Variant()); + + // With COW, we copy the wrapper by value into the Variant's memory + Variant result; + result.type = Variant::STRUCT; + + // Use placement new to copy the wrapper into _mem + // The copy constructor of GDScriptStructInstance copies the Ref<> properly + new (result._data._mem) GDScriptStructInstance(*p_instance); + + return result; +} + +Variant GDScriptStructClass::_new(const Variant **p_args, int p_argcount, Callable::CallError &r_error) { + ERR_FAIL_COND_V(!struct_type.is_valid(), Variant()); + + // Create the struct instance and return it as a Variant + // The COW implementation handles everything internally + return struct_type->create_variant_instance(p_args, p_argcount); +} + +Variant GDScriptStructClass::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { + if (p_method == SNAME("new")) { + return _new(p_args, p_argcount, r_error); + } + + r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; + return Variant(); +} + +// Vararg wrapper for the "new" method to be used with ClassDB binding +Variant GDScriptStructClass::_new_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error) { + return _new(p_args, p_argcount, r_error); +} + GDScriptFunction *GDScript::_super_constructor(GDScript *p_script) { if (likely(p_script->valid) && p_script->initializer) { return p_script->initializer; @@ -1592,6 +1685,18 @@ void GDScript::clear(ClearData *p_clear_data) { static_variables.clear(); static_variables_indices.clear(); + // Clear structs - with Ref<>, cleanup happens automatically + for (KeyValue> &E : structs) { + // Step 1: Remove the constant wrapper (releases wrapper's reference) + constants.erase(E.key); + // Step 2: Unregister from global registry (releases registry's reference) + if (E.value.is_valid()) { + GDScriptLanguage::get_singleton()->unregister_struct(E.value->get_fully_qualified_name()); + } + // Step 3: When structs.clear() is called, Ref<> automatically handles cleanup + } + structs.clear(); + if (implicit_initializer) { clear_data->functions.insert(implicit_initializer); implicit_initializer = nullptr; @@ -1616,9 +1721,11 @@ void GDScript::clear(ClearData *p_clear_data) { } #endif - // If it's not the root, skip clearing the data + // All dependencies have been accounted for + // Only root scripts clean up functions and scripts to avoid double-free + // Structs are cleaned up immediately above (not added to clear_data) if (is_root) { - // All dependencies have been accounted for + // Only root scripts clean up functions and scripts for (GDScriptFunction *E : clear_data->functions) { memdelete(E); } @@ -2763,6 +2870,7 @@ Vector GDScriptLanguage::get_reserved_words() const { "namespace", // Reserved for potential future use. "signal", "static", + "struct", "trait", // Reserved for potential future use. "var", // Other keywords. @@ -3031,6 +3139,152 @@ Ref GDScriptLanguage::get_script_by_fully_qualified_name(const String return scr; } +/***************** STRUCT REGISTRY *****************/ + +void GDScriptLanguage::register_struct(const String &p_fully_qualified_name, const Ref &p_struct) { + MutexLock lock(mutex); + + // Check if a struct with this name already exists + HashMap>::Iterator existing = global_structs.find(p_fully_qualified_name); + if (existing) { + // The Ref<> will automatically clean up when overwritten + global_structs.erase(p_fully_qualified_name); + } + + // Store the Ref<> (takes ownership automatically) + global_structs.insert(p_fully_qualified_name, p_struct); +} + +void GDScriptLanguage::unregister_struct(const String &p_fully_qualified_name) { + MutexLock lock(mutex); + + HashMap>::Iterator existing = global_structs.find(p_fully_qualified_name); + if (existing) { + // The Ref<> cleanup happens automatically on erase + global_structs.erase(p_fully_qualified_name); + } +} + +Ref GDScriptLanguage::get_struct_by_name(const String &p_fully_qualified_name) { + MutexLock lock(mutex); + HashMap>::Iterator E = global_structs.find(p_fully_qualified_name); + if (!E) { + return Ref(); + } + return E->value; +} + +/***************** STRUCT WRAPPER REGISTRY *****************/ + +void GDScriptLanguage::register_struct_wrapper(const String &p_fully_qualified_name, const Ref &p_wrapper) { + MutexLock lock(mutex); + + // Check if a wrapper with this name already exists + HashMap>::Iterator existing = global_struct_wrappers.find(p_fully_qualified_name); + if (existing) { + // Unreference the old wrapper's struct before replacing + // The old wrapper holds a reference to the struct type that needs to be released + Ref old_wrapper = existing->value; + if (old_wrapper.is_valid()) { + // Clear the old wrapper's reference to the struct type + // This will decrement the struct's ref_count + old_wrapper->set_struct_type(nullptr); + } + // Now erase and replace - the Ref will go out of scope and be destroyed + global_struct_wrappers.erase(p_fully_qualified_name); + } + + global_struct_wrappers.insert(p_fully_qualified_name, p_wrapper); +} + +void GDScriptLanguage::unregister_struct_wrapper(const String &p_fully_qualified_name) { + MutexLock lock(mutex); + + HashMap>::Iterator existing = global_struct_wrappers.find(p_fully_qualified_name); + if (existing) { + global_struct_wrappers.erase(p_fully_qualified_name); + } +} + +Ref GDScriptLanguage::get_struct_wrapper(const String &p_fully_qualified_name) { + MutexLock lock(mutex); + HashMap>::Iterator E = global_struct_wrappers.find(p_fully_qualified_name); + if (!E) { + return Ref(); + } + return E->value; +} + +Variant GDScriptLanguage::create_struct_instance(const String &p_fully_qualified_name, const Dictionary &p_data) { + Ref struct_type = get_struct_by_name(p_fully_qualified_name); + if (struct_type.is_null()) { + ERR_FAIL_V_MSG(Variant(), vformat("Cannot create struct instance: struct type '%s' not found in registry.", p_fully_qualified_name)); + } + + // Create the instance data wrapper + Ref instance_data = GDScriptStructInstanceData::create(struct_type); + + // Deserialize the data into the instance + if (!p_data.is_empty()) { + if (!instance_data->deserialize(p_data)) { + ERR_FAIL_V_MSG(Variant(), vformat("Failed to deserialize struct instance for type '%s'.", p_fully_qualified_name)); + } + } + + // Create the wrapper and return as Variant + GDScriptStructInstance wrapper(instance_data); + Variant result; + result.type = Variant::STRUCT; + // Use placement new to construct the wrapper in the Variant's memory + new (result._data._mem) GDScriptStructInstance(wrapper); + return result; +} + +/* STRUCT SERIALIZATION VIRTUAL METHODS */ + +bool GDScriptLanguage::can_create_struct_by_name() const { + return true; +} + +Variant GDScriptLanguage::create_struct_by_name(const String &p_fully_qualified_name, const Dictionary &p_data) { + // This is the virtual method implementation called by ScriptServer + // It delegates to the existing registry method + return const_cast(this)->create_struct_instance(p_fully_qualified_name, p_data); +} + +Dictionary GDScriptLanguage::struct_to_dict(const Variant &p_struct) const { + Dictionary result; + if (p_struct.get_type() != Variant::STRUCT) { + return result; + } + + // GDScriptLanguage is now a friend of Variant, so we can access _data._mem + // With COW, the wrapper is stored by value in _mem + const GDScriptStructInstance *wrapper = reinterpret_cast(p_struct._data._mem); + if (!wrapper || !wrapper->is_valid()) { + return result; + } + + // Use the existing serialize() method on the wrapper + return wrapper->serialize(); +} + +void GDScriptLanguage::get_struct_property_list(const Variant &p_struct, List *p_list) const { + if (p_struct.get_type() != Variant::STRUCT || !p_list) { + return; + } + + // GDScriptLanguage is now a friend of Variant, so we can access _data._mem + // With COW, the wrapper is stored by value in _mem + const GDScriptStructInstance *wrapper = reinterpret_cast(p_struct._data._mem); + if (!wrapper || !wrapper->is_valid()) { + return; + } + + // Use the existing get_property_list method on the wrapper + wrapper->get_property_list(p_list); +} + /*************** RESOURCE ***************/ Ref ResourceFormatLoaderGDScript::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) { @@ -3177,3 +3431,82 @@ void ResourceFormatSaverGDScript::get_recognized_extensions(const Ref bool ResourceFormatSaverGDScript::recognize(const Ref &p_resource) const { return Object::cast_to(*p_resource) != nullptr; } + +/***************** STRUCT SERIALIZATION HELPERS FOR JSON *****************/ + +// These functions are called from core/io/json.cpp with C linkage +// to allow serialization without direct header dependencies + +extern "C" { + +void gdscript_struct_instance_serialize(const GDScriptStructInstance *p_instance, Dictionary &r_dict) { + ERR_FAIL_NULL(p_instance); + // Call the serialize method + r_dict = p_instance->serialize(); +} + +bool gdscript_struct_instance_get_type_name(const GDScriptStructInstance *p_instance, String &r_name) { + ERR_FAIL_NULL_V(p_instance, false); + Ref struct_type = p_instance->get_struct_type(); + ERR_FAIL_COND_V(!struct_type.is_valid(), false); + r_name = struct_type->get_fully_qualified_name(); + return true; +} + +} // extern "C" + +/***************** STRUCT SERIALIZATION HELPERS FOR MARSHALLS *****************/ + +// These functions are called from core/io/marshalls.cpp with C linkage +// to allow serialization without direct header dependencies + +extern "C" { + +// Check if a Variant contains a struct instance +bool gdscript_variant_is_struct(const Variant &p_variant) { + return p_variant.get_type() == Variant::STRUCT; +} + +// Serialize a struct variant +// For debugger/marshaling, we encode the struct as a Dictionary +// The header (STRUCT type) has already been written by the caller +Error gdscript_variant_encode_struct(const Variant &p_variant, uint8_t *r_buffer, int &r_len) { + if (p_variant.get_type() != Variant::STRUCT) { + return ERR_INVALID_PARAMETER; + } + + // get_struct returns void*, need to cast to GDScriptStructInstance* + const GDScriptStructInstance *instance = reinterpret_cast(VariantInternal::get_struct(&p_variant)); + ERR_FAIL_NULL_V(instance, ERR_BUG); + + // Serialize struct to Dictionary + Dictionary dict = instance->serialize(); + + // Encode the Dictionary as our data payload + // Note: This will write a DICTIONARY type header, which is fine + // because the decoder will read it and return a Dictionary (not reconstruct the struct) + Error err = encode_variant(dict, r_buffer, r_len, false, 0); + return err; +} + +// Decode a struct variant (returns Dictionary for debugger compatibility) +Error gdscript_variant_decode_struct(const uint8_t *p_buffer, int p_len, int *r_len, Variant &r_variant) { + // For debugger/marshaling use, decode as Dictionary + // The struct blueprint may not be available on the receiving end + // This allows the debugger to display struct data without the struct definition + Variant dict_var; + Error err = decode_variant(dict_var, p_buffer, p_len, r_len, false, 0); + if (err != OK) { + return err; + } + + if (dict_var.get_type() != Variant::DICTIONARY) { + return ERR_INVALID_DATA; + } + + // Return as Dictionary (not reconstructed as struct) + r_variant = dict_var; + return OK; +} + +} // extern "C" diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index ebf2565d3af..a7b3d14b594 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -34,6 +34,10 @@ #include "gdscript_function.h" +// Forward declarations +class GDScriptStruct; +class GDScriptStructInstance; + #include "core/debugger/engine_debugger.h" #include "core/debugger/script_debugger.h" #include "core/doc_data.h" @@ -59,6 +63,28 @@ class GDScriptNativeClass : public RefCounted { GDScriptNativeClass(const StringName &p_name); }; +class GDScriptStructClass : public RefCounted { + GDCLASS(GDScriptStructClass, RefCounted); + + Ref struct_type; + +protected: + static void _bind_methods(); + +public: + // Temporary helper to create a STRUCT Variant from a GDScriptStructInstance + // TODO: Replace with proper Variant API once Variant exposes a safe constructor + static Variant _variant_from_struct_instance(GDScriptStructInstance *p_instance); + + Ref get_struct_type() const; + void set_struct_type(const Ref &p_struct); + Variant _new(const Variant **p_args, int p_argcount, Callable::CallError &r_error); + Variant _new_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error); + virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; + GDScriptStructClass(const Ref &p_struct = Ref()); + ~GDScriptStructClass() = default; +}; + class GDScript : public Script { GDCLASS(GDScript, Script); bool tool = false; @@ -77,9 +103,11 @@ class GDScript : public Script { struct ClearData { RBSet functions; RBSet> scripts; + RBSet> structs; void clear() { functions.clear(); scripts.clear(); + structs.clear(); } }; @@ -92,6 +120,8 @@ class GDScript : public Script { friend class GDScriptLambdaSelfCallable; friend class GDScriptLanguage; friend struct GDScriptUtilityFunctionsDefinitions; + friend class GDScriptStruct; + friend class GDScriptStructInstance; Ref native; Ref base; @@ -110,6 +140,7 @@ class GDScript : public Script { HashMap member_functions; HashMap> subclasses; HashMap _signals; + HashMap> structs; Dictionary rpc_config; public: @@ -476,6 +507,11 @@ class GDScriptLanguage : public ScriptLanguage { HashMap orphan_subclasses; + // Global struct registry for serialization/deserialization + HashMap> global_structs; + // Global struct wrapper registry for constructor access + HashMap> global_struct_wrappers; + #ifdef TOOLS_ENABLED void _extension_loaded(const Ref &p_extension); void _extension_unloading(const Ref &p_extension); @@ -592,6 +628,17 @@ class GDScriptLanguage : public ScriptLanguage { _FORCE_INLINE_ static GDScriptLanguage *get_singleton() { return singleton; } + // Struct registry methods + void register_struct(const String &p_fully_qualified_name, const Ref &p_struct); + void unregister_struct(const String &p_fully_qualified_name); + Ref get_struct_by_name(const String &p_fully_qualified_name); + Variant create_struct_instance(const String &p_fully_qualified_name, const Dictionary &p_data); + + // Struct wrapper registry methods + void register_struct_wrapper(const String &p_fully_qualified_name, const Ref &p_wrapper); + void unregister_struct_wrapper(const String &p_fully_qualified_name); + Ref get_struct_wrapper(const String &p_fully_qualified_name); + virtual String get_name() const override; /* LANGUAGE FUNCTIONS */ @@ -669,6 +716,12 @@ class GDScriptLanguage : public ScriptLanguage { virtual bool handles_global_class_type(const String &p_type) const override; virtual String get_global_class_name(const String &p_path, String *r_base_type = nullptr, String *r_icon_path = nullptr, bool *r_is_abstract = nullptr, bool *r_is_tool = nullptr) const override; + /* STRUCT SERIALIZATION */ + virtual bool can_create_struct_by_name() const override; + virtual Variant create_struct_by_name(const String &p_fully_qualified_name, const Dictionary &p_data) override; + virtual Dictionary struct_to_dict(const Variant &p_struct) const override; + virtual void get_struct_property_list(const Variant &p_struct, List *p_list) const override; + void add_orphan_subclass(const String &p_qualified_name, const ObjectID &p_subclass); Ref get_orphan_subclass(const String &p_qualified_name); diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 03ae20c5a71..b981e8ab30e 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -866,6 +866,10 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type result = member.get_datatype(); found = true; break; + case GDScriptParser::ClassNode::Member::STRUCT: + result = member.get_datatype(); + found = true; + break; case GDScriptParser::ClassNode::Member::ENUM: result = member.get_datatype(); found = true; @@ -1256,6 +1260,15 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, case GDScriptParser::ClassNode::Member::GROUP: // No-op, but needed to silence warnings. break; + case GDScriptParser::ClassNode::Member::STRUCT: { + // Check for name conflicts before resolving struct body + if (member.m_struct->identifier != nullptr) { + check_class_member_name_conflict(p_class, member.m_struct->identifier->name, member.m_struct); + } + // Resolve struct body during interface resolution so method signatures are available + resolve_struct_body(member.m_struct); + break; + } case GDScriptParser::ClassNode::Member::UNDEFINED: ERR_PRINT("Trying to resolve undefined member."); break; @@ -1350,6 +1363,10 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas GDScriptParser::ClassNode::Member member = p_class->members[i]; if (member.type == GDScriptParser::ClassNode::Member::CLASS) { resolve_class_interface(member.m_class, true); + } else if (member.type == GDScriptParser::ClassNode::Member::STRUCT) { + // Resolve struct method signatures during interface resolution + // This is needed so that struct methods can be called during analysis + resolve_struct_body(member.m_struct); } } } @@ -1578,11 +1595,79 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, bo GDScriptParser::ClassNode::Member member = p_class->members[i]; if (member.type == GDScriptParser::ClassNode::Member::CLASS) { resolve_class_body(member.m_class, true); + } else if (member.type == GDScriptParser::ClassNode::Member::STRUCT) { + resolve_struct_body(member.m_struct, true); } } } } +void GDScriptAnalyzer::resolve_struct_body(GDScriptParser::StructNode *p_struct, const GDScriptParser::Node *p_source) { + if (p_struct == nullptr) { + return; + } + + // Guard against null identifier before dereferencing + if (p_struct->identifier == nullptr) { + return; + } + + // Re-entrancy guard: return early if already resolving this struct + if (p_struct->resolving_body) { + return; + } + + // Idempotence guard: skip if already resolved + if (p_struct->resolved_body) { + return; + } + + // Set resolving sentinel before any recursive work + p_struct->resolving_body = true; + + // Save current class context and set struct context + GDScriptParser::ClassNode *previous_class = parser->current_class; + + GDScriptParser::StructNode *previous_struct = parser->current_struct; + parser->current_struct = p_struct; + + // Resolve base struct if extends is used + if (!p_struct->extends.is_empty()) { + // TODO: Implement struct inheritance resolution + // For now, mark as unresolved + } + + // Resolve field types + for (const GDScriptParser::StructNode::Field &field : p_struct->fields) { + if (field.variable != nullptr) { + // `resolve_variable()` already handles explicit types + initializer reduction/compat checks. + resolve_variable(field.variable, false); + } + } + + // Resolve method signatures only (not bodies) for struct methods + // Method body resolution is skipped until struct method calling is implemented + for (GDScriptParser::FunctionNode *method : p_struct->methods) { + if (method != nullptr && method->identifier != nullptr && !method->resolved_signature) { + resolve_function_signature(method, p_source); + } + } + + p_struct->resolved_body = true; + + // Clear resolving sentinel on all exit paths + p_struct->resolving_body = false; + + // Restore previous context + parser->current_struct = previous_struct; + parser->current_class = previous_class; +} + +void GDScriptAnalyzer::resolve_struct_body(GDScriptParser::StructNode *p_struct, bool p_recursive) { + resolve_struct_body(p_struct); + // Recursive resolution not needed for structs since they can't contain nested structs (yet) +} + void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node, bool p_is_root) { ERR_FAIL_NULL_MSG(p_node, "Trying to resolve type of a null node."); @@ -1596,6 +1681,10 @@ void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node, bool p_is_root resolve_class_body(static_cast(p_node), true); } break; + case GDScriptParser::Node::STRUCT: + // Resolve struct body + resolve_struct_body(static_cast(p_node), p_node); + break; case GDScriptParser::Node::CONSTANT: resolve_constant(static_cast(p_node), true); break; @@ -1829,13 +1918,27 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * if (!p_is_lambda && function_name == GDScriptLanguage::get_singleton()->strings._init) { // Constructor. - GDScriptParser::DataType return_type = parser->current_class->get_datatype(); - return_type.is_meta_type = false; - p_function->set_datatype(return_type); - if (p_function->return_type) { - GDScriptParser::DataType declared_return = resolve_datatype(p_function->return_type); - if (declared_return.kind != GDScriptParser::DataType::BUILTIN || declared_return.builtin_type != Variant::NIL) { - push_error("Constructor cannot have an explicit return type.", p_function->return_type); + if (parser->current_struct != nullptr) { + // Struct methods named _init are NOT constructors - they're regular methods + // Treat them as normal functions, not special constructors + if (p_function->return_type != nullptr) { + p_function->set_datatype(type_from_metatype(resolve_datatype(p_function->return_type))); + } else { + GDScriptParser::DataType return_type; + return_type.type_source = GDScriptParser::DataType::INFERRED; + return_type.kind = GDScriptParser::DataType::VARIANT; + p_function->set_datatype(return_type); + } + } else { + // Class constructor + GDScriptParser::DataType return_type = parser->current_class->get_datatype(); + return_type.is_meta_type = false; + p_function->set_datatype(return_type); + if (p_function->return_type) { + GDScriptParser::DataType declared_return = resolve_datatype(p_function->return_type); + if (declared_return.kind != GDScriptParser::DataType::BUILTIN || declared_return.builtin_type != Variant::NIL) { + push_error("Constructor cannot have an explicit return type.", p_function->return_type); + } } } } else if (!p_is_lambda && function_name == GDScriptLanguage::get_singleton()->strings._static_init) { @@ -1865,103 +1968,106 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * #ifdef TOOLS_ENABLED // Check if the function signature matches the parent. If not it's an error since it breaks polymorphism. // Not for the constructor which can vary in signature. - GDScriptParser::DataType base_type = parser->current_class->base_type; - base_type.is_meta_type = false; - GDScriptParser::DataType parent_return_type; - List parameters_types; - int default_par_count = 0; - BitField method_flags = {}; - StringName native_base; - if (!p_is_lambda && get_function_signature(p_function, false, base_type, function_name, parent_return_type, parameters_types, default_par_count, method_flags, &native_base)) { - bool valid = p_function->is_static == method_flags.has_flag(METHOD_FLAG_STATIC); - - if (p_function->return_type != nullptr) { - // Check return type covariance. - GDScriptParser::DataType return_type = p_function->get_datatype(); - if (return_type.is_variant()) { - // `is_type_compatible()` returns `true` if one of the types is `Variant`. - // Don't allow an explicitly specified `Variant` if the parent return type is narrower. - valid = valid && parent_return_type.is_variant(); - } else if (return_type.kind == GDScriptParser::DataType::BUILTIN && return_type.builtin_type == Variant::NIL) { - // `is_type_compatible()` returns `true` if target is an `Object` and source is `null`. - // Don't allow `void` if the parent return type is a hard non-`void` type. - if (parent_return_type.is_hard_type() && !(parent_return_type.kind == GDScriptParser::DataType::BUILTIN && parent_return_type.builtin_type == Variant::NIL)) { - valid = false; + // Skip this check for struct methods since structs don't have polymorphism in the same way + if (parser->current_class != nullptr && parser->current_struct == nullptr) { + GDScriptParser::DataType base_type = parser->current_class->base_type; + base_type.is_meta_type = false; + GDScriptParser::DataType parent_return_type; + List parameters_types; + int default_par_count = 0; + BitField method_flags = {}; + StringName native_base; + if (!p_is_lambda && get_function_signature(p_function, false, base_type, function_name, parent_return_type, parameters_types, default_par_count, method_flags, &native_base)) { + bool valid = p_function->is_static == method_flags.has_flag(METHOD_FLAG_STATIC); + + if (p_function->return_type != nullptr) { + // Check return type covariance. + GDScriptParser::DataType return_type = p_function->get_datatype(); + if (return_type.is_variant()) { + // `is_type_compatible()` returns `true` if one of the types is `Variant`. + // Don't allow an explicitly specified `Variant` if the parent return type is narrower. + valid = valid && parent_return_type.is_variant(); + } else if (return_type.kind == GDScriptParser::DataType::BUILTIN && return_type.builtin_type == Variant::NIL) { + // `is_type_compatible()` returns `true` if target is an `Object` and source is `null`. + // Don't allow `void` if the parent return type is a hard non-`void` type. + if (parent_return_type.is_hard_type() && !(parent_return_type.kind == GDScriptParser::DataType::BUILTIN && parent_return_type.builtin_type == Variant::NIL)) { + valid = false; + } + } else { + valid = valid && is_type_compatible(parent_return_type, return_type); } - } else { - valid = valid && is_type_compatible(parent_return_type, return_type); } - } - int parent_min_argc = parameters_types.size() - default_par_count; - int parent_max_argc = (method_flags & METHOD_FLAG_VARARG) ? INT_MAX : parameters_types.size(); - int current_min_argc = p_function->parameters.size() - default_value_count; - int current_max_argc = p_function->is_vararg() ? INT_MAX : p_function->parameters.size(); + int parent_min_argc = parameters_types.size() - default_par_count; + int parent_max_argc = (method_flags & METHOD_FLAG_VARARG) ? INT_MAX : parameters_types.size(); + int current_min_argc = p_function->parameters.size() - default_value_count; + int current_max_argc = p_function->is_vararg() ? INT_MAX : p_function->parameters.size(); - // `[current_min_argc..current_max_argc]` must include `[parent_min_argc..parent_max_argc]`. - valid = valid && current_min_argc <= parent_min_argc && parent_max_argc <= current_max_argc; + // `[current_min_argc..current_max_argc]` must include `[parent_min_argc..parent_max_argc]`. + valid = valid && current_min_argc <= parent_min_argc && parent_max_argc <= current_max_argc; - if (valid) { - int i = 0; - for (const GDScriptParser::DataType &parent_par_type : parameters_types) { - if (i >= p_function->parameters.size()) { - break; - } - const GDScriptParser::DataType ¤t_par_type = p_function->parameters[i]->datatype; - i++; - // Check parameter type contravariance. - if (parent_par_type.is_variant() && parent_par_type.is_hard_type()) { - // `is_type_compatible()` returns `true` if one of the types is `Variant`. - // Don't allow narrowing a hard `Variant`. - valid = valid && current_par_type.is_variant(); - } else { - valid = valid && is_type_compatible(current_par_type, parent_par_type); + if (valid) { + int i = 0; + for (const GDScriptParser::DataType &parent_par_type : parameters_types) { + if (i >= p_function->parameters.size()) { + break; + } + const GDScriptParser::DataType ¤t_par_type = p_function->parameters[i]->datatype; + i++; + // Check parameter type contravariance. + if (parent_par_type.is_variant() && parent_par_type.is_hard_type()) { + // `is_type_compatible()` returns `true` if one of the types is `Variant`. + // Don't allow narrowing a hard `Variant`. + valid = valid && current_par_type.is_variant(); + } else { + valid = valid && is_type_compatible(current_par_type, parent_par_type); + } } } - } - if (!valid) { - // Compute parent signature as a string to show in the error message. - String parent_signature = String(function_name) + "("; - int j = 0; - for (const GDScriptParser::DataType &par_type : parameters_types) { - if (j > 0) { - parent_signature += ", "; - } - String parameter = par_type.to_string(); - if (parameter == "null") { - parameter = "Variant"; + if (!valid) { + // Compute parent signature as a string to show in the error message. + String parent_signature = String(function_name) + "("; + int j = 0; + for (const GDScriptParser::DataType &par_type : parameters_types) { + if (j > 0) { + parent_signature += ", "; + } + String parameter = par_type.to_string(); + if (parameter == "null") { + parameter = "Variant"; + } + parent_signature += parameter; + if (j >= parameters_types.size() - default_par_count) { + parent_signature += " = "; + } + + j++; } - parent_signature += parameter; - if (j >= parameters_types.size() - default_par_count) { - parent_signature += " = "; + if (method_flags & METHOD_FLAG_VARARG) { + if (!parameters_types.is_empty()) { + parent_signature += ", "; + } + parent_signature += "..."; } + parent_signature += ") -> "; - j++; - } - if (method_flags & METHOD_FLAG_VARARG) { - if (!parameters_types.is_empty()) { - parent_signature += ", "; + const String return_type = parent_return_type.to_string_strict(); + if (return_type == "null") { + parent_signature += "void"; + } else { + parent_signature += return_type; } - parent_signature += "..."; - } - parent_signature += ") -> "; - const String return_type = parent_return_type.to_string_strict(); - if (return_type == "null") { - parent_signature += "void"; - } else { - parent_signature += return_type; + push_error(vformat(R"(The function signature doesn't match the parent. Parent signature is "%s".)", parent_signature), p_function); } - - push_error(vformat(R"(The function signature doesn't match the parent. Parent signature is "%s".)", parent_signature), p_function); - } #ifdef DEBUG_ENABLED - if (native_base != StringName()) { - parser->push_warning(p_function, GDScriptWarning::NATIVE_METHOD_OVERRIDE, function_name, native_base); - } + if (native_base != StringName()) { + parser->push_warning(p_function, GDScriptWarning::NATIVE_METHOD_OVERRIDE, function_name, native_base); + } #endif // DEBUG_ENABLED - } + } + } // End of if (parser->current_class != nullptr) check #endif // TOOLS_ENABLED } @@ -2675,6 +2781,7 @@ void GDScriptAnalyzer::reduce_expression(GDScriptParser::ExpressionNode *p_expre case GDScriptParser::Node::PATTERN: case GDScriptParser::Node::RETURN: case GDScriptParser::Node::SIGNAL: + case GDScriptParser::Node::STRUCT: case GDScriptParser::Node::SUITE: case GDScriptParser::Node::TYPE: case GDScriptParser::Node::VARIABLE: @@ -3591,6 +3698,8 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a } else { reduce_expression(subscript->base); base_type = subscript->base->get_datatype(); + if (base_type.kind == GDScriptParser::DataType::STRUCT) { + } is_self = subscript->base->type == GDScriptParser::Node::SELF; } } else { @@ -3607,6 +3716,8 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a List par_types; bool is_constructor = (base_type.is_meta_type || (p_call->callee && p_call->callee->type == GDScriptParser::Node::IDENTIFIER)) && p_call->function_name == SNAME("new"); + if (p_call->function_name == SNAME("new") && base_type.kind == GDScriptParser::DataType::STRUCT) { + } if (is_constructor) { if (Engine::get_singleton()->has_singleton(base_type.native_type)) { @@ -4153,6 +4264,35 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod return; } + // Handle struct types + if (base.kind == GDScriptParser::DataType::STRUCT) { + if (base.struct_type == nullptr) { + return; + } + + if (base.is_meta_type) { + // Accessing struct static methods/constructors (e.g., Point.new) + // For now, just set the identifier as having a valid type + // The actual method resolution happens in resolve_call + p_identifier->set_datatype(base); + return; + } else { + // Accessing struct instance fields (e.g., point.x) + if (base.struct_type->has_field(name)) { + int field_index = base.struct_type->field_indices[name]; + if (field_index >= 0 && field_index < base.struct_type->fields.size()) { + const GDScriptParser::StructNode::Field &field = base.struct_type->fields[field_index]; + if (field.variable != nullptr && field.variable->datatype.is_set()) { + p_identifier->set_datatype(field.variable->datatype); + return; + } + } + } + push_error(vformat(R"(Cannot find field "%s" in struct "%s".)", name, base.struct_type->identifier->name), p_identifier); + return; + } + } + GDScriptParser::ClassNode *base_class = base.class_type; List script_classes; bool is_base = true; @@ -4165,6 +4305,20 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod for (GDScriptParser::ClassNode *script_class : script_classes) { if (p_base == nullptr && script_class->identifier && script_class->identifier->name == name) { + // Check if there's a struct with the same name - structs take precedence over classes + bool struct_exists = false; + if (parser->current_class != nullptr) { + for (const GDScriptParser::ClassNode::Member &member : parser->current_class->members) { + if (member.type == GDScriptParser::ClassNode::Member::STRUCT && member.m_struct->identifier->name == name) { + struct_exists = true; + break; + } + } + } + if (struct_exists) { + // Skip this class, there's a struct with the same name + continue; + } reduce_identifier_from_base_set_class(p_identifier, script_class->get_datatype()); if (script_class->outer != nullptr) { p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_CLASS; @@ -4242,6 +4396,17 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod return; } + case GDScriptParser::ClassNode::Member::STRUCT: { + // Structs are similar to classes - treat them as types + // Use the member's get_datatype() which now properly sets kind, is_meta_type, is_constant, type_source, and struct_type + p_identifier->set_datatype(member.get_datatype()); + // Set script_path since Member doesn't have access to the parser context + p_identifier->datatype.script_path = parser->script_path; + p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_CLASS; + p_identifier->is_constant = true; + return; + } + default: { // Do nothing } @@ -4369,6 +4534,9 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_identifier, bool can_be_builtin) { // TODO: This is an opportunity to further infer types. + if (p_identifier->name == StringName("TestStruct")) { + } + // Check if we are inside an enum. This allows enum values to access other elements of the same enum. if (current_enum) { for (int i = 0; i < current_enum->values.size(); i++) { @@ -4763,6 +4931,18 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { } void GDScriptAnalyzer::reduce_self(GDScriptParser::SelfNode *p_self) { + // Check if 'self' is being used outside of a class context (e.g., in a struct) + if (parser->current_class == nullptr) { + p_self->is_constant = false; + // Emit a clear compile error: 'self' is not valid outside of class context + push_error(R"(Using "self" outside of a class context is not allowed.)", p_self); + // Set VARIANT fallback instead of void to allow type checking to continue + GDScriptParser::DataType variant_type; + variant_type.kind = GDScriptParser::DataType::VARIANT; + p_self->set_datatype(variant_type); + return; + } + p_self->is_constant = false; p_self->set_datatype(type_from_metatype(parser->current_class->get_datatype())); mark_lambda_use_self(); @@ -4972,6 +5152,7 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri case Variant::NODE_PATH: case Variant::SIGNAL: case Variant::STRING_NAME: + case Variant::STRUCT: break; // Support depends on if the dictionary has a typed key, otherwise anything is valid. case Variant::DICTIONARY: @@ -5040,6 +5221,7 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri case Variant::NODE_PATH: case Variant::SIGNAL: case Variant::STRING_NAME: + case Variant::STRUCT: result_type.kind = GDScriptParser::DataType::VARIANT; push_error(vformat(R"(Cannot use subscript operator on a base of type "%s".)", base_type.to_string()), p_subscript->base); break; @@ -5772,6 +5954,162 @@ bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bo } } + // Handle struct construction and methods + if (p_base_type.kind == GDScriptParser::DataType::STRUCT) { + const GDScriptParser::StructNode *struct_node = p_base_type.struct_type; + + // Use non-mutating lookup - don't call push_error if struct_node is null + if (!struct_node) { + return false; + } + + if (p_is_constructor && p_function == SNAME("new")) { + // Struct constructor - returns instance of the struct + r_return_type = p_base_type; + r_return_type.is_meta_type = false; + r_method_flags.set_flag(METHOD_FLAG_STATIC); + + // Get constructor parameters from struct members or explicit _init method + // struct_node is already declared and null-checked above + if (struct_node) { + if (struct_node->constructor) { + // Extract signature from the constructor function + const GDScriptParser::FunctionNode *ctor = struct_node->constructor; + if (ctor->return_type) { + // Constructors cannot have explicit return types + push_error("Struct constructor cannot have an explicit return type.", ctor->return_type); + } + for (const GDScriptParser::ParameterNode *param : ctor->parameters) { + if (param->datatype.is_set()) { + r_par_types.push_back(param->datatype); + } else if (param->initializer && param->initializer->is_constant) { + // Infer type from default value + r_par_types.push_back(type_from_variant(param->initializer->reduced_value, param)); + } else { + // No type info, use Variant + GDScriptParser::DataType variant_type; + variant_type.kind = GDScriptParser::DataType::VARIANT; + r_par_types.push_back(variant_type); + } + } + // Count default arguments by checking initializers, consistent with class path + r_default_arg_count = 0; + for (const GDScriptParser::ParameterNode *param : ctor->parameters) { + if (param->initializer != nullptr) { + r_default_arg_count++; + } + } + if (ctor->is_static) { + r_method_flags.set_flag(METHOD_FLAG_STATIC); + } + } else { + // No explicit constructor - derive signature from struct fields + // Default constructor takes one argument per field + // ALL struct fields are optional (have default values), so + // default_arg_count equals the total number of fields + for (const GDScriptParser::StructNode::Field &field : struct_node->fields) { + if (field.variable && field.variable->datatype.is_set()) { + r_par_types.push_back(field.variable->datatype); + } else { + // No type info, use Variant + GDScriptParser::DataType variant_type; + variant_type.kind = GDScriptParser::DataType::VARIANT; + r_par_types.push_back(variant_type); + } + } + // All struct fields have default values (nil if not specified) + // This allows calling the constructor with 0 to N arguments + r_default_arg_count = struct_node->fields.size(); + } + } + + return true; + } + + // Look up the method in the struct + // struct_node is already declared and null-checked above + // Check if method exists in this struct + if (struct_node->has_method(p_function)) { + const GDScriptParser::FunctionNode *func = struct_node->method_map[p_function]; + if (func && func->resolved_signature) { + // Extract return type + if (func->return_type) { + r_return_type = type_from_metatype(resolve_datatype(func->return_type)); + } else { + r_return_type.kind = GDScriptParser::DataType::VARIANT; + } + + // Extract parameter types + for (const GDScriptParser::ParameterNode *param : func->parameters) { + if (param->datatype.is_set()) { + r_par_types.push_back(param->datatype); + } else if (param->initializer && param->initializer->is_constant) { + // Infer type from default value + r_par_types.push_back(type_from_variant(param->initializer->reduced_value, param)); + } else { + // No type info, use Variant + GDScriptParser::DataType variant_type; + variant_type.kind = GDScriptParser::DataType::VARIANT; + r_par_types.push_back(variant_type); + } + } + + r_default_arg_count = func->default_arg_values.size(); + + // Set method flags + if (func->is_static) { + r_method_flags.set_flag(METHOD_FLAG_STATIC); + } + + return true; + } + } + + // Check base structs + const GDScriptParser::StructNode *current = struct_node; + while (current->base_struct_type.kind == GDScriptParser::DataType::STRUCT && current->base_struct_type.struct_type) { + current = current->base_struct_type.struct_type; + if (current->has_method(p_function)) { + const GDScriptParser::FunctionNode *func = current->method_map[p_function]; + if (func && func->resolved_signature) { + // Extract return type + if (func->return_type) { + r_return_type = type_from_metatype(resolve_datatype(func->return_type)); + } else { + r_return_type.kind = GDScriptParser::DataType::VARIANT; + } + + // Extract parameter types + for (const GDScriptParser::ParameterNode *param : func->parameters) { + if (param->datatype.is_set()) { + r_par_types.push_back(param->datatype); + } else if (param->initializer && param->initializer->is_constant) { + // Infer type from default value + r_par_types.push_back(type_from_variant(param->initializer->reduced_value, param)); + } else { + // No type info, use Variant + GDScriptParser::DataType variant_type; + variant_type.kind = GDScriptParser::DataType::VARIANT; + r_par_types.push_back(variant_type); + } + } + + r_default_arg_count = func->default_arg_values.size(); + + // Set method flags + if (func->is_static) { + r_method_flags.set_flag(METHOD_FLAG_STATIC); + } + + return true; + } + } + } + + // Method not found in struct or its bases + return false; + } + if (p_base_type.kind == GDScriptParser::DataType::BUILTIN) { // Construct a base type to get methods. Callable::CallError err; @@ -6205,6 +6543,7 @@ bool GDScriptAnalyzer::check_type_compatibility(const GDScriptParser::DataType & StringName src_native; Ref