diff --git a/.gitignore b/.gitignore index 361f03b1771..6489c8062f1 100644 --- a/.gitignore +++ b/.gitignore @@ -393,3 +393,5 @@ $RECYCLE.BIN/ *.msp *.lnk *.generated.props +.gdbinit +CLAUDE.md diff --git a/core/core_constants.cpp b/core/core_constants.cpp index 00f8e5e7f47..541ad667d9a 100644 --- a/core/core_constants.cpp +++ b/core/core_constants.cpp @@ -776,6 +776,7 @@ void register_global_constants() { BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_PACKED_VECTOR3_ARRAY", Variant::PACKED_VECTOR3_ARRAY); BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_PACKED_COLOR_ARRAY", Variant::PACKED_COLOR_ARRAY); BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_PACKED_VECTOR4_ARRAY", Variant::PACKED_VECTOR4_ARRAY); + BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_STRUCT", Variant::STRUCT); BIND_CORE_ENUM_CONSTANT_CUSTOM("TYPE_MAX", Variant::VARIANT_MAX); //comparison diff --git a/core/extension/extension_api_dump.cpp b/core/extension/extension_api_dump.cpp index 1d79ee17f59..a2e8dbc61eb 100644 --- a/core/extension/extension_api_dump.cpp +++ b/core/extension/extension_api_dump.cpp @@ -49,6 +49,9 @@ #ifdef TOOLS_ENABLED #include "editor/doc/editor_help.h" +// Forward declaration for struct type +class GDScriptStructInstance; + static String get_builtin_or_variant_type_name(const Variant::Type p_type) { if (p_type == Variant::NIL) { return "Variant"; @@ -229,6 +232,7 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { { Variant::PACKED_VECTOR3_ARRAY, ptrsize_32 * 2, ptrsize_64 * 2, ptrsize_32 * 2, ptrsize_64 * 2 }, { Variant::PACKED_COLOR_ARRAY, ptrsize_32 * 2, ptrsize_64 * 2, ptrsize_32 * 2, ptrsize_64 * 2 }, { Variant::PACKED_VECTOR4_ARRAY, ptrsize_32 * 2, ptrsize_64 * 2, ptrsize_32 * 2, ptrsize_64 * 2 }, + { Variant::STRUCT, ptrsize_32, ptrsize_64, ptrsize_32, ptrsize_64 }, { Variant::VARIANT_MAX, sizeof(uint64_t) + sizeof(float) * 4, sizeof(uint64_t) + sizeof(float) * 4, sizeof(uint64_t) + sizeof(double) * 4, sizeof(uint64_t) + sizeof(double) * 4 }, }; @@ -257,6 +261,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"); @@ -281,6 +286,12 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { Array sizes; for (int j = 0; j <= Variant::VARIANT_MAX; j++) { Variant::Type t = type_size_array[j].type; + // Structs are a GDScript-internal type with no GDExtension ABI; keep + // them out of the extension API so bindings generators don't try to + // emit a (nonexistent) Struct type. + if (t == Variant::STRUCT) { + continue; + } String name = t == Variant::VARIANT_MAX ? String("Variant") : Variant::get_type_name(t); Dictionary d2; d2["name"] = name; @@ -658,6 +669,11 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { if (i == Variant::OBJECT) { continue; } + // Structs are a GDScript-internal type with no GDExtension ABI, so they + // are not exposed as a builtin class in the extension API. + if (i == Variant::STRUCT) { + continue; + } Variant::Type type = Variant::Type(i); diff --git a/core/extension/gdextension_interface.cpp b/core/extension/gdextension_interface.cpp index 18a92bc6a1e..0c20ea81951 100644 --- a/core/extension/gdextension_interface.cpp +++ b/core/extension/gdextension_interface.cpp @@ -641,6 +641,7 @@ static GDExtensionVariantFromTypeConstructorFunc gdextension_get_variant_from_ty return VariantTypeConstructor::variant_from_type; case GDEXTENSION_VARIANT_TYPE_PACKED_COLOR_ARRAY: return VariantTypeConstructor::variant_from_type; + case GDEXTENSION_VARIANT_TYPE_STRUCT: // Structs are GDScript-internal and have no GDExtension type representation. case GDEXTENSION_VARIANT_TYPE_NIL: case GDEXTENSION_VARIANT_TYPE_VARIANT_MAX: ERR_FAIL_V_MSG(nullptr, "Getting Variant conversion function with invalid type"); @@ -726,6 +727,7 @@ static GDExtensionTypeFromVariantConstructorFunc gdextension_get_variant_to_type return VariantTypeConstructor::type_from_variant; case GDEXTENSION_VARIANT_TYPE_PACKED_COLOR_ARRAY: return VariantTypeConstructor::type_from_variant; + case GDEXTENSION_VARIANT_TYPE_STRUCT: // Structs are GDScript-internal and have no GDExtension type representation. case GDEXTENSION_VARIANT_TYPE_NIL: case GDEXTENSION_VARIANT_TYPE_VARIANT_MAX: ERR_FAIL_V_MSG(nullptr, "Getting Variant conversion function with invalid type"); @@ -811,6 +813,7 @@ static GDExtensionVariantGetInternalPtrFunc gdextension_variant_get_ptr_internal return reinterpret_cast(static_cast(VariantInternal::get_color_array)); case GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR4_ARRAY: return reinterpret_cast(static_cast(VariantInternal::get_vector4_array)); + case GDEXTENSION_VARIANT_TYPE_STRUCT: // Structs are GDScript-internal and have no GDExtension type representation. case GDEXTENSION_VARIANT_TYPE_NIL: case GDEXTENSION_VARIANT_TYPE_VARIANT_MAX: ERR_FAIL_V_MSG(nullptr, "Getting Variant get internal pointer function with invalid type."); @@ -823,6 +826,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) { @@ -833,6 +841,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/extension/gdextension_interface.h b/core/extension/gdextension_interface.h index 2f2479d8c01..83e357edbe2 100644 --- a/core/extension/gdextension_interface.h +++ b/core/extension/gdextension_interface.h @@ -122,6 +122,8 @@ typedef enum { GDEXTENSION_VARIANT_TYPE_PACKED_COLOR_ARRAY, GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR4_ARRAY, + GDEXTENSION_VARIANT_TYPE_STRUCT, + GDEXTENSION_VARIANT_TYPE_VARIANT_MAX } GDExtensionVariantType; diff --git a/core/io/json.cpp b/core/io/json.cpp index b4739ad1e47..f49f399e06e 100644 --- a/core/io/json.cpp +++ b/core/io/json.cpp @@ -41,6 +41,35 @@ #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) +// TODO: Replace this extern "C" bridge with a proper Variant-level struct serialization +// API so core/io no longer needs to know about GDScript struct internals. + +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); +} + +// 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; +} +#endif // MODULE_GDSCRIPT_ENABLED const char *JSON::tk_name[TK_MAX] = { "'{'", @@ -830,6 +859,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; @@ -1303,6 +1376,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 dc857f044cd..b488f158be0 100644 --- a/core/io/marshalls.cpp +++ b/core/io/marshalls.cpp @@ -46,6 +46,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); @@ -1302,6 +1314,24 @@ 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. + // The helper resets its length counter internally, so pass a local + // accumulator and add the bytes it consumed to r_len (which already + // counts the STRUCT header) instead of letting it overwrite it. + int used = 0; + Error err = gdscript_variant_decode_struct(buf, len, &used, r_variant); + ERR_FAIL_COND_V(err, err); + if (r_len) { + (*r_len) += used; + } + } break; +#else + case Variant::STRUCT: { + ERR_FAIL_V(ERR_BUG); + } break; +#endif default: { ERR_FAIL_V(ERR_BUG); } @@ -2130,6 +2160,22 @@ 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. + // The helper resets its length counter to 0 internally, so pass a + // local accumulator and add it to r_len (which already counts the + // STRUCT header) instead of letting it clobber the outer accounting. + int used = 0; + Error err = gdscript_variant_encode_struct(p_variant, buf, used); + ERR_FAIL_COND_V(err, err); + r_len += used; + } 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 7cd08b46422..cdbf8b42cf3 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -854,6 +854,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 37601f1b6cf..56a90c89964 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -556,6 +556,69 @@ 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) { + // Snapshot the languages under the lock, then release it before calling into + // language virtuals: create_struct_by_name() can re-enter the ScriptServer or + // take other locks, so holding languages_mutex across it risks deadlock. + ScriptLanguage *langs[MAX_LANGUAGES]; + int lang_count = 0; + { + MutexLock lock(languages_mutex); + if (!languages_ready) { + ERR_FAIL_V_MSG(Variant(), "Cannot create struct instance: languages not initialized."); + } + lang_count = _language_count; + for (int i = 0; i < _language_count; i++) { + langs[i] = _languages[i]; + } + } + + // Try each language to see if it can create the struct + for (int i = 0; i < lang_count; i++) { + ScriptLanguage *lang = langs[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) { + // Snapshot the languages under the lock, then release it before calling the + // language virtuals (see create_struct_instance() above). + ScriptLanguage *langs[MAX_LANGUAGES]; + int lang_count = 0; + { + MutexLock lock(languages_mutex); + if (!languages_ready) { + return false; + } + lang_count = _language_count; + for (int i = 0; i < _language_count; i++) { + langs[i] = _languages[i]; + } + } + + // Check if any language can create this struct + for (int i = 0; i < lang_count; i++) { + ScriptLanguage *lang = langs[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 e7cff1d1e6c..4dac373daa8 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -110,6 +110,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(); @@ -472,6 +476,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 68ff96cca0a..e3d7b690251 100644 --- a/core/variant/variant.cpp +++ b/core/variant/variant.cpp @@ -44,6 +44,10 @@ #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 + #include #include #include @@ -196,6 +200,7 @@ static const std::array TYPE_STRING_TABLE = "PackedVector3Array", "PackedColorArray", "PackedVector4Array", + "Struct", }; /* @@ -257,6 +262,7 @@ static const std::array TYPE_CAST_TAB (1ull << Variant::ARRAY) | (1ull << Variant::PACKED_VECTOR3_ARRAY), // packedvec3array (1ull << Variant::ARRAY) | (1ull << Variant::PACKED_COLOR_ARRAY), // packedcolorarray (1ull << Variant::ARRAY) | (1ull << Variant::PACKED_VECTOR4_ARRAY), // packedvec4array + 0, // struct (not convertible to/from other types) }; /* @@ -318,6 +324,7 @@ static const std::array TYPE_STRICT_C (1ull << Variant::ARRAY) | (1ull << Variant::PACKED_VECTOR3_ARRAY), // packedvec3array (1ull << Variant::ARRAY) | (1ull << Variant::PACKED_COLOR_ARRAY), // packedcolorarray (1ull << Variant::ARRAY) | (1ull << Variant::PACKED_VECTOR4_ARRAY), // packedvec4array + 0, // struct (not convertible to/from other types) }; /* @@ -364,6 +371,7 @@ static const std::unordered_map STRING_TO_TYPE_TBL = { "PackedVector3Array", Variant::PACKED_VECTOR3_ARRAY }, { "PackedColorArray", Variant::PACKED_COLOR_ARRAY }, { "PackedVector4Array", Variant::PACKED_VECTOR4_ARRAY }, + { "Struct", Variant::STRUCT }, }; String Variant::get_type_name(Variant::Type p_type) { @@ -770,6 +778,15 @@ void Variant::reference(const Variant &p_variant) { _data.packed_array = PACKED_ARRAY_CREATE_TBL[p_variant.type - PACKED_BYTE_ARRAY](); } return; +#ifdef MODULE_GDSCRIPT_ENABLED + case STRUCT: { + // COW: Copy the wrapper by value; the Ref<> inside handles reference counting. + // Placement new to copy the wrapper into _mem. + const GDScriptStructInstance *src_wrapper = reinterpret_cast(p_variant._data._mem); + new (_data._mem) GDScriptStructInstance(*src_wrapper); + return; + } +#endif default: dst = _data._mem; src = (void *)p_variant._data._mem; @@ -895,6 +912,16 @@ void Variant::_clear_internal() { case PACKED_COLOR_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. @@ -1154,6 +1181,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) + ">"; } @@ -2216,6 +2255,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: { } } @@ -2918,7 +2967,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 66eab56eb63..bcb2d4d258a 100644 --- a/core/variant/variant.h +++ b/core/variant/variant.h @@ -75,6 +75,7 @@ class Object; class RefCounted; +class GDScriptStructInstance; template class Ref; @@ -153,6 +154,9 @@ class Variant { PACKED_COLOR_ARRAY, PACKED_VECTOR4_ARRAY, /// @} + + STRUCT, + VARIANT_MAX }; @@ -202,6 +206,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), @@ -340,7 +350,8 @@ class Variant { (1ull << Variant::PACKED_COLOR_ARRAY) | (1ull << Variant::PACKED_VECTOR2_ARRAY) | (1ull << Variant::PACKED_VECTOR3_ARRAY) | - (1ull << Variant::PACKED_VECTOR4_ARRAY); + (1ull << Variant::PACKED_VECTOR4_ARRAY) | + (1ull << Variant::STRUCT); _FORCE_INLINE_ void clear() { if (unlikely((needs_deinit & (1ull << type)) != 0)) { // Make it fast for types that don't need deinit. diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index fe0c215756d..c7c2f30dc5a 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -1382,6 +1382,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) { @@ -1414,6 +1420,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) { @@ -1433,6 +1445,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) { @@ -1458,16 +1476,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; @@ -1475,6 +1510,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; @@ -1482,6 +1523,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); @@ -1489,6 +1536,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; @@ -1496,6 +1549,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); @@ -1504,6 +1563,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 @@ -1516,6 +1580,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; @@ -1523,6 +1593,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; @@ -1530,6 +1606,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); } @@ -1537,11 +1619,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; @@ -1549,6 +1643,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; @@ -1556,6 +1656,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; @@ -1563,6 +1669,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; @@ -1570,6 +1682,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); @@ -1594,6 +1712,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 939be8e3da4..ef5b2187343 100644 --- a/core/variant/variant_construct.cpp +++ b/core/variant/variant_construct.cpp @@ -271,6 +271,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; @@ -303,30 +315,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]; } @@ -334,6 +375,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 dc36fd910ac..580c45d74f9 100644 --- a/core/variant/variant_internal.h +++ b/core/variant/variant_internal.h @@ -128,6 +128,9 @@ class VariantInternal { case Variant::OBJECT: init_object(v); break; + case Variant::STRUCT: + init_struct(v); + break; default: break; } @@ -228,6 +231,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; @@ -338,6 +345,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(); @@ -451,6 +464,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); } @@ -537,6 +552,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); } @@ -791,6 +808,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 10c4692bda6..bddec42d60f 100644 --- a/core/variant/variant_parser.cpp +++ b/core/variant/variant_parser.cpp @@ -44,6 +44,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? @@ -1036,6 +1039,84 @@ Error VariantParser::parse_value(Token &token, Variant &value, Stream *p_stream, r_err_str = "Expected ')'"; return ERR_PARSE_ERROR; } + } else if (id == "Struct") { +#ifdef MODULE_GDSCRIPT_ENABLED + Error err = get_token(p_stream, token, line, r_err_str); + + if (err != OK) { + return err; + } + + if (token.type != TK_PARENTHESIS_OPEN) { + r_err_str = "Expected '(' after Struct"; + return ERR_PARSE_ERROR; + } + + err = get_token(p_stream, token, line, r_err_str); + + if (err != OK) { + return err; + } + + Variant dict_value; + err = parse_value(token, dict_value, p_stream, line, r_err_str, p_res_parser, p_allow_objects); + + if (err != OK) { + return err; + } + + err = get_token(p_stream, token, line, r_err_str); + + if (err != OK) { + return err; + } + + if (token.type != TK_PARENTHESIS_CLOSE) { + r_err_str = "Expected ')' after Struct data"; + return ERR_PARSE_ERROR; + } + + if (dict_value.get_type() != Variant::DICTIONARY) { + r_err_str = "Expected a Dictionary inside Struct()"; + return ERR_PARSE_ERROR; + } + + const Dictionary struct_data = dict_value; + + if (!struct_data.has("__type__")) { + r_err_str = "Invalid struct data: missing __type__ field"; + return ERR_PARSE_ERROR; + } + + // Requires explicit String/StringName tag. + const Variant type_value = struct_data["__type__"]; + + if (type_value.get_type() != Variant::STRING && type_value.get_type() != Variant::STRING_NAME) { + r_err_str = "Invalid struct data: __type__ must be a String or StringName"; + return ERR_PARSE_ERROR; + } + + const String type_name = type_value; + + if (type_name.is_empty()) { + r_err_str = "Invalid struct data: empty __type__ field"; + return ERR_PARSE_ERROR; + } + + // Only assign the output once construction succeeds, so a failed + // factory call doesn't leave a partially-mutated value behind. + Variant struct_value = ScriptServer::create_struct_instance(type_name, struct_data); + if (struct_value.get_type() != Variant::STRUCT) { + r_err_str = vformat("Failed to create struct instance for type '%s'", type_name); + return ERR_PARSE_ERROR; + } + + value = struct_value; + return OK; +#else + r_err_str = "Struct decoding requires the GDScript module"; + return ERR_PARSE_ERROR; +#endif } else if (id == "Object") { if (!p_allow_objects) { r_err_str = R"(Object decoding is prevented because "allow_objects" is false)"; @@ -2215,6 +2296,37 @@ Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_str } break; // Misc types. +#ifdef MODULE_GDSCRIPT_ENABLED + case Variant::STRUCT: { + const GDScriptStructInstance *struct_instance = VariantGetInternalPtr::get_ptr(&p_variant); + ERR_FAIL_NULL_V_MSG(struct_instance, ERR_INVALID_DATA, "Invalid Struct Variant: missing internal struct instance."); + + // This really shouldn't happen, but it is to be safe. + if (unlikely(p_recursion_count >= MAX_RECURSION)) { + ERR_PRINT("Max recursion reached"); + p_store_string_func(p_store_string_ud, "null"); + break; + } + + const Dictionary dict = struct_instance->serialize(); + const Variant *type_value = dict.getptr("__type__"); + ERR_FAIL_COND_V_MSG(type_value == nullptr || (type_value->get_type() != Variant::STRING && type_value->get_type() != Variant::STRING_NAME) || String(*type_value).is_empty(), ERR_INVALID_DATA, "Struct serialization produced an invalid __type__."); + + p_store_string_func(p_store_string_ud, "Struct("); + const Error err = write(dict, p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, p_recursion_count + 1, p_compat, p_full_objects); + + if (err != OK) { + return err; + } + + p_store_string_func(p_store_string_ud, ")"); + } 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 bcd622694fe..cdb65fab0d6 100644 --- a/core/variant/variant_setget.cpp +++ b/core/variant/variant_setget.cpp @@ -40,6 +40,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); @@ -263,6 +267,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); @@ -294,6 +308,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) { @@ -1098,6 +1127,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) { + // base points directly to the in-place struct instance, matching ptr_set + // and the other in-place keyed types (e.g. Dictionary). It is NOT a Variant*. + const 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(); + } + + // 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) { + // base points directly to the in-place struct instance, matching ptr_set + // and the other in-place keyed types (e.g. Dictionary). It is NOT a Variant*. + const GDScriptStructInstance *wrapper = reinterpret_cast(base); + 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; @@ -1131,6 +1321,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); @@ -1305,6 +1498,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 f076e606db6..d57a40c719a 100644 --- a/core/variant/variant_utility.cpp +++ b/core/variant/variant_utility.cpp @@ -951,6 +951,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: @@ -1091,7 +1094,7 @@ void VariantUtilityFunctions::push_warning(Callable::CallError &r_error, const V 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; } @@ -1106,12 +1109,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; } @@ -1121,12 +1121,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; } diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 3d6e28eeabb..e215ef5fd0d 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -3397,7 +3397,10 @@ Variable is of type [PackedVector4Array]. - + + Variable is of type struct (a GDScript value-semantics struct). + + Represents the size of the [enum Variant.Type] enum. diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index a2a9750f90b..391efa2c1d4 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -1334,6 +1334,9 @@ If [code]true[/code], copying or cutting without a selection is performed on all lines with a caret. Otherwise, copy and cut require a selection. + + If [code]true[/code], the script editor shows a warning when a [code]struct[/code] is declared, noting that structs are an experimental feature. Set to [code]false[/code] to hide this warning. + If [code]true[/code], automatically indents code when pressing the [kbd]Enter[/kbd] key based on blocks above the new line. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index b6862b7808b..584ccfd5b6c 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -556,6 +556,9 @@ If [code]true[/code], scripts in the [code]res://addons[/code] folder will not generate warnings. + + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a [code]struct[/code] is declared. Structs are an experimental feature and their syntax and behavior may change in future releases. + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when [method Node.get_node] (or the shorthand [code]$[/code]) is used as default value of a class variable without the [code]@onready[/code] annotation. diff --git a/doc/classes/Struct.xml b/doc/classes/Struct.xml new file mode 100644 index 00000000000..b095624d761 --- /dev/null +++ b/doc/classes/Struct.xml @@ -0,0 +1,21 @@ + + + + A built-in [Variant] type representing a GDScript struct instance. + + + The [Struct] type stores an instance of a struct declared in GDScript with the [code]struct[/code] keyword. Unlike objects, structs have value semantics: assigning a struct to a variable or passing it to a function copies its contents rather than sharing a reference. + [b]Note:[/b] Structs are an experimental feature. Their syntax and behavior may change in future versions. + + + + + + + + + Returns the value of the struct member named [param key]. Fails if no member with that name exists. + + + + diff --git a/editor/settings/editor_settings.cpp b/editor/settings/editor_settings.cpp index da3902dc6d0..1828a186b80 100644 --- a/editor/settings/editor_settings.cpp +++ b/editor/settings/editor_settings.cpp @@ -787,6 +787,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { // Behavior // Behavior: General _initial_set("text_editor/behavior/general/empty_selection_clipboard", true); + _initial_set("text_editor/behavior/general/show_experimental_struct_warning", true); // Behavior: Navigation _initial_set("text_editor/behavior/navigation/move_caret_on_right_click", true, true); 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 a17371d143f..b5ebc205ddb 100644 --- a/modules/gdscript/editor/gdscript_docgen.cpp +++ b/modules/gdscript/editor/gdscript_docgen.cpp @@ -155,6 +155,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 307cdf638d0..03683f4e57c 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -43,6 +43,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" @@ -58,6 +59,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" @@ -127,6 +130,98 @@ 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) { + r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; + 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 + r_error.error = Callable::CallError::CALL_OK; + 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; @@ -1598,6 +1693,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; @@ -1622,9 +1729,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); } @@ -2769,6 +2878,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. @@ -2839,6 +2949,23 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b return String(); // No class parsed. } + if (parser.is_file_struct()) { + if (r_base_type) { + *r_base_type = String(); + } + if (r_icon_path) { + *r_icon_path = String(); + } + if (r_is_abstract) { + *r_is_abstract = false; + } + if (r_is_tool) { + *r_is_tool = parser.is_tool(); + } + const GDScriptParser::StructNode *fs = parser.get_file_struct(); + return fs != nullptr && fs->identifier != nullptr ? String(fs->identifier->name) : String(); + } + /** * **WARNING** * @@ -3038,6 +3165,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) { @@ -3184,3 +3457,74 @@ 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(); +} + +} // 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 4864d110cb2..ee0fb2f2215 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -40,6 +40,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" @@ -65,6 +69,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; @@ -83,9 +109,11 @@ class GDScript : public Script { struct ClearData { RBSet functions; RBSet> scripts; + RBSet> structs; void clear() { functions.clear(); scripts.clear(); + structs.clear(); } }; @@ -98,6 +126,8 @@ class GDScript : public Script { friend class GDScriptLambdaSelfCallable; friend class GDScriptLanguage; friend struct GDScriptUtilityFunctionsDefinitions; + friend class GDScriptStruct; + friend class GDScriptStructInstance; Ref native; Ref base; @@ -116,6 +146,7 @@ class GDScript : public Script { HashMap member_functions; HashMap> subclasses; HashMap _signals; + HashMap> structs; Dictionary rpc_config; public: @@ -482,6 +513,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); @@ -600,6 +636,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 @@ -681,6 +728,12 @@ class GDScriptLanguage : public ScriptLanguage { 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 176afa7651f..d3c68765544 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -52,6 +52,10 @@ #include "core/templates/hash_map.h" #include "scene/main/node.h" +#ifdef TOOLS_ENABLED +#include "editor/settings/editor_settings.h" +#endif + #if defined(TOOLS_ENABLED) && !defined(DISABLE_DEPRECATED) #define SUGGEST_GODOT4_RENAMES #include "editor/project_upgrade/renames_map_3_to_4.h" @@ -794,7 +798,11 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type result.native_type = first; } else if (ScriptServer::is_global_class(first)) { if (GDScript::is_canonically_equal_paths(parser->script_path, ScriptServer::get_global_class_path(first))) { - result = parser->head->get_datatype(); + if (parser->is_file_struct()) { + result = make_struct_meta_type(parser->get_file_struct(), parser->script_path); + } else { + result = parser->head->get_datatype(); + } } else { String path = ScriptServer::get_global_class_path(first); String ext = path.get_extension(); @@ -804,7 +812,11 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type push_error(vformat(R"(Could not parse global class "%s" from "%s".)", first, ScriptServer::get_global_class_path(first)), p_type); return bad_type; } - result = ref->get_parser()->head->get_datatype(); + if (ref->get_parser()->is_file_struct()) { + result = make_struct_meta_type(ref->get_parser()->get_file_struct(), path); + } else { + result = ref->get_parser()->head->get_datatype(); + } } else { result = make_script_meta_type(ResourceLoader::load(path, "Script")); } @@ -872,6 +884,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; @@ -1262,6 +1278,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; @@ -1356,6 +1381,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); } } } @@ -1584,11 +1613,100 @@ 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; + } + +#ifdef TOOLS_ENABLED + // Structs are experimental: warn once per analysis on the first struct declaration. + // Editor-only, and suppressible via the editor setting. + if (!experimental_struct_warned) { + experimental_struct_warned = true; + bool show_warning = true; + const EditorSettings *editor_settings = EditorSettings::get_singleton(); + if (editor_settings != nullptr && editor_settings->has_setting("text_editor/behavior/general/show_experimental_struct_warning")) { + show_warning = editor_settings->get_setting("text_editor/behavior/general/show_experimental_struct_warning"); + } + if (show_warning) { + parser->push_warning(p_struct, GDScriptWarning::EXPERIMENTAL_STRUCT); + } + } +#endif + + // 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); + } + } + + // Validate the constructor once at declaration time. + if (p_struct->constructor != nullptr && p_struct->constructor->return_type != nullptr) { + push_error("Struct constructor cannot have an explicit return type.", p_struct->constructor->return_type); + } + + 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."); @@ -1602,6 +1720,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; @@ -1835,13 +1957,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) { @@ -1871,103 +2007,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 } @@ -2681,6 +2820,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: @@ -3900,12 +4040,32 @@ GDScriptParser::DataType GDScriptAnalyzer::make_global_class_meta_type(const Str return type; } + if (ref->get_parser()->is_file_struct()) { + return make_struct_meta_type(ref->get_parser()->get_file_struct(), path); + } + return ref->get_parser()->head->get_datatype(); } else { return make_script_meta_type(ResourceLoader::load(path, "Script")); } } +GDScriptParser::DataType GDScriptAnalyzer::make_struct_meta_type(GDScriptParser::StructNode *p_struct, const String &p_script_path) { + GDScriptParser::DataType type; + if (p_struct == nullptr) { + type.type_source = GDScriptParser::DataType::UNDETECTED; + type.kind = GDScriptParser::DataType::VARIANT; + return type; + } + type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + type.kind = GDScriptParser::DataType::STRUCT; + type.struct_type = p_struct; + type.is_meta_type = true; + type.is_constant = true; + type.script_path = p_script_path; + return type; +} + Ref GDScriptAnalyzer::ensure_cached_external_parser_for_class(const GDScriptParser::ClassNode *p_class, const GDScriptParser::ClassNode *p_from_class, const char *p_context, const GDScriptParser::Node *p_source) { if (p_class == nullptr) { return nullptr; @@ -4149,6 +4309,40 @@ 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). + // Ensure the struct body is resolved first so field datatypes are + // populated; otherwise valid field access can fail depending on the + // order in which structs and their users are analyzed. Guarded to be + // idempotent, so this is a no-op if already resolved. + resolve_struct_body(base.struct_type, p_identifier); + 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; @@ -4161,6 +4355,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; @@ -4238,6 +4446,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 } @@ -4759,6 +4978,20 @@ 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). + // A struct nested in a class still has current_class set to the enclosing + // class, so also reject when we're currently inside a struct body. + if (parser->current_class == nullptr || parser->current_struct != 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(); @@ -4968,6 +5201,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: @@ -5036,6 +5270,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; @@ -5768,6 +6003,149 @@ 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. + // The lookup runs per call site and must not push errors. + const GDScriptParser::FunctionNode *ctor = struct_node->constructor; + 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) { + r_return_type = func->get_datatype(); + + // 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) { + r_return_type = func->get_datatype(); + + // 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; @@ -6201,6 +6579,7 @@ bool GDScriptAnalyzer::check_type_compatibility(const GDScriptParser::DataType & StringName src_native; Ref