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..9abb8b2ec34 100644 --- a/core/extension/extension_api_dump.cpp +++ b/core/extension/extension_api_dump.cpp @@ -229,6 +229,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 }, }; @@ -271,6 +272,7 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) { static_assert(type_size_array[Variant::PACKED_VECTOR3_ARRAY][sizeof(void *)] == sizeof(PackedVector3Array), "Size of PackedVector3Array mismatch"); static_assert(type_size_array[Variant::PACKED_COLOR_ARRAY][sizeof(void *)] == sizeof(PackedColorArray), "Size of PackedColorArray mismatch"); static_assert(type_size_array[Variant::PACKED_VECTOR4_ARRAY][sizeof(void *)] == sizeof(PackedVector4Array), "Size of PackedVector4Array mismatch"); + static_assert(type_size_array[Variant::STRUCT][sizeof(void *)] == sizeof(Struct), "Size of Struct mismatch"); static_assert(type_size_array[Variant::VARIANT_MAX][sizeof(void *)] == sizeof(Variant), "Size of Variant mismatch"); Array core_type_sizes; diff --git a/core/extension/gdextension_interface.cpp b/core/extension/gdextension_interface.cpp index 18a92bc6a1e..133d85c8ca5 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: 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: 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: 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."); diff --git a/core/extension/gdextension_interface.h b/core/extension/gdextension_interface.h index 2f2479d8c01..18320e8adca 100644 --- a/core/extension/gdextension_interface.h +++ b/core/extension/gdextension_interface.h @@ -121,6 +121,7 @@ typedef enum { GDEXTENSION_VARIANT_TYPE_PACKED_VECTOR3_ARRAY, 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..5d3c4daea1c 100644 --- a/core/io/json.cpp +++ b/core/io/json.cpp @@ -41,6 +41,8 @@ #include "core/config/engine.h" #include "core/object/script_language.h" #include "core/variant/container_type_validate.h" +#include "core/variant/struct.h" +#include "core/variant/struct_info.h" const char *JSON::tk_name[TK_MAX] = { "'{'", @@ -1023,6 +1025,49 @@ Variant JSON::_from_native(const Variant &p_variant, bool p_full_objects, int p_ RETURN_ARGS; } break; + case Variant::STRUCT: { + ERR_FAIL_COND_V_MSG(p_depth > Variant::MAX_RECURSION_DEPTH, Variant(), "Variant is too deep. Bailing."); + const Struct s = p_variant; + Ref info = s.get_info(); + Dictionary ret; + ret[TYPE] = "Struct"; + if (info.is_null()) { + return ret; + } + + Dictionary schema; + schema["version"] = StructInfo::SERIALIZATION_VERSION; + schema["id"] = String(info->get_logical_type_id()); + Array fields_arr; + Array defaults_arr; + for (int i = 0; i < info->get_field_count(); i++) { + Dictionary fd; + fd["name"] = String(info->get_field_name(i)); + fd["type"] = String(StructInfo::type_to_token(info->get_field_type(i))); + fd["typed"] = info->is_field_typed(i); + if (info->get_field_class_name(i) != StringName()) { + fd["class_name"] = String(info->get_field_class_name(i)); + } + if (info->get_field_struct_type_id(i) != StringName()) { + fd["struct_type_id"] = String(info->get_field_struct_type_id(i)); + } + fields_arr.push_back(fd); + defaults_arr.push_back(_from_native(Struct::_make_serializable(info->_get_field_default_raw(i)), p_full_objects, p_depth + 1)); + } + schema["fields"] = fields_arr; + schema["defaults"] = defaults_arr; + + Array values_arr; + for (int i = 0; i < s.get_field_count(); i++) { + values_arr.push_back(_from_native(s.get_member_serializable(i), p_full_objects, p_depth + 1)); + } + + Dictionary args; + args["schema"] = schema; + args["values"] = values_arr; + ret[ARGS] = args; + return ret; + } break; case Variant::VARIANT_MAX: { // Nothing to do. } break; @@ -1070,6 +1115,23 @@ static bool _decode_container_type(const Dictionary &p_dict, const String &p_key ERR_FAIL_V_MSG(false, vformat(R"(Invalid type "%s".)", type_name)); } +static bool _json_decode_ok(const Variant &p_src, const Variant &p_decoded) { + if (p_decoded.get_type() != Variant::NIL) { + return true; + } + if (p_src.get_type() == Variant::NIL) { + return true; + } + if (p_src.get_type() == Variant::DICTIONARY) { + const Dictionary d = p_src; + const String type_name = d.get("type", String()); + if (Variant::get_type_by_name(type_name) == Variant::OBJECT || ClassDB::class_exists(type_name)) { + return true; + } + } + return false; +} + Variant JSON::_to_native(const Variant &p_json, bool p_allow_objects, int p_depth) { switch (p_json.get_type()) { case Variant::NIL: @@ -1325,7 +1387,10 @@ Variant JSON::_to_native(const Variant &p_json, bool p_allow_objects, int p_dept ERR_FAIL_COND_V_MSG(p_depth > Variant::MAX_RECURSION_DEPTH, ret, "Variant is too deep. Bailing."); for (int i = 0; i < args.size() / 2; i++) { - ret[_to_native(args[i * 2 + 0], p_allow_objects, p_depth + 1)] = _to_native(args[i * 2 + 1], p_allow_objects, p_depth + 1); + const Variant k = _to_native(args[i * 2 + 0], p_allow_objects, p_depth + 1); + const Variant v = _to_native(args[i * 2 + 1], p_allow_objects, p_depth + 1); + ERR_FAIL_COND_V_MSG(!_json_decode_ok(args[i * 2 + 0], k) || !_json_decode_ok(args[i * 2 + 1], v), Variant(), "Failed to decode a nested dictionary entry."); + ret[k] = v; } return ret; @@ -1350,7 +1415,9 @@ Variant JSON::_to_native(const Variant &p_json, bool p_allow_objects, int p_dept size_t args_size = args.size(); ret.resize(args_size); for (size_t i = 0; i < args_size; i++) { - ret[i] = _to_native(args[i], p_allow_objects, p_depth + 1); + const Variant e = _to_native(args[i], p_allow_objects, p_depth + 1); + ERR_FAIL_COND_V_MSG(!_json_decode_ok(args[i], e), Variant(), "Failed to decode a nested array element."); + ret[i] = e; } return ret; @@ -1477,6 +1544,61 @@ Variant JSON::_to_native(const Variant &p_json, bool p_allow_objects, int p_dept return arr; } break; + case Variant::STRUCT: { + ERR_FAIL_COND_V_MSG(p_depth > Variant::MAX_RECURSION_DEPTH, Variant(), "Variant is too deep. Bailing."); + if (!dict.has(ARGS)) { + return Struct(); + } + ERR_FAIL_COND_V_MSG(dict.get(ARGS, Variant()).get_type() != Variant::DICTIONARY, Variant(), R"(Struct "args" must be a Dictionary.)"); + Dictionary args = dict[ARGS]; + ERR_FAIL_COND_V_MSG(args.get("schema", Variant()).get_type() != Variant::DICTIONARY, Variant(), R"(Struct is missing a Dictionary "schema".)"); + Dictionary schema = args["schema"]; + ERR_FAIL_COND_V_MSG(uint32_t(int64_t(schema.get("version", 0))) != StructInfo::SERIALIZATION_VERSION, Variant(), "Unsupported struct schema version."); + ERR_FAIL_COND_V_MSG(schema.get("id", Variant()).get_type() != Variant::STRING, Variant(), "Struct schema id must be a String."); + const String id = schema["id"]; + ERR_FAIL_COND_V_MSG(id.is_empty(), Variant(), "Struct schema has an empty logical id."); + ERR_FAIL_COND_V_MSG(schema.get("fields", Variant()).get_type() != Variant::ARRAY, Variant(), R"(Struct "fields" must be an Array.)"); + ERR_FAIL_COND_V_MSG(schema.get("defaults", Variant()).get_type() != Variant::ARRAY, Variant(), R"(Struct "defaults" must be an Array.)"); + Array fields_arr = schema["fields"]; + Array defaults_arr = schema["defaults"]; + ERR_FAIL_COND_V_MSG(defaults_arr.size() != fields_arr.size(), Variant(), "Struct defaults do not match its field count."); + + StructInfoBuilder b; + b.set_logical_type_id(StringName(id)); + for (int i = 0; i < fields_arr.size(); i++) { + ERR_FAIL_COND_V_MSG(fields_arr[i].get_type() != Variant::DICTIONARY, Variant(), "Struct field descriptor must be a Dictionary."); + Dictionary fd = fields_arr[i]; + StructInfo::Field f; + f.name = StringName(String(fd.get("name", String()))); + f.type = StructInfo::type_from_token(String(fd.get("type", "nil"))); + ERR_FAIL_COND_V_MSG(f.type == Variant::VARIANT_MAX, Variant(), "Unknown struct field type token."); + f.is_typed = bool(fd.get("typed", false)); + if (fd.has("class_name")) { + f.class_name = StringName(String(fd["class_name"])); + } + if (fd.has("struct_type_id")) { + f.struct_type_id = StringName(String(fd["struct_type_id"])); + } + const Variant def_src = defaults_arr[i]; + f.default_value = _to_native(def_src, p_allow_objects, p_depth + 1); + ERR_FAIL_COND_V_MSG(!_json_decode_ok(def_src, f.default_value), Variant(), "Failed to decode a nested struct default."); + b.add_field(f); + } + Ref info = b.build(); + ERR_FAIL_COND_V_MSG(info.is_null(), Variant(), "Failed to rebuild StructInfo from JSON."); + + Struct s(info); + ERR_FAIL_COND_V_MSG(args.get("values", Variant()).get_type() != Variant::ARRAY, Variant(), R"(Struct "values" must be an Array.)"); + Array values_arr = args["values"]; + ERR_FAIL_COND_V_MSG(values_arr.size() != s.get_field_count(), Variant(), "Struct value count does not match its field count."); + for (int i = 0; i < values_arr.size(); i++) { + const Variant val_src = values_arr[i]; + const Variant val = _to_native(val_src, p_allow_objects, p_depth + 1); + ERR_FAIL_COND_V_MSG(!_json_decode_ok(val_src, val), Variant(), "Failed to decode a nested struct value."); + ERR_FAIL_COND_V_MSG(!s.try_set_member(i, val), Variant(), "Struct value incompatible with its field schema."); + } + return s; + } break; case Variant::VARIANT_MAX: { // Nothing to do. } break; @@ -1513,6 +1635,7 @@ Variant JSON::_to_native(const Variant &p_json, bool p_allow_objects, int p_dept for (int i = 0; i < props.size() / 2; i++) { const StringName name = props[i * 2 + 0]; const Variant value = _to_native(props[i * 2 + 1], p_allow_objects, p_depth + 1); + ERR_FAIL_COND_V_MSG(!_json_decode_ok(props[i * 2 + 1], value), Variant(), "Failed to decode a nested object property."); if (name == CoreStringName(script) && value.get_type() != Variant::NIL) { const String path = value; @@ -1544,7 +1667,9 @@ Variant JSON::_to_native(const Variant &p_json, bool p_allow_objects, int p_dept size_t arr_size = arr.size(); ret.resize(arr_size); for (size_t i = 0; i < arr_size; i++) { - ret[i] = _to_native(arr[i], p_allow_objects, p_depth + 1); + const Variant e = _to_native(arr[i], p_allow_objects, p_depth + 1); + ERR_FAIL_COND_V_MSG(!_json_decode_ok(arr[i], e), Variant(), "Failed to decode a nested array element."); + ret[i] = e; } return ret; diff --git a/core/io/marshalls.cpp b/core/io/marshalls.cpp index dc857f044cd..428b3f5faba 100644 --- a/core/io/marshalls.cpp +++ b/core/io/marshalls.cpp @@ -42,6 +42,9 @@ #include "core/object/ref_counted.h" #include "core/object/script_language.h" #include "core/variant/container_type_validate.h" +#include "core/variant/struct.h" +#include "core/variant/struct_info.h" +#include "core/variant/variant_internal.h" #include #include @@ -1301,6 +1304,113 @@ Error decode_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int } r_variant = varray; + } break; + case Variant::STRUCT: { + String id; + { + Error err = _decode_string(buf, len, r_len, id); + if (err) { + return err; + } + } + if (id.is_empty()) { + r_variant = Struct(); + break; + } + + ERR_FAIL_COND_V(len < 4, ERR_INVALID_DATA); + uint32_t version = decode_uint32(buf); + buf += 4; + len -= 4; + if (r_len) { + (*r_len) += 4; + } + ERR_FAIL_COND_V_MSG(version != StructInfo::SERIALIZATION_VERSION, ERR_INVALID_DATA, "Unsupported struct schema version."); + + ERR_FAIL_COND_V(len < 4, ERR_INVALID_DATA); + int32_t field_count = decode_uint32(buf); + buf += 4; + len -= 4; + if (r_len) { + (*r_len) += 4; + } + ERR_FAIL_COND_V(field_count < 0, ERR_INVALID_DATA); + + StructInfoBuilder b; + b.set_logical_type_id(StringName(id)); + for (int i = 0; i < field_count; i++) { + StructInfo::Field f; + { + String name; + Error err = _decode_string(buf, len, r_len, name); + if (err) { + return err; + } + f.name = StringName(name); + } + { + String token; + Error err = _decode_string(buf, len, r_len, token); + if (err) { + return err; + } + f.type = StructInfo::type_from_token(token); + ERR_FAIL_COND_V_MSG(f.type == Variant::VARIANT_MAX, ERR_INVALID_DATA, "Unknown struct field type token."); + } + ERR_FAIL_COND_V(len < 4, ERR_INVALID_DATA); + f.is_typed = decode_uint32(buf) != 0; + buf += 4; + len -= 4; + if (r_len) { + (*r_len) += 4; + } + { + String cn; + Error err = _decode_string(buf, len, r_len, cn); + if (err) { + return err; + } + f.class_name = StringName(cn); + } + { + String st; + Error err = _decode_string(buf, len, r_len, st); + if (err) { + return err; + } + f.struct_type_id = StringName(st); + } + { + int used; + Error err = decode_variant(f.default_value, buf, len, &used, p_allow_objects, p_depth + 1); + ERR_FAIL_COND_V_MSG(err, err, "Error decoding struct field default."); + buf += used; + len -= used; + if (r_len) { + (*r_len) += used; + } + } + b.add_field(f); + } + Ref info = b.build(); + ERR_FAIL_COND_V(info.is_null(), ERR_INVALID_DATA); + ERR_FAIL_COND_V_MSG(info->get_field_count() != field_count, ERR_INVALID_DATA, "Decoded struct schema field count mismatch."); + + Struct s(info); + for (int i = 0; i < field_count; i++) { + Variant value; + int used; + Error err = decode_variant(value, buf, len, &used, p_allow_objects, p_depth + 1); + ERR_FAIL_COND_V_MSG(err, err, "Error decoding struct value."); + buf += used; + len -= used; + if (r_len) { + (*r_len) += used; + } + ERR_FAIL_COND_V_MSG(!s.try_set_member(i, value), ERR_INVALID_DATA, "Struct value incompatible with its field schema."); + } + r_variant = s; + } break; default: { ERR_FAIL_V(ERR_BUG); @@ -1903,6 +2013,62 @@ Error encode_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len, bo r_len += len; } + } break; + case Variant::STRUCT: { + const Struct &s = *VariantInternal::get_struct(&p_variant); + const Ref info = s.get_info(); + const int field_count = info.is_valid() ? info->get_field_count() : 0; + + _encode_string(info.is_valid() ? String(info->get_logical_type_id()) : String(), buf, r_len); + if (info.is_null()) { + break; + } + + if (buf) { + encode_uint32(StructInfo::SERIALIZATION_VERSION, buf); + buf += 4; + } + r_len += 4; + + if (buf) { + encode_uint32(uint32_t(field_count), buf); + buf += 4; + } + r_len += 4; + + for (int i = 0; i < field_count; i++) { + _encode_string(String(info->get_field_name(i)), buf, r_len); + _encode_string(String(StructInfo::type_to_token(info->get_field_type(i))), buf, r_len); + if (buf) { + encode_uint32(info->is_field_typed(i) ? 1 : 0, buf); + buf += 4; + } + r_len += 4; + _encode_string(String(info->get_field_class_name(i)), buf, r_len); + _encode_string(String(info->get_field_struct_type_id(i)), buf, r_len); + + int len; + Error err = encode_variant(Struct::_make_serializable(info->_get_field_default_raw(i)), buf, len, p_full_objects, p_depth + 1); + ERR_FAIL_COND_V(err, err); + ERR_FAIL_COND_V(len % 4, ERR_BUG); + r_len += len; + if (buf) { + buf += len; + } + } + + for (int i = 0; i < field_count; i++) { + const Variant member = s.get_member_serializable(i); + int len; + Error err = encode_variant(member, buf, len, p_full_objects, p_depth + 1); + ERR_FAIL_COND_V(err, err); + ERR_FAIL_COND_V(len % 4, ERR_BUG); + r_len += len; + if (buf) { + buf += len; + } + } + } break; // Packed arrays. diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp index 987645de53c..14b4386a764 100644 --- a/core/io/resource_format_binary.cpp +++ b/core/io/resource_format_binary.cpp @@ -43,6 +43,9 @@ #include "core/io/file_access_compressed.h" #include "core/io/missing_resource.h" #include "core/object/script_language.h" +#include "core/variant/struct.h" +#include "core/variant/struct_info.h" +#include "core/variant/variant_internal.h" #include "core/version.h" #include "scene/property_utils.h" #include "scene/resources/packed_scene.h" @@ -94,6 +97,7 @@ enum { VARIANT_VECTOR4I = 51, VARIANT_PROJECTION = 52, VARIANT_PACKED_VECTOR4_ARRAY = 53, + VARIANT_STRUCT = 54, OBJECT_EMPTY = 0, OBJECT_EXTERNAL_RESOURCE = 1, OBJECT_INTERNAL_RESOURCE = 2, @@ -513,6 +517,53 @@ Error ResourceLoaderBinary::parse_variant(Variant &r_v) { } r_v = a; + } break; + case VARIANT_STRUCT: { + Variant id_v; + Error err = parse_variant(id_v); + ERR_FAIL_COND_V_MSG(err, ERR_FILE_CORRUPT, "Error parsing struct id."); + const String id = id_v; + if (id.is_empty()) { + r_v = Struct(); + break; + } + uint32_t version = f->get_32(); + ERR_FAIL_COND_V_MSG(version != StructInfo::SERIALIZATION_VERSION, ERR_FILE_CORRUPT, "Unsupported struct schema version."); + uint32_t field_count = f->get_32(); + StructInfoBuilder b; + b.set_logical_type_id(StringName(id)); + for (uint32_t i = 0; i < field_count; i++) { + StructInfo::Field field; + Variant v; + err = parse_variant(v); + ERR_FAIL_COND_V_MSG(err, ERR_FILE_CORRUPT, "Error parsing struct field name."); + field.name = StringName(String(v)); + err = parse_variant(v); + ERR_FAIL_COND_V_MSG(err, ERR_FILE_CORRUPT, "Error parsing struct field type."); + field.type = StructInfo::type_from_token(String(v)); + ERR_FAIL_COND_V_MSG(field.type == Variant::VARIANT_MAX, ERR_FILE_CORRUPT, "Unknown struct field type token."); + field.is_typed = f->get_32() != 0; + err = parse_variant(v); + ERR_FAIL_COND_V_MSG(err, ERR_FILE_CORRUPT, "Error parsing struct field class_name."); + field.class_name = StringName(String(v)); + err = parse_variant(v); + ERR_FAIL_COND_V_MSG(err, ERR_FILE_CORRUPT, "Error parsing struct field struct_type_id."); + field.struct_type_id = StringName(String(v)); + err = parse_variant(field.default_value); + ERR_FAIL_COND_V_MSG(err, ERR_FILE_CORRUPT, "Error parsing struct field default."); + b.add_field(field); + } + Ref info = b.build(); + ERR_FAIL_COND_V(info.is_null(), ERR_FILE_CORRUPT); + Struct s(info); + for (uint32_t i = 0; i < field_count; i++) { + Variant val; + err = parse_variant(val); + ERR_FAIL_COND_V_MSG(err, ERR_FILE_CORRUPT, "Error parsing struct value."); + ERR_FAIL_COND_V_MSG(!s.try_set_member(i, val), ERR_FILE_CORRUPT, "Struct value incompatible with its field schema."); + } + r_v = s; + } break; case VARIANT_PACKED_BYTE_ARRAY: { uint32_t len = f->get_32(); @@ -1921,6 +1972,31 @@ void ResourceFormatSaverBinaryInstance::write_variant(Ref f, const V write_variant(f, var, resource_map, external_resources, string_map); } + } break; + case Variant::STRUCT: { + f->store_32(VARIANT_STRUCT); + const Struct &s = *VariantInternal::get_struct(&p_property); + const Ref info = s.get_info(); + const int field_count = info.is_valid() ? info->get_field_count() : 0; + + write_variant(f, info.is_valid() ? String(info->get_logical_type_id()) : String(), resource_map, external_resources, string_map); + if (info.is_null()) { + break; + } + f->store_32(StructInfo::SERIALIZATION_VERSION); + f->store_32(uint32_t(field_count)); + for (int i = 0; i < field_count; i++) { + write_variant(f, String(info->get_field_name(i)), resource_map, external_resources, string_map); + write_variant(f, String(StructInfo::type_to_token(info->get_field_type(i))), resource_map, external_resources, string_map); + f->store_32(info->is_field_typed(i) ? 1 : 0); + write_variant(f, String(info->get_field_class_name(i)), resource_map, external_resources, string_map); + write_variant(f, String(info->get_field_struct_type_id(i)), resource_map, external_resources, string_map); + write_variant(f, Struct::_make_serializable(info->_get_field_default_raw(i)), resource_map, external_resources, string_map); + } + for (int i = 0; i < field_count; i++) { + write_variant(f, s.get_member_serializable(i), resource_map, external_resources, string_map); + } + } break; case Variant::PACKED_BYTE_ARRAY: { f->store_32(VARIANT_PACKED_BYTE_ARRAY); @@ -2119,6 +2195,18 @@ void ResourceFormatSaverBinaryInstance::_find_resources(const Variant &p_variant _find_resources(kv.value); } } break; + case Variant::STRUCT: { + const Struct &s = *VariantInternal::get_struct(&p_variant); + const Ref info = s.get_info(); + if (info.is_valid()) { + for (int i = 0; i < info->get_field_count(); i++) { + _find_resources(info->_get_field_default_raw(i)); + } + } + for (int i = 0; i < s.get_field_count(); i++) { + _find_resources(s.get_member(i)); + } + } break; case Variant::NODE_PATH: { //take the chance and save node path strings NodePath np = p_variant; diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index 37601f1b6cf..9399e140181 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -281,6 +281,7 @@ Error ScriptServer::unregister_language(const ScriptLanguage *p_language) { void ScriptServer::init_languages() { { // Load global classes. global_classes_clear(); + global_structs_clear(); #ifndef DISABLE_DEPRECATED if (ProjectSettings::get_singleton()->has_setting("_global_script_classes")) { Array script_classes = GLOBAL_GET("_global_script_classes"); @@ -351,6 +352,7 @@ void ScriptServer::finish_languages() { } global_classes_clear(); + global_structs_clear(); } bool ScriptServer::are_languages_initialized() { @@ -405,12 +407,62 @@ void ScriptServer::thread_exit() { HashMap ScriptServer::global_classes; HashMap> ScriptServer::inheriters_cache; bool ScriptServer::inheriters_cache_dirty = true; +HashMap ScriptServer::global_structs; void ScriptServer::global_classes_clear() { global_classes.clear(); inheriters_cache.clear(); } +void ScriptServer::global_structs_clear() { + global_structs.clear(); +} + +void ScriptServer::add_global_struct(const StringName &p_struct, const StringName &p_language, const String &p_path) { + GlobalScriptStruct *existing = global_structs.getptr(p_struct); + if (existing != nullptr) { + existing->language = p_language; + existing->path = p_path; + return; + } + GlobalScriptStruct g; + g.language = p_language; + g.path = p_path; + global_structs[p_struct] = g; +} + +void ScriptServer::remove_global_struct_by_path(const String &p_path) { + List to_remove; + for (const KeyValue &kv : global_structs) { + if (kv.value.path == p_path) { + to_remove.push_back(kv.key); + } + } + for (const StringName &name : to_remove) { + global_structs.erase(name); + } +} + +bool ScriptServer::is_global_struct(const StringName &p_struct) { + return global_structs.has(p_struct); +} + +StringName ScriptServer::get_global_struct_language(const StringName &p_struct) { + const GlobalScriptStruct *g = global_structs.getptr(p_struct); + return g ? g->language : StringName(); +} + +String ScriptServer::get_global_struct_path(const StringName &p_struct) { + const GlobalScriptStruct *g = global_structs.getptr(p_struct); + return g ? g->path : String(); +} + +void ScriptServer::get_global_struct_list(List *r_global_structs) { + for (const KeyValue &kv : global_structs) { + r_global_structs->push_back(kv.key); + } +} + void ScriptServer::add_global_class(const StringName &p_class, const StringName &p_base, const StringName &p_language, const String &p_path, bool p_is_abstract, bool p_is_tool) { ERR_FAIL_COND_MSG(p_class == p_base || (global_classes.has(p_base) && get_global_class_native_base(p_base) == p_class), "Cyclic inheritance in script class."); GlobalScriptClass *existing = global_classes.getptr(p_class); diff --git a/core/object/script_language.h b/core/object/script_language.h index e7cff1d1e6c..4ece3dc473f 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -78,6 +78,12 @@ class ScriptServer { static HashMap> inheriters_cache; static bool inheriters_cache_dirty; + struct GlobalScriptStruct { + StringName language; + String path; + }; + static HashMap global_structs; + public: static ScriptEditRequestFunction edit_request_func; @@ -110,6 +116,14 @@ class ScriptServer { static void get_inheriters_list(const StringName &p_base_type, List *r_classes); static void save_global_classes(); + static void global_structs_clear(); + static void add_global_struct(const StringName &p_struct, const StringName &p_language, const String &p_path); + static void remove_global_struct_by_path(const String &p_path); + static bool is_global_struct(const StringName &p_struct); + static StringName get_global_struct_language(const StringName &p_struct); + static String get_global_struct_path(const StringName &p_struct); + static void get_global_struct_list(List *r_global_structs); + static Vector> capture_script_backtraces(bool p_include_variables = false); static void init_languages(); @@ -471,6 +485,7 @@ 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(); } + virtual void get_global_struct_names(const String &p_path, List *r_names) const {} virtual ~ScriptLanguage() {} }; diff --git a/core/variant/method_ptrcall.h b/core/variant/method_ptrcall.h index bb5dcb7db24..e56cf5e6521 100644 --- a/core/variant/method_ptrcall.h +++ b/core/variant/method_ptrcall.h @@ -226,6 +226,8 @@ struct PtrToArg : Internal::PtrToArgDirect {}; template <> struct PtrToArg : Internal::PtrToArgDirect {}; template <> +struct PtrToArg : Internal::PtrToArgDirect {}; +template <> struct PtrToArg : Internal::PtrToArgDirect {}; template <> struct PtrToArg : Internal::PtrToArgDirect {}; diff --git a/core/variant/struct.cpp b/core/variant/struct.cpp new file mode 100644 index 00000000000..8cc72c9c996 --- /dev/null +++ b/core/variant/struct.cpp @@ -0,0 +1,287 @@ +/**************************************************************************/ +/* struct.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* REDOT ENGINE */ +/* https://redotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2024-present Redot Engine contributors */ +/* (see REDOT_AUTHORS.md) */ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "struct.h" + +#include "core/os/memory.h" +#include "core/variant/struct_info.h" +#include "core/variant/variant.h" + +#include + +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); +static_assert(std::is_move_constructible_v); +static_assert(std::is_move_assignable_v); + +static_assert(std::is_nothrow_default_constructible_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_assignable_v); +static_assert(std::is_nothrow_destructible_v); + +struct StructData { + Ref info; + Variant *values = nullptr; + int field_count = 0; + + static StructData *create(const Ref &p_info) { + ERR_FAIL_COND_V_MSG(p_info.is_null(), nullptr, + "Cannot construct a Struct without a schema."); + ERR_FAIL_COND_V_MSG(!p_info->is_frozen(), nullptr, + "Cannot construct a Struct from an unfinished (unfrozen) schema."); + + StructData *d = memnew(StructData); + d->info = p_info; + d->field_count = p_info->get_field_count(); + if (d->field_count > 0) { + d->values = memnew_arr(Variant, d->field_count); + for (int i = 0; i < d->field_count; i++) { + d->values[i] = p_info->instantiate_default(i); + } + } + return d; + } + + ~StructData() { + if (values) { + memdelete_arr(values); + values = nullptr; + } + } +}; + +StructData *Struct::_copy_data(const StructData *p_from) { + if (!p_from) { + return nullptr; + } + DEV_ASSERT(p_from->info.is_valid()); + DEV_ASSERT(p_from->info->is_frozen()); + DEV_ASSERT(p_from->field_count >= 0); + + StructData *d = memnew(StructData); + d->info = p_from->info; + d->field_count = p_from->field_count; + if (d->field_count > 0) { + d->values = memnew_arr(Variant, d->field_count); + for (int i = 0; i < d->field_count; i++) { + d->values[i] = p_from->values[i]; + } + } + return d; +} + +void Struct::_free_data() noexcept { + if (_p) { + memdelete(_p); + _p = nullptr; + } +} + +Struct::Struct(const Ref &p_info) { + _p = StructData::create(p_info); +} + +Struct::Struct(const Struct &p_from) { + _p = _copy_data(p_from._p); +} + +Struct::Struct(Struct &&p_from) noexcept { + _p = p_from._p; + p_from._p = nullptr; +} + +Struct &Struct::operator=(const Struct &p_from) { + if (this == &p_from) { + return *this; + } + _free_data(); + _p = _copy_data(p_from._p); + return *this; +} + +Struct &Struct::operator=(Struct &&p_from) noexcept { + if (this == &p_from) { + return *this; + } + _free_data(); + _p = p_from._p; + p_from._p = nullptr; + return *this; +} + +Struct::~Struct() noexcept { + _free_data(); +} + +Ref Struct::get_info() const { + return _p ? _p->info : Ref(); +} + +StringName Struct::get_type_id() const { + // A non-null _p always has a valid, frozen info (see create()/_copy_data()). + return _p ? _p->info->get_logical_type_id() : StringName(); +} + +uint64_t Struct::get_layout_hash() const noexcept { + return _p ? _p->info->get_layout_hash() : 0; +} + +int Struct::get_field_count() const noexcept { + return _p ? _p->field_count : 0; +} + +Variant Struct::get_member(int p_index) const { + ERR_FAIL_NULL_V(_p, Variant()); + ERR_FAIL_INDEX_V(p_index, _p->field_count, Variant()); + return _p->values[p_index]; +} + +Variant Struct::_make_serializable(const Variant &p_value) { + if (p_value.get_type() == Variant::OBJECT && p_value.get_validated_object() == nullptr) { + return Variant(); + } + return p_value; +} + +Variant Struct::get_member_serializable(int p_index) const { + return _make_serializable(get_member(p_index)); +} + +void Struct::set_member(int p_index, const Variant &p_value) { + ERR_FAIL_NULL(_p); + ERR_FAIL_INDEX(p_index, _p->field_count); + Variant normalized; + ERR_FAIL_COND_MSG(!_p->info->normalize_value(p_index, p_value, normalized), + vformat(R"(Value of type "%s" is incompatible with struct field %d.)", + Variant::get_type_name(p_value.get_type()), p_index)); + _p->values[p_index] = normalized; +} + +bool Struct::get_named(const StringName &p_name, Variant &r_value) const { + // A non-null _p always has a valid info; a valid index_of() result is always + // within [0, field_count) since both derive from the same frozen schema. + if (!_p) { + return false; + } + const int idx = _p->info->index_of(p_name); + if (idx < 0) { + return false; + } + DEV_ASSERT(idx < _p->field_count); + r_value = _p->values[idx]; + return true; +} + +bool Struct::set_named(const StringName &p_name, const Variant &p_value) { + if (!_p) { + return false; + } + const int idx = _p->info->index_of(p_name); + if (idx < 0) { + return false; + } + DEV_ASSERT(idx < _p->field_count); + Variant normalized; + ERR_FAIL_COND_V_MSG(!_p->info->normalize_value(idx, p_value, normalized), false, + vformat(R"(Value of type "%s" is incompatible with struct field "%s".)", + Variant::get_type_name(p_value.get_type()), p_name)); + _p->values[idx] = normalized; + return true; +} + +uint32_t Struct::recursive_hash(int recursion_count) const { + if (recursion_count > MAX_RECURSION) { + ERR_PRINT("Max recursion reached"); + return 0; + } + uint32_t h = hash_murmur3_one_32(Variant::STRUCT); + if (_p) { + h = hash_murmur3_one_32(_p->info->get_logical_type_id().hash(), h); + h = hash_murmur3_one_64(_p->info->get_layout_hash(), h); + recursion_count++; + for (int i = 0; i < _p->field_count; i++) { + h = hash_murmur3_one_32(_p->values[i].recursive_hash(recursion_count), h); + } + } + return hash_fmix32(h); +} + +int Struct::index_of(const StringName &p_name) const { + return _p ? _p->info->index_of(p_name) : -1; +} + +bool Struct::try_set_member(int p_index, const Variant &p_value) { + Variant normalized; + if (!_p || p_index < 0 || p_index >= _p->field_count || !_p->info->normalize_value(p_index, p_value, normalized)) { + return false; + } + _p->values[p_index] = normalized; + return true; +} + +Struct Struct::recursive_duplicate(bool p_deep, ResourceDeepDuplicateMode p_deep_subresources_mode, int recursion_count) const { + if (!p_deep || !_p) { + return *this; + } + if (recursion_count > MAX_RECURSION) { + ERR_PRINT("Max recursion reached"); + return *this; + } + recursion_count++; + Struct dup = *this; + for (int i = 0; i < dup._p->field_count; i++) { + dup._p->values[i] = dup._p->values[i].recursive_duplicate(p_deep, p_deep_subresources_mode, recursion_count); + } + return dup; +} + +bool Struct::operator==(const Struct &p_other) const { + if (_p == p_other._p) { + return true; + } + if (!_p || !p_other._p) { + return false; + } + const StructInfo *lhs_info = _p->info.ptr(); + const StructInfo *rhs_info = p_other._p->info.ptr(); + // Same frozen schema => same layout (and same field_count); skip the + // structural comparison in the common case where instances share a schema. + if (lhs_info != rhs_info && !lhs_info->is_same_layout_as(*rhs_info)) { + return false; + } + for (int i = 0; i < _p->field_count; i++) { + if (!(_p->values[i] == p_other._p->values[i])) { + return false; + } + } + return true; +} diff --git a/core/variant/struct.h b/core/variant/struct.h new file mode 100644 index 00000000000..5138d402f5b --- /dev/null +++ b/core/variant/struct.h @@ -0,0 +1,83 @@ +/**************************************************************************/ +/* struct.h */ +/**************************************************************************/ +/* This file is part of: */ +/* REDOT ENGINE */ +/* https://redotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2024-present Redot Engine contributors */ +/* (see REDOT_AUTHORS.md) */ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/string/string_name.h" +#include "core/variant/variant_deep_duplicate.h" + +template +class Ref; +class Variant; +class StructInfo; +struct StructData; + +class Struct { + StructData *_p = nullptr; + + void _free_data() noexcept; + static StructData *_copy_data(const StructData *p_from); + +public: + Struct() noexcept = default; + explicit Struct(const Ref &p_info); + Struct(const Struct &p_from); + Struct(Struct &&p_from) noexcept; + Struct &operator=(const Struct &p_from); + Struct &operator=(Struct &&p_from) noexcept; + ~Struct() noexcept; + + bool is_null() const noexcept { return _p == nullptr; } + + Ref get_info() const; + StringName get_type_id() const; + uint64_t get_layout_hash() const noexcept; + int get_field_count() const noexcept; + + Variant get_member(int p_index) const; + void set_member(int p_index, const Variant &p_value); + + static Variant _make_serializable(const Variant &p_value); + Variant get_member_serializable(int p_index) const; + + bool get_named(const StringName &p_name, Variant &r_value) const; + bool set_named(const StringName &p_name, const Variant &p_value); + + int index_of(const StringName &p_name) const; + bool try_set_member(int p_index, const Variant &p_value); + + uint32_t recursive_hash(int recursion_count) const; + Struct recursive_duplicate(bool p_deep, ResourceDeepDuplicateMode p_deep_subresources_mode, int recursion_count) const; + + bool operator==(const Struct &p_other) const; + bool operator!=(const Struct &p_other) const { return !(*this == p_other); } +}; diff --git a/core/variant/struct_info.cpp b/core/variant/struct_info.cpp new file mode 100644 index 00000000000..486c4552812 --- /dev/null +++ b/core/variant/struct_info.cpp @@ -0,0 +1,244 @@ +/**************************************************************************/ +/* struct_info.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* REDOT ENGINE */ +/* https://redotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2024-present Redot Engine contributors */ +/* (see REDOT_AUTHORS.md) */ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "struct_info.h" + +#include "core/object/class_db.h" +#include "core/object/object.h" +#include "core/string/ustring.h" +#include "core/variant/struct.h" +#include "core/variant/variant_internal.h" + +const char *StructInfo::type_to_token(Variant::Type p_type) { + switch (p_type) { + case Variant::NIL: + return "nil"; + case Variant::BOOL: + return "bool"; + case Variant::INT: + return "int"; + case Variant::FLOAT: + return "float"; + case Variant::STRING: + return "string"; + case Variant::VECTOR2: + return "vector2"; + case Variant::VECTOR2I: + return "vector2i"; + case Variant::RECT2: + return "rect2"; + case Variant::RECT2I: + return "rect2i"; + case Variant::VECTOR3: + return "vector3"; + case Variant::VECTOR3I: + return "vector3i"; + case Variant::TRANSFORM2D: + return "transform2d"; + case Variant::VECTOR4: + return "vector4"; + case Variant::VECTOR4I: + return "vector4i"; + case Variant::PLANE: + return "plane"; + case Variant::QUATERNION: + return "quaternion"; + case Variant::AABB: + return "aabb"; + case Variant::BASIS: + return "basis"; + case Variant::TRANSFORM3D: + return "transform3d"; + case Variant::PROJECTION: + return "projection"; + case Variant::COLOR: + return "color"; + case Variant::STRING_NAME: + return "string_name"; + case Variant::NODE_PATH: + return "node_path"; + case Variant::RID: + return "rid"; + case Variant::OBJECT: + return "object"; + case Variant::CALLABLE: + return "callable"; + case Variant::SIGNAL: + return "signal"; + case Variant::DICTIONARY: + return "dictionary"; + case Variant::ARRAY: + return "array"; + case Variant::PACKED_BYTE_ARRAY: + return "packed_byte_array"; + case Variant::PACKED_INT32_ARRAY: + return "packed_int32_array"; + case Variant::PACKED_INT64_ARRAY: + return "packed_int64_array"; + case Variant::PACKED_FLOAT32_ARRAY: + return "packed_float32_array"; + case Variant::PACKED_FLOAT64_ARRAY: + return "packed_float64_array"; + case Variant::PACKED_STRING_ARRAY: + return "packed_string_array"; + case Variant::PACKED_VECTOR2_ARRAY: + return "packed_vector2_array"; + case Variant::PACKED_VECTOR3_ARRAY: + return "packed_vector3_array"; + case Variant::PACKED_COLOR_ARRAY: + return "packed_color_array"; + case Variant::PACKED_VECTOR4_ARRAY: + return "packed_vector4_array"; + case Variant::STRUCT: + return "struct"; + case Variant::VARIANT_MAX: + return ""; + } + return ""; +} + +Variant::Type StructInfo::type_from_token(const String &p_token) { + for (int t = 0; t < Variant::VARIANT_MAX; t++) { + if (p_token == type_to_token((Variant::Type)t)) { + return (Variant::Type)t; + } + } + return Variant::VARIANT_MAX; +} + +static void _append_u32_le(Vector &r_buf, uint32_t p_value) { + r_buf.push_back(p_value & 0xFF); + r_buf.push_back((p_value >> 8) & 0xFF); + r_buf.push_back((p_value >> 16) & 0xFF); + r_buf.push_back((p_value >> 24) & 0xFF); +} + +static void _append_lp(Vector &r_buf, const String &p_str) { + const CharString utf8 = p_str.utf8(); + _append_u32_le(r_buf, (uint32_t)utf8.length()); + const uint8_t *data = (const uint8_t *)utf8.get_data(); + for (int i = 0; i < utf8.length(); i++) { + r_buf.push_back(data[i]); + } +} + +static uint32_t _murmur3_32_le(const uint8_t *p_data, int p_len, uint32_t p_seed) { + const uint32_t c1 = 0xcc9e2d51; + const uint32_t c2 = 0x1b873593; + uint32_t h = p_seed; + const int nblocks = p_len / 4; + for (int i = 0; i < nblocks; i++) { + const int o = i * 4; + uint32_t k = uint32_t(p_data[o]) | (uint32_t(p_data[o + 1]) << 8) | (uint32_t(p_data[o + 2]) << 16) | (uint32_t(p_data[o + 3]) << 24); + k *= c1; + k = hash_rotl32(k, 15); + k *= c2; + h ^= k; + h = hash_rotl32(h, 13); + h = h * 5 + 0xe6546b64; + } + const int tail = nblocks * 4; + uint32_t k = 0; + switch (p_len & 3) { + case 3: + k ^= uint32_t(p_data[tail + 2]) << 16; + [[fallthrough]]; + case 2: + k ^= uint32_t(p_data[tail + 1]) << 8; + [[fallthrough]]; + case 1: + k ^= uint32_t(p_data[tail + 0]); + k *= c1; + k = hash_rotl32(k, 15); + k *= c2; + h ^= k; + break; + default: + break; + } + h ^= uint32_t(p_len); + return hash_fmix32(h); +} + +static String _h128_hex(const Vector &p_bytes) { + static const uint32_t seeds[4] = { 0x00000000, 0x9E3779B9, 0x85EBCA6B, 0xC2B2AE35 }; + uint8_t out[16]; + for (int k = 0; k < 4; k++) { + const uint32_t h = _murmur3_32_le(p_bytes.ptr(), p_bytes.size(), seeds[k]); + out[k * 4 + 0] = h & 0xFF; + out[k * 4 + 1] = (h >> 8) & 0xFF; + out[k * 4 + 2] = (h >> 16) & 0xFF; + out[k * 4 + 3] = (h >> 24) & 0xFF; + } + return String::hex_encode_buffer(out, 16); +} + +Vector StructInfo::get_layout_descriptor() const { + Vector buf; + buf.push_back(1); + _append_u32_le(buf, (uint32_t)fields.size()); + for (const Field &f : fields) { + _append_lp(buf, String(f.name)); + _append_lp(buf, String(type_to_token(f.type))); + buf.push_back(f.is_typed ? 1 : 0); + _append_lp(buf, String(f.class_name)); + _append_lp(buf, String(f.struct_type_id)); + } + return buf; +} + +String StructInfo::get_layout_fingerprint() const { + return _h128_hex(get_layout_descriptor()); +} + +String StructInfo::get_schema_fingerprint() const { + Vector buf; + _append_lp(buf, String(logical_type_id)); + buf.append_array(get_layout_descriptor()); + return _h128_hex(buf); +} + +bool StructInfo::_field_metadata_ok(const Field &p_field, const Variant &p_value) { + if (p_field.type == Variant::OBJECT && p_field.class_name != StringName()) { + Object *obj = p_value.get_validated_object(); + if (obj == nullptr) { + return true; + } + const StringName &obj_class = obj->get_class_name(); + return obj_class == p_field.class_name || ClassDB::is_parent_class(obj_class, p_field.class_name); + } + if (p_field.type == Variant::STRUCT && p_field.struct_type_id != StringName()) { + const Struct *s = VariantInternal::get_struct(&p_value); + return s->is_null() || s->get_type_id() == p_field.struct_type_id; + } + return true; +} diff --git a/core/variant/struct_info.h b/core/variant/struct_info.h new file mode 100644 index 00000000000..9888f2acdc7 --- /dev/null +++ b/core/variant/struct_info.h @@ -0,0 +1,295 @@ +/**************************************************************************/ +/* struct_info.h */ +/**************************************************************************/ +/* This file is part of: */ +/* REDOT ENGINE */ +/* https://redotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2024-present Redot Engine contributors */ +/* (see REDOT_AUTHORS.md) */ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/error/error_macros.h" +#include "core/object/ref_counted.h" +#include "core/string/string_name.h" +#include "core/templates/hash_map.h" +#include "core/templates/hashfuncs.h" +#include "core/templates/vector.h" +#include "core/variant/variant.h" + +class StructInfoBuilder; + +class StructInfo : public RefCounted { + GDCLASS(StructInfo, RefCounted); + friend class StructInfoBuilder; + +public: + struct Field { + StringName name; + Variant::Type type = Variant::NIL; + bool is_typed = false; + StringName class_name; + StringName struct_type_id; + Variant default_value; + }; + +private: + StringName logical_type_id; + Vector fields; + HashMap index_by_name; + uint64_t layout_hash = 0; + bool frozen = false; + + void set_logical_type_id(const StringName &p_id) { + ERR_FAIL_COND_MSG(frozen, "Cannot modify a frozen StructInfo."); + logical_type_id = p_id; + } + + int add_field(const Field &p_field) { + ERR_FAIL_COND_V_MSG(frozen, -1, "Cannot modify a frozen StructInfo."); + ERR_FAIL_COND_V_MSG(index_by_name.has(p_field.name), -1, + vformat(R"(Duplicate struct field "%s".)", p_field.name)); + Field stored = p_field; + stored.default_value = p_field.default_value.duplicate(true); + const int idx = fields.size(); + fields.push_back(stored); + index_by_name[stored.name] = idx; + return idx; + } + + Error freeze() { + ERR_FAIL_COND_V_MSG(frozen, ERR_ALREADY_EXISTS, "StructInfo is already frozen."); + ERR_FAIL_COND_V_MSG(logical_type_id == StringName(), ERR_INVALID_DATA, + "StructInfo requires a non-empty logical type id."); + for (const Field &f : fields) { + ERR_FAIL_COND_V_MSG(f.name == StringName(), ERR_INVALID_DATA, + "StructInfo has a field with an empty name."); + if (f.is_typed) { + ERR_FAIL_COND_V_MSG(f.type == Variant::NIL, ERR_INVALID_DATA, + vformat(R"(Typed struct field "%s" requires a concrete type.)", f.name)); + } else { + ERR_FAIL_COND_V_MSG(f.type != Variant::NIL || f.class_name != StringName() || f.struct_type_id != StringName(), + ERR_INVALID_DATA, vformat(R"(Untyped struct field "%s" carries typed metadata.)", f.name)); + } + ERR_FAIL_COND_V_MSG(!_value_matches_field(f, f.default_value), ERR_INVALID_DATA, + vformat(R"(Struct field "%s" default value is incompatible with its declared type.)", f.name)); + } + for (int i = 0; i < fields.size(); i++) { + Field &f = fields.write[i]; + f.default_value = _coerce_value(f, f.default_value); + if (!(f.is_typed && f.type == Variant::OBJECT)) { + f.class_name = StringName(); + } + if (!(f.is_typed && f.type == Variant::STRUCT)) { + f.struct_type_id = StringName(); + } + } + uint64_t h = hash_murmur3_one_64(fields.size()); + for (const Field &f : fields) { + h = hash_murmur3_one_64(f.name.hash(), h); + h = hash_murmur3_one_64((uint64_t)f.type, h); + h = hash_murmur3_one_64(f.is_typed ? 1 : 0, h); + h = hash_murmur3_one_64(f.class_name.hash(), h); + h = hash_murmur3_one_64(f.struct_type_id.hash(), h); + } + layout_hash = h; + frozen = true; + return OK; + } + static bool _field_metadata_ok(const Field &p_field, const Variant &p_value); + static bool _value_matches_field(const Field &p_field, const Variant &p_value) { + if (!p_field.is_typed) { + return true; + } + const Variant::Type vt = p_value.get_type(); + if (vt == Variant::NIL) { + return true; + } + if (vt == p_field.type) { + return _field_metadata_ok(p_field, p_value); + } + if ((p_field.type == Variant::INT || p_field.type == Variant::FLOAT) && + (vt == Variant::INT || vt == Variant::FLOAT)) { + return true; + } + if ((p_field.type == Variant::STRING || p_field.type == Variant::STRING_NAME) && + (vt == Variant::STRING || vt == Variant::STRING_NAME)) { + return true; + } + return false; + } + static Variant _coerce_value(const Field &p_field, const Variant &p_value) { + const Variant::Type vt = p_value.get_type(); + if (!p_field.is_typed || vt == p_field.type || vt == Variant::NIL) { + return p_value; + } + switch (p_field.type) { + case Variant::INT: + return p_value.operator int64_t(); + case Variant::FLOAT: + return p_value.operator double(); + case Variant::STRING: + return p_value.operator String(); + case Variant::STRING_NAME: + return p_value.operator StringName(); + default: + return p_value; + } + } + +protected: + static void _bind_methods() {} + +public: + static constexpr uint32_t SERIALIZATION_VERSION = 1; + + bool is_frozen() const noexcept { return frozen; } + StringName get_logical_type_id() const { return logical_type_id; } + uint64_t get_layout_hash() const noexcept { return layout_hash; } + + int get_field_count() const noexcept { return fields.size(); } + + StringName get_field_name(int p_index) const { + ERR_FAIL_INDEX_V(p_index, fields.size(), StringName()); + return fields[p_index].name; + } + Variant::Type get_field_type(int p_index) const { + ERR_FAIL_INDEX_V(p_index, fields.size(), Variant::NIL); + return fields[p_index].type; + } + bool is_field_typed(int p_index) const { + ERR_FAIL_INDEX_V(p_index, fields.size(), false); + return fields[p_index].is_typed; + } + StringName get_field_class_name(int p_index) const { + ERR_FAIL_INDEX_V(p_index, fields.size(), StringName()); + return fields[p_index].class_name; + } + StringName get_field_struct_type_id(int p_index) const { + ERR_FAIL_INDEX_V(p_index, fields.size(), StringName()); + return fields[p_index].struct_type_id; + } + + int index_of(const StringName &p_name) const { + const int *idx = index_by_name.getptr(p_name); + return idx ? *idx : -1; + } + bool has_field(const StringName &p_name) const { return index_by_name.has(p_name); } + + Variant instantiate_default(int p_index) const { + ERR_FAIL_COND_V_MSG(!frozen, Variant(), "Cannot instantiate a default from an unfinished schema."); + ERR_FAIL_INDEX_V(p_index, fields.size(), Variant()); + return fields[p_index].default_value.duplicate(true); + } + + const Variant &_get_field_default_raw(int p_index) const { + CRASH_BAD_INDEX(p_index, fields.size()); + return fields[p_index].default_value; + } + + bool is_value_compatible(int p_index, const Variant &p_value) const { + ERR_FAIL_INDEX_V(p_index, fields.size(), false); + return _value_matches_field(fields[p_index], p_value); + } + + bool normalize_value(int p_index, const Variant &p_value, Variant &r_normalized) const { + if (p_index < 0 || p_index >= fields.size()) { + return false; + } + if (!_value_matches_field(fields[p_index], p_value)) { + return false; + } + r_normalized = _coerce_value(fields[p_index], p_value); + return true; + } + + Vector get_layout_descriptor() const; + String get_layout_fingerprint() const; + String get_schema_fingerprint() const; + static const char *type_to_token(Variant::Type p_type); + static Variant::Type type_from_token(const String &p_token); + + bool is_same_layout_as(const StructInfo &p_other) const { + if (!frozen || !p_other.frozen) { + return false; + } + if (logical_type_id != p_other.logical_type_id || + layout_hash != p_other.layout_hash || + fields.size() != p_other.fields.size()) { + return false; // Fast reject. + } + for (int i = 0; i < fields.size(); i++) { + const Field &a = fields[i]; + const Field &b = p_other.fields[i]; + if (a.name != b.name || a.type != b.type || a.is_typed != b.is_typed || + a.class_name != b.class_name || a.struct_type_id != b.struct_type_id) { + return false; + } + } + return true; + } +}; + +class StructInfoBuilder { + Ref info; + +public: + StructInfoBuilder() { info.instantiate(); } + + StructInfoBuilder(const StructInfoBuilder &) = delete; + StructInfoBuilder &operator=(const StructInfoBuilder &) = delete; + + StructInfoBuilder(StructInfoBuilder &&p_other) { + info = p_other.info; + p_other.info = Ref(); + } + StructInfoBuilder &operator=(StructInfoBuilder &&p_other) { + if (this != &p_other) { + info = p_other.info; + p_other.info = Ref(); + } + return *this; + } + + void set_logical_type_id(const StringName &p_id) { + ERR_FAIL_COND(info.is_null()); + info->set_logical_type_id(p_id); + } + int add_field(const StructInfo::Field &p_field) { + ERR_FAIL_COND_V(info.is_null(), -1); + return info->add_field(p_field); + } + + Ref build() { + ERR_FAIL_COND_V_MSG(info.is_null(), Ref(), + "StructInfoBuilder has already been consumed."); + ERR_FAIL_COND_V_MSG(info->freeze() != OK, Ref(), + "Failed to finalize StructInfo."); + Ref result = info; + info = Ref(); + return result; + } +}; diff --git a/core/variant/type_info.h b/core/variant/type_info.h index 2dd3eb2d11e..138fbe0228e 100644 --- a/core/variant/type_info.h +++ b/core/variant/type_info.h @@ -130,6 +130,7 @@ MAKE_TYPE_INFO(NodePath, Variant::NODE_PATH) MAKE_TYPE_INFO(RID, Variant::RID) MAKE_TYPE_INFO(Callable, Variant::CALLABLE) MAKE_TYPE_INFO(Signal, Variant::SIGNAL) +MAKE_TYPE_INFO(Struct, Variant::STRUCT) MAKE_TYPE_INFO(Dictionary, Variant::DICTIONARY) MAKE_TYPE_INFO(Array, Variant::ARRAY) MAKE_TYPE_INFO(PackedByteArray, Variant::PACKED_BYTE_ARRAY) diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp index 68ff96cca0a..1398cec7b56 100644 --- a/core/variant/variant.cpp +++ b/core/variant/variant.cpp @@ -196,6 +196,7 @@ static const std::array TYPE_STRING_TABLE = "PackedVector3Array", "PackedColorArray", "PackedVector4Array", + "Struct", }; /* @@ -257,6 +258,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 + (1ull << Variant::STRUCT), }; /* @@ -318,6 +320,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 + (1ull << Variant::STRUCT), }; /* @@ -364,6 +367,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) { @@ -505,6 +509,9 @@ bool Variant::is_zero() const { case SIGNAL: { return reinterpret_cast(_data._mem)->is_null(); } + case STRUCT: { + return reinterpret_cast(_data._mem)->is_null(); + } case STRING_NAME: { return *reinterpret_cast(_data._mem) == StringName(); } @@ -770,6 +777,9 @@ void Variant::reference(const Variant &p_variant) { _data.packed_array = PACKED_ARRAY_CREATE_TBL[p_variant.type - PACKED_BYTE_ARRAY](); } return; + case STRUCT: + memnew_placement(_data._mem, Struct(*reinterpret_cast(p_variant._data._mem))); + return; default: dst = _data._mem; src = (void *)p_variant._data._mem; @@ -865,6 +875,9 @@ void Variant::_clear_internal() { case ARRAY: { DESTRUCTOR_TABLE[type](_data._mem); } break; + case STRUCT: { + reinterpret_cast(_data._mem)->~Struct(); + } break; // Math types. case AABB: @@ -1150,6 +1163,10 @@ String Variant::stringify(int recursion_count) const { const Signal &s = *reinterpret_cast(_data._mem); return String(s); } + case STRUCT: { + const Struct &s = *reinterpret_cast(_data._mem); + return "Struct(" + String(s.get_type_id()) + ")"; + } case RID: { const ::RID &s = *reinterpret_cast(_data._mem); return "RID(" + itos(s.get_id()) + ")"; @@ -1495,6 +1512,14 @@ Variant::operator Callable() const { } } +Variant::operator Struct() const { + if (type == STRUCT) { + return *reinterpret_cast(_data._mem); + } else { + return Struct(); + } +} + Variant::operator Signal() const { if (type == SIGNAL) { return *reinterpret_cast(_data._mem); @@ -1950,6 +1975,12 @@ Variant::Variant(const Signal &p_callable) : static_assert(sizeof(Signal) <= sizeof(_data._mem)); } +Variant::Variant(const Struct &p_struct) : + type(STRUCT) { + memnew_placement(_data._mem, Struct(p_struct)); + static_assert(sizeof(Struct) <= sizeof(_data._mem)); +} + Variant::Variant(const Dictionary &p_dictionary) : type(DICTIONARY) { memnew_placement(_data._mem, Dictionary(p_dictionary)); @@ -2171,6 +2202,9 @@ void Variant::operator=(const Variant &p_variant) { case SIGNAL: { *reinterpret_cast(_data._mem) = *reinterpret_cast(p_variant._data._mem); } break; + case STRUCT: { + *reinterpret_cast(_data._mem) = *reinterpret_cast(p_variant._data._mem); + } break; case STRING_NAME: { *reinterpret_cast(_data._mem) = *reinterpret_cast(p_variant._data._mem); @@ -2388,6 +2422,9 @@ uint32_t Variant::recursive_hash(int recursion_count) const { return reinterpret_cast(_data._mem)->recursive_hash(recursion_count); } break; + case STRUCT: { + return reinterpret_cast(_data._mem)->recursive_hash(recursion_count); + } break; case CALLABLE: { return reinterpret_cast(_data._mem)->hash(); diff --git a/core/variant/variant.h b/core/variant/variant.h index 66eab56eb63..6b576c418a9 100644 --- a/core/variant/variant.h +++ b/core/variant/variant.h @@ -69,6 +69,7 @@ #include "core/variant/array.h" #include "core/variant/callable.h" #include "core/variant/dictionary.h" +#include "core/variant/struct.h" #include "core/variant/variant_deep_duplicate.h" #include @@ -153,6 +154,7 @@ class Variant { PACKED_COLOR_ARRAY, PACKED_VECTOR4_ARRAY, /// @} + STRUCT, VARIANT_MAX }; @@ -329,6 +331,7 @@ class Variant { (1ull << Variant::OBJECT) | (1ull << Variant::CALLABLE) | (1ull << Variant::SIGNAL) | + (1ull << Variant::STRUCT) | (1ull << Variant::DICTIONARY) | (1ull << Variant::ARRAY) | (1ull << Variant::PACKED_BYTE_ARRAY) | @@ -478,6 +481,7 @@ class Variant { operator Callable() const; operator Signal() const; + operator Struct() const; operator Dictionary() const; operator Array() const; @@ -547,6 +551,7 @@ class Variant { Variant(const RefCounted *p_object, bool p_is_weak_ref = false); Variant(const Callable &p_callable); Variant(const Signal &p_signal); + Variant(const Struct &p_struct); Variant(const Dictionary &p_dictionary); Variant(std::initializer_list p_init); diff --git a/core/variant/variant_construct.cpp b/core/variant/variant_construct.cpp index 939be8e3da4..9e0403fdfb9 100644 --- a/core/variant/variant_construct.cpp +++ b/core/variant/variant_construct.cpp @@ -204,6 +204,9 @@ void Variant::_register_variant_constructors() { add_constructor>(sarray("from")); add_constructor(sarray("object", "signal")); + add_constructor>(sarray()); + add_constructor>(sarray("from")); + add_constructor>(sarray()); add_constructor>(sarray("from")); add_constructor(sarray("base", "key_type", "key_class_name", "key_script", "value_type", "value_class_name", "value_script")); diff --git a/core/variant/variant_construct.h b/core/variant/variant_construct.h index b121b1e190c..ec6cc824423 100644 --- a/core/variant/variant_construct.h +++ b/core/variant/variant_construct.h @@ -92,6 +92,7 @@ struct PtrConstruct { MAKE_PTRCONSTRUCT(Callable); MAKE_PTRCONSTRUCT(Signal); +MAKE_PTRCONSTRUCT(Struct); MAKE_PTRCONSTRUCT(Dictionary); MAKE_PTRCONSTRUCT(Array); MAKE_PTRCONSTRUCT(PackedByteArray); diff --git a/core/variant/variant_destruct.cpp b/core/variant/variant_destruct.cpp index 5a3c4c95346..b187a364b53 100644 --- a/core/variant/variant_destruct.cpp +++ b/core/variant/variant_destruct.cpp @@ -51,6 +51,7 @@ void Variant::_register_variant_destructors() { add_destructor>(); add_destructor>(); add_destructor>(); + add_destructor>(); add_destructor>(); add_destructor>(); add_destructor>(); diff --git a/core/variant/variant_destruct.h b/core/variant/variant_destruct.h index 0919650b09d..25de96fa564 100644 --- a/core/variant/variant_destruct.h +++ b/core/variant/variant_destruct.h @@ -61,6 +61,7 @@ MAKE_PTRDESTRUCT(StringName); MAKE_PTRDESTRUCT(NodePath); MAKE_PTRDESTRUCT(Callable); MAKE_PTRDESTRUCT(Signal); +MAKE_PTRDESTRUCT(Struct); MAKE_PTRDESTRUCT(Dictionary); MAKE_PTRDESTRUCT(Array); MAKE_PTRDESTRUCT(PackedByteArray); diff --git a/core/variant/variant_internal.h b/core/variant/variant_internal.h index dc36fd910ac..116e46b5587 100644 --- a/core/variant/variant_internal.h +++ b/core/variant/variant_internal.h @@ -89,6 +89,9 @@ class VariantInternal { case Variant::SIGNAL: init_signal(v); break; + case Variant::STRUCT: + init_struct(v); + break; case Variant::DICTIONARY: init_dictionary(v); break; @@ -193,6 +196,8 @@ class VariantInternal { _FORCE_INLINE_ static const Callable *get_callable(const Variant *v) { return reinterpret_cast(v->_data._mem); } _FORCE_INLINE_ static Signal *get_signal(Variant *v) { return reinterpret_cast(v->_data._mem); } _FORCE_INLINE_ static const Signal *get_signal(const Variant *v) { return reinterpret_cast(v->_data._mem); } + _FORCE_INLINE_ static Struct *get_struct(Variant *v) { return reinterpret_cast(v->_data._mem); } + _FORCE_INLINE_ static const Struct *get_struct(const Variant *v) { return reinterpret_cast(v->_data._mem); } _FORCE_INLINE_ static Dictionary *get_dictionary(Variant *v) { return reinterpret_cast(v->_data._mem); } _FORCE_INLINE_ static const Dictionary *get_dictionary(const Variant *v) { return reinterpret_cast(v->_data._mem); } _FORCE_INLINE_ static Array *get_array(Variant *v) { return reinterpret_cast(v->_data._mem); } @@ -286,6 +291,10 @@ class VariantInternal { memnew_placement(v->_data._mem, Signal); v->type = Variant::SIGNAL; } + _FORCE_INLINE_ static void init_struct(Variant *v) { + memnew_placement(v->_data._mem, Struct); + v->type = Variant::STRUCT; + } _FORCE_INLINE_ static void init_dictionary(Variant *v) { memnew_placement(v->_data._mem, Dictionary); v->type = Variant::DICTIONARY; @@ -425,6 +434,8 @@ class VariantInternal { return get_callable(v); case Variant::SIGNAL: return get_signal(v); + case Variant::STRUCT: + return get_struct(v); case Variant::DICTIONARY: return get_dictionary(v); case Variant::ARRAY: @@ -511,6 +522,8 @@ class VariantInternal { return get_callable(v); case Variant::SIGNAL: return get_signal(v); + case Variant::STRUCT: + return get_struct(v); case Variant::DICTIONARY: return get_dictionary(v); case Variant::ARRAY: @@ -719,6 +732,12 @@ struct VariantGetInternalPtr { static const Signal *get_ptr(const Variant *v) { return VariantInternal::get_signal(v); } }; +template <> +struct VariantGetInternalPtr { + static Struct *get_ptr(Variant *v) { return VariantInternal::get_struct(v); } + static const Struct *get_ptr(const Variant *v) { return VariantInternal::get_struct(v); } +}; + template <> struct VariantGetInternalPtr { static Dictionary *get_ptr(Variant *v) { return VariantInternal::get_dictionary(v); } @@ -982,6 +1001,12 @@ struct VariantInternalAccessor { static _FORCE_INLINE_ void set(Variant *v, const Signal &p_value) { *VariantInternal::get_signal(v) = p_value; } }; +template <> +struct VariantInternalAccessor { + static _FORCE_INLINE_ const Struct &get(const Variant *v) { return *VariantInternal::get_struct(v); } + static _FORCE_INLINE_ void set(Variant *v, const Struct &p_value) { *VariantInternal::get_struct(v) = p_value; } +}; + template <> struct VariantInternalAccessor { static _FORCE_INLINE_ const Dictionary &get(const Variant *v) { return *VariantInternal::get_dictionary(v); } @@ -1143,6 +1168,11 @@ struct VariantInitializer { static _FORCE_INLINE_ void init(Variant *v) { VariantInternal::init_signal(v); } }; +template <> +struct VariantInitializer { + static _FORCE_INLINE_ void init(Variant *v) { VariantInternal::init_struct(v); } +}; + template <> struct VariantInitializer { static _FORCE_INLINE_ void init(Variant *v) { VariantInternal::init_dictionary(v); } @@ -1349,6 +1379,11 @@ struct VariantDefaultInitializer { static _FORCE_INLINE_ void init(Variant *v) { *VariantInternal::get_signal(v) = Signal(); } }; +template <> +struct VariantDefaultInitializer { + static _FORCE_INLINE_ void init(Variant *v) { *VariantInternal::get_struct(v) = Struct(); } +}; + template <> struct VariantDefaultInitializer { static _FORCE_INLINE_ void init(Variant *v) { *VariantInternal::get_dictionary(v) = Dictionary(); } @@ -1412,13 +1447,13 @@ struct VariantDefaultInitializer { template struct VariantTypeChanger { static _FORCE_INLINE_ void change(Variant *v) { - if (v->get_type() != GetTypeInfo::VARIANT_TYPE || GetTypeInfo::VARIANT_TYPE >= Variant::PACKED_BYTE_ARRAY) { //second condition removed by optimizer + if (v->get_type() != GetTypeInfo::VARIANT_TYPE || (GetTypeInfo::VARIANT_TYPE >= Variant::PACKED_BYTE_ARRAY && GetTypeInfo::VARIANT_TYPE <= Variant::PACKED_VECTOR4_ARRAY)) { //second condition removed by optimizer VariantInternal::clear(v); VariantInitializer::init(v); } } static _FORCE_INLINE_ void change_and_reset(Variant *v) { - if (v->get_type() != GetTypeInfo::VARIANT_TYPE || GetTypeInfo::VARIANT_TYPE >= Variant::PACKED_BYTE_ARRAY) { //second condition removed by optimizer + if (v->get_type() != GetTypeInfo::VARIANT_TYPE || (GetTypeInfo::VARIANT_TYPE >= Variant::PACKED_BYTE_ARRAY && GetTypeInfo::VARIANT_TYPE <= Variant::PACKED_VECTOR4_ARRAY)) { //second condition removed by optimizer VariantInternal::clear(v); VariantInitializer::init(v); } diff --git a/core/variant/variant_op.cpp b/core/variant/variant_op.cpp index 70ab98f3807..795ac7bc139 100644 --- a/core/variant/variant_op.cpp +++ b/core/variant/variant_op.cpp @@ -478,6 +478,7 @@ void Variant::_register_variant_operators() { register_string_modulo_op(Object, Variant::OBJECT); register_string_modulo_op(Callable, Variant::CALLABLE); register_string_modulo_op(Signal, Variant::SIGNAL); + register_string_modulo_op(Struct, Variant::STRUCT); register_string_modulo_op(Dictionary, Variant::DICTIONARY); register_string_modulo_op(Array, Variant::ARRAY); @@ -561,6 +562,7 @@ void Variant::_register_variant_operators() { register_op>(Variant::OP_EQUAL, Variant::CALLABLE, Variant::CALLABLE); register_op>(Variant::OP_EQUAL, Variant::SIGNAL, Variant::SIGNAL); + register_op>(Variant::OP_EQUAL, Variant::STRUCT, Variant::STRUCT); register_op>(Variant::OP_EQUAL, Variant::DICTIONARY, Variant::DICTIONARY); register_op>(Variant::OP_EQUAL, Variant::ARRAY, Variant::ARRAY); register_op>(Variant::OP_EQUAL, Variant::PACKED_BYTE_ARRAY, Variant::PACKED_BYTE_ARRAY); @@ -599,6 +601,7 @@ void Variant::_register_variant_operators() { register_op>(Variant::OP_EQUAL, Variant::RID, Variant::NIL); register_op>(Variant::OP_EQUAL, Variant::CALLABLE, Variant::NIL); register_op>(Variant::OP_EQUAL, Variant::SIGNAL, Variant::NIL); + register_op>(Variant::OP_EQUAL, Variant::STRUCT, Variant::NIL); register_op>(Variant::OP_EQUAL, Variant::DICTIONARY, Variant::NIL); register_op>(Variant::OP_EQUAL, Variant::ARRAY, Variant::NIL); register_op>(Variant::OP_EQUAL, Variant::PACKED_BYTE_ARRAY, Variant::NIL); @@ -637,6 +640,7 @@ void Variant::_register_variant_operators() { register_op>(Variant::OP_EQUAL, Variant::NIL, Variant::RID); register_op>(Variant::OP_EQUAL, Variant::NIL, Variant::CALLABLE); register_op>(Variant::OP_EQUAL, Variant::NIL, Variant::SIGNAL); + register_op>(Variant::OP_EQUAL, Variant::NIL, Variant::STRUCT); register_op>(Variant::OP_EQUAL, Variant::NIL, Variant::DICTIONARY); register_op>(Variant::OP_EQUAL, Variant::NIL, Variant::ARRAY); register_op>(Variant::OP_EQUAL, Variant::NIL, Variant::PACKED_BYTE_ARRAY); @@ -683,6 +687,7 @@ void Variant::_register_variant_operators() { register_op>(Variant::OP_NOT_EQUAL, Variant::CALLABLE, Variant::CALLABLE); register_op>(Variant::OP_NOT_EQUAL, Variant::SIGNAL, Variant::SIGNAL); + register_op>(Variant::OP_NOT_EQUAL, Variant::STRUCT, Variant::STRUCT); register_op>(Variant::OP_NOT_EQUAL, Variant::DICTIONARY, Variant::DICTIONARY); register_op>(Variant::OP_NOT_EQUAL, Variant::ARRAY, Variant::ARRAY); register_op>(Variant::OP_NOT_EQUAL, Variant::PACKED_BYTE_ARRAY, Variant::PACKED_BYTE_ARRAY); @@ -721,6 +726,7 @@ void Variant::_register_variant_operators() { register_op>(Variant::OP_NOT_EQUAL, Variant::RID, Variant::NIL); register_op>(Variant::OP_NOT_EQUAL, Variant::CALLABLE, Variant::NIL); register_op>(Variant::OP_NOT_EQUAL, Variant::SIGNAL, Variant::NIL); + register_op>(Variant::OP_NOT_EQUAL, Variant::STRUCT, Variant::NIL); register_op>(Variant::OP_NOT_EQUAL, Variant::DICTIONARY, Variant::NIL); register_op>(Variant::OP_NOT_EQUAL, Variant::ARRAY, Variant::NIL); register_op>(Variant::OP_NOT_EQUAL, Variant::PACKED_BYTE_ARRAY, Variant::NIL); @@ -759,6 +765,7 @@ void Variant::_register_variant_operators() { register_op>(Variant::OP_NOT_EQUAL, Variant::NIL, Variant::RID); register_op>(Variant::OP_NOT_EQUAL, Variant::NIL, Variant::CALLABLE); register_op>(Variant::OP_NOT_EQUAL, Variant::NIL, Variant::SIGNAL); + register_op>(Variant::OP_NOT_EQUAL, Variant::NIL, Variant::STRUCT); register_op>(Variant::OP_NOT_EQUAL, Variant::NIL, Variant::DICTIONARY); register_op>(Variant::OP_NOT_EQUAL, Variant::NIL, Variant::ARRAY); register_op>(Variant::OP_NOT_EQUAL, Variant::NIL, Variant::PACKED_BYTE_ARRAY); @@ -950,6 +957,7 @@ void Variant::_register_variant_operators() { register_op>(Variant::OP_NOT, Variant::RID, Variant::NIL); register_op>(Variant::OP_NOT, Variant::CALLABLE, Variant::NIL); register_op>(Variant::OP_NOT, Variant::SIGNAL, Variant::NIL); + register_op>(Variant::OP_NOT, Variant::STRUCT, Variant::NIL); register_op>(Variant::OP_NOT, Variant::DICTIONARY, Variant::NIL); register_op>(Variant::OP_NOT, Variant::ARRAY, Variant::NIL); register_op>(Variant::OP_NOT, Variant::PACKED_BYTE_ARRAY, Variant::NIL); @@ -993,6 +1001,7 @@ void Variant::_register_variant_operators() { register_op(Variant::OP_IN, Variant::OBJECT, Variant::DICTIONARY); register_op>(Variant::OP_IN, Variant::CALLABLE, Variant::DICTIONARY); register_op>(Variant::OP_IN, Variant::SIGNAL, Variant::DICTIONARY); + register_op>(Variant::OP_IN, Variant::STRUCT, Variant::DICTIONARY); register_op>(Variant::OP_IN, Variant::DICTIONARY, Variant::DICTIONARY); register_op>(Variant::OP_IN, Variant::ARRAY, Variant::DICTIONARY); @@ -1035,6 +1044,7 @@ void Variant::_register_variant_operators() { register_op(Variant::OP_IN, Variant::OBJECT, Variant::ARRAY); register_op>(Variant::OP_IN, Variant::CALLABLE, Variant::ARRAY); register_op>(Variant::OP_IN, Variant::SIGNAL, Variant::ARRAY); + register_op>(Variant::OP_IN, Variant::STRUCT, Variant::ARRAY); register_op>(Variant::OP_IN, Variant::DICTIONARY, Variant::ARRAY); register_op>(Variant::OP_IN, Variant::ARRAY, Variant::ARRAY); diff --git a/core/variant/variant_parser.cpp b/core/variant/variant_parser.cpp index 10c4692bda6..aeb15360360 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" +#include "core/variant/struct.h" +#include "core/variant/struct_info.h" +#include "core/variant/variant_internal.h" char32_t VariantParser::Stream::get_char() { // is within buffer? @@ -1136,6 +1139,108 @@ Error VariantParser::parse_value(Token &token, Variant &value, Stream *p_stream, at_key = true; } } + } else if (id == "Struct") { + get_token(p_stream, token, line, r_err_str); + if (token.type != TK_PARENTHESIS_OPEN) { + r_err_str = "Expected '('"; + return ERR_PARSE_ERROR; + } + + Variant version_v; + Variant id_v; + Variant fields_v; + Variant values_v; + Variant *slots[4] = { &version_v, &id_v, &fields_v, &values_v }; + for (int s = 0; s < 4; s++) { + get_token(p_stream, token, line, r_err_str); + Error err = parse_value(token, *slots[s], p_stream, line, r_err_str, p_res_parser, p_allow_objects); + if (err) { + return err; + } + get_token(p_stream, token, line, r_err_str); + const bool last = (s == 3); + if (token.type != (last ? TK_PARENTHESIS_CLOSE : TK_COMMA)) { + r_err_str = last ? "Expected ')'" : "Expected ','"; + return ERR_PARSE_ERROR; + } + } + + if (version_v.get_type() != Variant::INT) { + r_err_str = "Struct version must be an integer."; + return ERR_PARSE_ERROR; + } + if (uint32_t(int64_t(version_v)) != StructInfo::SERIALIZATION_VERSION) { + r_err_str = "Unsupported struct schema version."; + return ERR_PARSE_ERROR; + } + + if (id_v.get_type() != Variant::STRING) { + r_err_str = "Struct logical id must be a String."; + return ERR_PARSE_ERROR; + } + if (fields_v.get_type() != Variant::ARRAY) { + r_err_str = "Struct fields must be an Array."; + return ERR_PARSE_ERROR; + } + if (values_v.get_type() != Variant::ARRAY) { + r_err_str = "Struct values must be an Array."; + return ERR_PARSE_ERROR; + } + + const String sid = id_v; + if (sid.is_empty()) { + value = Struct(); + return OK; + } + const Array fields = fields_v; + const Array values = values_v; + StructInfoBuilder b; + b.set_logical_type_id(StringName(sid)); + for (int i = 0; i < fields.size(); i++) { + if (fields[i].get_type() != Variant::DICTIONARY) { + r_err_str = "Struct field descriptor must be a Dictionary."; + return ERR_PARSE_ERROR; + } + const Dictionary fd = fields[i]; + StructInfo::Field f; + f.name = StringName(String(fd.get("name", String()))); + f.type = StructInfo::type_from_token(String(fd.get("type", "nil"))); + if (f.type == Variant::VARIANT_MAX) { + r_err_str = "Unknown struct field type token."; + return ERR_PARSE_ERROR; + } + f.is_typed = bool(fd.get("typed", false)); + if (fd.has("class_name")) { + f.class_name = StringName(String(fd["class_name"])); + } + if (fd.has("struct_type_id")) { + f.struct_type_id = StringName(String(fd["struct_type_id"])); + } + if (!fd.has("default")) { + r_err_str = "Struct field is missing its default value."; + return ERR_PARSE_ERROR; + } + f.default_value = fd["default"]; + b.add_field(f); + } + Ref info = b.build(); + if (info.is_null()) { + r_err_str = "Failed to rebuild StructInfo from text."; + return ERR_PARSE_ERROR; + } + Struct st(info); + if (values.size() != st.get_field_count()) { + r_err_str = "Struct value count does not match its field count."; + return ERR_PARSE_ERROR; + } + for (int i = 0; i < values.size(); i++) { + if (!st.try_set_member(i, values[i])) { + r_err_str = "Struct value incompatible with its field schema."; + return ERR_PARSE_ERROR; + } + } + value = st; + return OK; } else if (id == "Resource" || id == "SubResource" || id == "ExtResource") { if (!p_allow_objects) { r_err_str = R"(Object decoding is prevented because "allow_objects" is false)"; @@ -2416,6 +2521,44 @@ Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_str } } break; + case Variant::STRUCT: { + const Struct &s = *VariantInternal::get_struct(&p_variant); + const Ref info = s.get_info(); + p_store_string_func(p_store_string_ud, "Struct(" + itos(StructInfo::SERIALIZATION_VERSION) + ", "); + if (info.is_null()) { + p_store_string_func(p_store_string_ud, R"("", [], []))"); + break; + } + + write(String(info->get_logical_type_id()), p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, p_recursion_count, p_compat, p_full_objects); + p_store_string_func(p_store_string_ud, ", "); + + Array fields; + for (int i = 0; i < info->get_field_count(); i++) { + Dictionary fd; + fd["name"] = String(info->get_field_name(i)); + fd["type"] = String(StructInfo::type_to_token(info->get_field_type(i))); + fd["typed"] = info->is_field_typed(i); + if (info->get_field_class_name(i) != StringName()) { + fd["class_name"] = String(info->get_field_class_name(i)); + } + if (info->get_field_struct_type_id(i) != StringName()) { + fd["struct_type_id"] = String(info->get_field_struct_type_id(i)); + } + fd["default"] = Struct::_make_serializable(info->_get_field_default_raw(i)); + fields.push_back(fd); + } + write(fields, p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, p_recursion_count, p_compat, p_full_objects); + p_store_string_func(p_store_string_ud, ", "); + + Array values; + for (int i = 0; i < s.get_field_count(); i++) { + values.push_back(s.get_member_serializable(i)); + } + write(values, p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, p_recursion_count, p_compat, p_full_objects); + p_store_string_func(p_store_string_ud, ")"); + } break; + case Variant::ARRAY: { Array array = p_variant; diff --git a/core/variant/variant_setget.cpp b/core/variant/variant_setget.cpp index bcd622694fe..ab5b6664b33 100644 --- a/core/variant/variant_setget.cpp +++ b/core/variant/variant_setget.cpp @@ -266,6 +266,8 @@ void Variant::set_named(const StringName &p_member, const Variant &p_value, bool } else if (type == Variant::DICTIONARY) { Dictionary &dict = *VariantGetInternalPtr::get_ptr(this); r_valid = dict.set(p_member, p_value); + } else if (type == Variant::STRUCT) { + r_valid = VariantGetInternalPtr::get_ptr(this)->set_named(p_member, p_value); } else { r_valid = false; } @@ -301,6 +303,13 @@ Variant Variant::get_named(const StringName &p_member, bool &r_valid) const { return *v; } } break; + case Variant::STRUCT: { + Variant ret; + if (VariantGetInternalPtr::get_ptr(this)->get_named(p_member, ret)) { + r_valid = true; + return ret; + } + } break; default: { if (Variant::has_builtin_method(type, p_member)) { r_valid = true; @@ -1098,6 +1107,63 @@ struct VariantKeyedSetGetObject { } }; +struct VariantKeyedSetGetStruct { + static int _resolve_index(const Struct &s, const Variant &key) { + switch (key.get_type()) { + case Variant::STRING_NAME: + return s.index_of(*VariantGetInternalPtr::get_ptr(&key)); + case Variant::STRING: + return s.index_of(StringName(*VariantGetInternalPtr::get_ptr(&key))); + case Variant::INT: { + const int64_t idx = *VariantGetInternalPtr::get_ptr(&key); + return (idx >= 0 && idx < s.get_field_count()) ? (int)idx : -1; + } + default: + return -1; + } + } + static bool _get(const Struct &s, const Variant &key, Variant &r_value) { + const int idx = _resolve_index(s, key); + if (idx < 0) { + return false; + } + r_value = s.get_member(idx); + return true; + } + static bool _set(Struct &s, const Variant &key, const Variant &value) { + const int idx = _resolve_index(s, key); + return idx >= 0 && s.try_set_member(idx, value); + } + static bool _has(const Struct &s, const Variant &key) { + return _resolve_index(s, key) >= 0; + } + + static void get(const Variant *base, const Variant *key, Variant *value, bool *r_valid) { + *r_valid = _get(*VariantGetInternalPtr::get_ptr(base), *key, *value); + } + static void ptr_get(const void *base, const void *key, void *value) { + const Struct &s = *reinterpret_cast(base); + Variant v; + _get(s, PtrToArg::convert(key), v); + PtrToArg::encode(v, value); + } + static void set(Variant *base, const Variant *key, const Variant *value, bool *r_valid) { + *r_valid = _set(*VariantGetInternalPtr::get_ptr(base), *key, *value); + } + static void ptr_set(void *base, const void *key, const void *value) { + Struct &s = *reinterpret_cast(base); + _set(s, PtrToArg::convert(key), PtrToArg::convert(value)); + } + static bool has(const Variant *base, const Variant *key, bool *r_valid) { + *r_valid = true; + return _has(*VariantGetInternalPtr::get_ptr(base), *key); + } + static uint32_t ptr_has(const void *base, const void *key) { + const Struct &s = *reinterpret_cast(base); + return _has(s, PtrToArg::convert(key)); + } +}; + struct VariantKeyedSetterGetterInfo { Variant::ValidatedKeyedSetter validated_setter = nullptr; Variant::ValidatedKeyedGetter validated_getter = nullptr; @@ -1131,6 +1197,7 @@ 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); + register_keyed_member(Variant::STRUCT); } bool Variant::is_keyed(Variant::Type p_type) { ERR_FAIL_INDEX_V(p_type, VARIANT_MAX, false); @@ -1193,7 +1260,7 @@ void Variant::set(const Variant &p_index, const Variant &p_value, bool *r_valid, if (err_code) { *err_code = VariantSetError::SET_OK; } - if (type == DICTIONARY || type == OBJECT) { + if (type == DICTIONARY || type == OBJECT || type == STRUCT) { bool valid; set_keyed(p_index, p_value, valid); if (r_valid) { @@ -1244,7 +1311,7 @@ Variant Variant::get(const Variant &p_index, bool *r_valid, VariantGetError *err *err_code = VariantGetError::GET_OK; } Variant ret; - if (type == DICTIONARY || type == OBJECT) { + if (type == DICTIONARY || type == OBJECT || type == STRUCT) { bool valid; ret = get_keyed(p_index, valid); if (r_valid) { @@ -2025,6 +2092,8 @@ Variant Variant::recursive_duplicate(bool p_deep, ResourceDeepDuplicateMode p_de return operator Vector().duplicate(); case PACKED_VECTOR4_ARRAY: return operator Vector().duplicate(); + case STRUCT: + return reinterpret_cast(_data._mem)->recursive_duplicate(p_deep, p_deep_subresources_mode, recursion_count); default: return *this; } diff --git a/core/variant/variant_utility.cpp b/core/variant/variant_utility.cpp index f076e606db6..90aab00c38a 100644 --- a/core/variant/variant_utility.cpp +++ b/core/variant/variant_utility.cpp @@ -979,6 +979,8 @@ Variant VariantUtilityFunctions::type_convert(const Variant &p_variant, const Va return p_variant.operator PackedColorArray(); case Variant::Type::PACKED_VECTOR4_ARRAY: return p_variant.operator PackedVector4Array(); + case Variant::Type::STRUCT: + return p_variant.operator Struct(); case Variant::Type::VARIANT_MAX: ERR_PRINT("Invalid type argument to type_convert(), use the TYPE_* constants. Returning the unconverted Variant."); } diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 3d6e28eeabb..267bf9039f0 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. + + Represents the size of the [enum Variant.Type] enum. diff --git a/doc/classes/Struct.xml b/doc/classes/Struct.xml new file mode 100644 index 00000000000..ea229038d4c --- /dev/null +++ b/doc/classes/Struct.xml @@ -0,0 +1,51 @@ + + + + A schema-backed container of named, typed fields with value semantics. + + + [Struct] is a fixed-layout container whose fields are described by a shared schema (name, type, and optional default per field). Unlike [Dictionary], a struct has value semantics: copying a struct copies its field values, and two structs compare equal when they share the same field layout and all field values are equal. + Fields are accessed by name (a [StringName] or [String]) or by integer index through the [code][][/code] operator. Values assigned to a field are validated against the schema and coerced to the field's declared type; incompatible values are rejected. + [b]Note:[/b] The schema itself is created and managed internally; structs are typically obtained from typed APIs or from GDScript [code]struct[/code] declarations rather than constructed field-by-field from script. + + + + + + + + Constructs an empty [Struct] with no schema (a null struct). + + + + + + + Constructs a [Struct] as a copy of [param from], duplicating its schema reference and field values. + + + + + + + + + Returns [code]true[/code] if the two structs are not equal, that is, if they have a different field layout or any field value differs. + + + + + + + Returns [code]true[/code] if the two structs are equal. Structs are equal when they share the same field layout and all corresponding field values compare equal; structs with different layouts are never equal. + + + + + + + Accesses a struct field. The key may be the field name (a [StringName] or [String]) or the integer field index. Assignment is validated against the struct's schema: incompatible values are rejected, and accepted values are coerced to the field's declared type. + + + + diff --git a/editor/file_system/editor_file_system.cpp b/editor/file_system/editor_file_system.cpp index 02337870063..92c4cc29cda 100644 --- a/editor/file_system/editor_file_system.cpp +++ b/editor/file_system/editor_file_system.cpp @@ -2554,6 +2554,18 @@ HashSet EditorFileSystem::get_valid_extensions() const { void EditorFileSystem::_register_global_class_script(const String &p_search_path, const String &p_target_path, const ScriptClassInfoUpdate &p_script_update) { ScriptServer::remove_global_class_by_path(p_search_path); // First remove, just in case it changed + ScriptServer::remove_global_struct_by_path(p_search_path); + + for (int j = 0; j < ScriptServer::get_language_count(); j++) { + if (ScriptServer::get_language(j)->handles_global_class_type(p_script_update.type)) { + List struct_names; + ScriptServer::get_language(j)->get_global_struct_names(p_target_path, &struct_names); + for (const StringName &struct_name : struct_names) { + ScriptServer::add_global_struct(struct_name, ScriptServer::get_language(j)->get_name(), p_target_path); + } + break; + } + } if (p_script_update.name.is_empty()) { return; diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 307cdf638d0..5ed4d85cdc9 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -2822,6 +2822,40 @@ bool GDScriptLanguage::handles_global_class_type(const String &p_type) const { return p_type == "GDScript"; } +void GDScriptLanguage::get_global_struct_names(const String &p_path, List *r_names) const { + Error err; + Ref f = FileAccess::open(p_path, FileAccess::READ, &err); + if (err) { + return; + } + + String source = f->get_as_utf8_string(); + + if (!source.contains("struct")) { + return; + } + + GDScriptParser parser; + parser.parse(source, p_path, false, true); + + const GDScriptParser::ClassNode *c = parser.get_tree(); + if (!c) { + return; + } + + HashSet seen; + for (int i = 0; i < c->members.size(); i++) { + const GDScriptParser::ClassNode::Member &m = c->members[i]; + if (m.type == GDScriptParser::ClassNode::Member::STRUCT && m.m_struct->identifier != nullptr) { + const StringName &name = m.m_struct->identifier->name; + if (!seen.has(name)) { + seen.insert(name); + r_names->push_back(name); + } + } + } +} + String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_base_type, String *r_icon_path, bool *r_is_abstract, bool *r_is_tool) const { Error err; Ref f = FileAccess::open(p_path, FileAccess::READ, &err); diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 4864d110cb2..ffacce76e8c 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -679,6 +679,7 @@ class GDScriptLanguage : public ScriptLanguage { /// @{ virtual bool handles_global_class_type(const String &p_type) const override; virtual String get_global_class_name(const String &p_path, String *r_base_type = nullptr, String *r_icon_path = nullptr, bool *r_is_abstract = nullptr, bool *r_is_tool = nullptr) const override; + virtual void get_global_struct_names(const String &p_path, List *r_names) const override; /// @} void add_orphan_subclass(const String &p_qualified_name, const ObjectID &p_subclass); diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 176afa7651f..4c6e09d3554 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -809,6 +809,20 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type result = make_script_meta_type(ResourceLoader::load(path, "Script")); } } + } else if (ScriptServer::is_global_struct(first) && !GDScript::is_canonically_equal_paths(parser->script_path, ScriptServer::get_global_struct_path(first))) { + String path = ScriptServer::get_global_struct_path(first); + Ref ref = parser->get_depended_parser_for(path); + if (ref.is_null() || ref->raise_status(GDScriptParserRef::INTERFACE_SOLVED) != OK) { + push_error(vformat(R"(Could not parse global struct "%s" from "%s".)", first, path), p_type); + return bad_type; + } + GDScriptParser::ClassNode *ext_head = ref->get_parser()->head; + if (ext_head->has_member(first) && ext_head->get_member(first).type == GDScriptParser::ClassNode::Member::STRUCT) { + result = ext_head->get_member(first).get_datatype(); + } else { + push_error(vformat(R"(Global struct "%s" was not found in "%s".)", first, path), p_type); + return bad_type; + } } else if (ProjectSettings::get_singleton()->has_autoload(first) && ProjectSettings::get_singleton()->get_autoload(first).is_singleton) { const ProjectSettings::AutoloadInfo &autoload = ProjectSettings::get_singleton()->get_autoload(first); String script_path; @@ -876,6 +890,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::CONSTANT: if (member.get_datatype().is_meta_type) { result = member.get_datatype(); @@ -961,6 +979,11 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type } } + if (result.kind == GDScriptParser::DataType::BUILTIN && result.builtin_type == Variant::STRUCT && + result.struct_type != nullptr && result.struct_type->resolve_state == GDScriptParser::StructNode::FAILED) { + return bad_type; + } + p_type->set_datatype(result); return result; } @@ -1259,6 +1282,10 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, resolve_class_inheritance(member.m_class, p_source); } break; + case GDScriptParser::ClassNode::Member::STRUCT: + check_class_member_name_conflict(p_class, member.m_struct->identifier->name, member.m_struct); + resolve_struct(member.m_struct); + break; case GDScriptParser::ClassNode::Member::GROUP: // No-op, but needed to silence warnings. break; @@ -1667,6 +1694,7 @@ void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node, bool p_is_root case GDScriptParser::Node::BREAK: case GDScriptParser::Node::BREAKPOINT: case GDScriptParser::Node::CONTINUE: + case GDScriptParser::Node::STRUCT: case GDScriptParser::Node::ENUM: case GDScriptParser::Node::FUNCTION: case GDScriptParser::Node::PASS: @@ -2214,6 +2242,137 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi p_assignable->set_datatype(type); } +bool GDScriptAnalyzer::struct_field_from_datatype(const GDScriptParser::DataType &p_type, StructInfo::Field &r_field) { + using DT = GDScriptParser::DataType; + if (!p_type.is_hard_type() || p_type.kind == DT::VARIANT) { + r_field.is_typed = false; + r_field.type = Variant::NIL; + return true; + } + r_field.is_typed = true; + switch (p_type.kind) { + case DT::BUILTIN: + r_field.type = p_type.builtin_type; + if (p_type.builtin_type == Variant::STRUCT && p_type.struct_type != nullptr && p_type.struct_type->identifier != nullptr) { + r_field.struct_type_id = p_type.struct_type->identifier->name; + } + return true; + case DT::NATIVE: + r_field.type = Variant::OBJECT; + r_field.class_name = p_type.native_type; + return true; + case DT::SCRIPT: + case DT::CLASS: + r_field.type = Variant::OBJECT; + r_field.class_name = p_type.native_type; + return true; + case DT::ENUM: + r_field.type = Variant::INT; + return true; + default: + return false; + } +} + +void GDScriptAnalyzer::resolve_struct(GDScriptParser::StructNode *p_struct) { + using SN = GDScriptParser::StructNode; + switch (p_struct->resolve_state) { + case SN::RESOLVED: + case SN::FAILED: + return; + case SN::RESOLVING: + return; + case SN::UNRESOLVED: + break; + } + p_struct->resolve_state = SN::RESOLVING; + + { + GDScriptParser::DataType nominal; + nominal.kind = GDScriptParser::DataType::BUILTIN; + nominal.builtin_type = Variant::STRUCT; + nominal.struct_type = p_struct; + nominal.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + nominal.is_meta_type = true; + p_struct->set_datatype(nominal); + } + + if (p_struct->identifier != nullptr) { + const StringName struct_name = p_struct->identifier->name; + if (ScriptServer::is_global_class(struct_name)) { + push_error(vformat(R"(Struct "%s" hides a global script class.)", struct_name), p_struct->identifier); + } else if (ScriptServer::is_global_struct(struct_name) && !GDScript::is_canonically_equal_paths(ScriptServer::get_global_struct_path(struct_name), parser->script_path)) { + push_error(vformat(R"(Struct "%s" hides a global struct declared in "%s".)", struct_name, ScriptServer::get_global_struct_path(struct_name)), p_struct->identifier); + } + } + + StructInfoBuilder builder; + builder.set_logical_type_id(p_struct->identifier != nullptr ? p_struct->identifier->name : StringName()); + + bool ok = true; + for (GDScriptParser::VariableNode *field : p_struct->fields) { + resolve_variable(field, false); + + StructInfo::Field f; + f.name = field->identifier != nullptr ? field->identifier->name : StringName(); + + const GDScriptParser::DataType field_type = field->get_datatype(); + + if (field_type.kind == GDScriptParser::DataType::BUILTIN && field_type.builtin_type == Variant::STRUCT && + field_type.struct_type != nullptr && field_type.struct_type->resolve_state == SN::RESOLVING) { + const StringName self_name = p_struct->identifier != nullptr ? p_struct->identifier->name : StringName(); + const StringName other_name = field_type.struct_type->identifier != nullptr ? field_type.struct_type->identifier->name : StringName(); + if (field_type.struct_type == p_struct) { + push_error(vformat(R"(Struct "%s" cannot contain itself by value.)", self_name), field); + } else { + const StringName a = String(self_name) < String(other_name) ? self_name : other_name; + const StringName b = String(self_name) < String(other_name) ? other_name : self_name; + push_error(vformat(R"(Structs "%s" and "%s" form a cyclic value dependency.)", a, b), field); + } + ok = false; + continue; + } + + if (field->datatype_specifier != nullptr && !field_type.is_hard_type()) { + ok = false; + continue; + } + + if (!struct_field_from_datatype(field_type, f)) { + push_error(vformat(R"(Struct field "%s" has an unsupported type.)", f.name), field); + ok = false; + continue; + } + + if (field->initializer != nullptr && field->initializer->is_constant) { + f.default_value = field->initializer->reduced_value; + } else if (f.is_typed && f.type != Variant::NIL && f.type != Variant::STRUCT) { + Callable::CallError err; + Variant zero; + Variant::construct(f.type, zero, nullptr, 0, err); + f.default_value = zero; + } else { + f.default_value = Variant(); + } + + builder.add_field(f); + } + + if (!ok) { + p_struct->resolve_state = SN::FAILED; + return; + } + + Ref info = builder.build(); + if (info.is_null()) { + push_error(vformat(R"(Could not build struct "%s".)", p_struct->fqcn), p_struct); + p_struct->resolve_state = SN::FAILED; + return; + } + p_struct->struct_info = info; + p_struct->resolve_state = SN::RESOLVED; +} + void GDScriptAnalyzer::resolve_variable(GDScriptParser::VariableNode *p_variable, bool p_is_local) { static constexpr const char *kind = "variable"; resolve_assignable(p_variable, kind); @@ -2681,6 +2840,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: @@ -3621,6 +3781,30 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a } } + if (is_constructor && base_type.kind == GDScriptParser::DataType::BUILTIN && base_type.builtin_type == Variant::STRUCT && base_type.struct_type != nullptr) { + GDScriptParser::StructNode *struct_type = base_type.struct_type; + resolve_struct(struct_type); + const StringName struct_name = struct_type->identifier != nullptr ? struct_type->identifier->name : StringName(); + const int field_count = struct_type->fields.size(); + if (p_call->arguments.size() > field_count) { + push_error(vformat(R"*(Too many arguments for struct "%s" constructor. Expected at most %d but received %d.)*", struct_name, field_count, p_call->arguments.size()), p_call); + } else { + for (int i = 0; i < p_call->arguments.size(); i++) { + const GDScriptParser::DataType field_type = struct_type->fields[i]->get_datatype(); + const GDScriptParser::DataType arg_type = p_call->arguments[i]->get_datatype(); + if (field_type.is_hard_type() && arg_type.is_hard_type() && !is_type_compatible(field_type, arg_type, true, p_call->arguments[i])) { + const StringName field_name = struct_type->fields[i]->identifier != nullptr ? struct_type->fields[i]->identifier->name : StringName(); + push_error(vformat(R"*(Cannot pass a value of type "%s" as struct field "%s" of type "%s".)*", arg_type.to_string(), field_name, field_type.to_string()), p_call->arguments[i]); + } + } + } + GDScriptParser::DataType result = base_type; + result.is_meta_type = false; + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + p_call->set_datatype(result); + return; + } + if (get_function_signature(p_call, is_constructor, base_type, p_call->function_name, return_type, par_types, default_arg_count, method_flags)) { p_call->is_static = method_flags.has_flag(METHOD_FLAG_STATIC); // If the method is implemented in the class hierarchy, the virtual/abstract flag will not be set for that `MethodInfo` and the search stops there. @@ -4238,6 +4422,12 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod return; } + case GDScriptParser::ClassNode::Member::STRUCT: { + p_identifier->set_datatype(member.get_datatype()); + p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_CLASS; + return; + } + default: { // Do nothing } @@ -4551,6 +4741,18 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident return; } + if (ScriptServer::is_global_struct(name) && !GDScript::is_canonically_equal_paths(parser->script_path, ScriptServer::get_global_struct_path(name))) { + const String path = ScriptServer::get_global_struct_path(name); + Ref ref = parser->get_depended_parser_for(path); + if (ref.is_valid() && ref->raise_status(GDScriptParserRef::INTERFACE_SOLVED) == OK) { + GDScriptParser::ClassNode *ext_head = ref->get_parser()->head; + if (ext_head->has_member(name) && ext_head->get_member(name).type == GDScriptParser::ClassNode::Member::STRUCT) { + p_identifier->set_datatype(ext_head->get_member(name).get_datatype()); + return; + } + } + } + // Try singletons. // Do this before globals because this might be a singleton loading another one before it's compiled. if (ProjectSettings::get_singleton()->has_autoload(name)) { @@ -4842,6 +5044,16 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri } else { mark_node_unsafe(p_subscript); } + } else if (base_type.kind == GDScriptParser::DataType::BUILTIN && base_type.builtin_type == Variant::STRUCT && !base_type.is_meta_type && base_type.struct_type != nullptr) { + GDScriptParser::StructNode *struct_type = base_type.struct_type; + resolve_struct(struct_type); + if (struct_type->fields_indices.has(p_subscript->attribute->name)) { + result_type = struct_type->fields[struct_type->fields_indices[p_subscript->attribute->name]]->get_datatype(); + result_type.type_source = base_type.type_source; + result_type.is_constant = false; + p_subscript->attribute->set_datatype(result_type); + valid = true; + } } else { reduce_identifier_from_base(p_subscript->attribute, &base_type); GDScriptParser::DataType attr_type = p_subscript->attribute->get_datatype(); @@ -5002,6 +5214,9 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri } } break; + case Variant::STRUCT: + error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::STRING && index_type.builtin_type != Variant::STRING_NAME; + break; // Here for completeness. case Variant::VARIANT_MAX: break; @@ -5115,6 +5330,10 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri result_type.type_source = GDScriptParser::DataType::UNDETECTED; } break; + case Variant::STRUCT: + result_type.kind = GDScriptParser::DataType::VARIANT; + result_type.type_source = GDScriptParser::DataType::UNDETECTED; + break; // Here for completeness. case Variant::VARIANT_MAX: break; diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h index f5937597277..443950424e8 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -94,6 +94,8 @@ class GDScriptAnalyzer { void resolve_suite(GDScriptParser::SuiteNode *p_suite); void resolve_assignable(GDScriptParser::AssignableNode *p_assignable, const char *p_kind); void resolve_variable(GDScriptParser::VariableNode *p_variable, bool p_is_local); + void resolve_struct(GDScriptParser::StructNode *p_struct); + bool struct_field_from_datatype(const GDScriptParser::DataType &p_type, StructInfo::Field &r_field); void resolve_constant(GDScriptParser::ConstantNode *p_constant, bool p_is_local); void resolve_parameter(GDScriptParser::ParameterNode *p_parameter); void resolve_if(GDScriptParser::IfNode *p_if); diff --git a/modules/gdscript/gdscript_byte_codegen.cpp b/modules/gdscript/gdscript_byte_codegen.cpp index e6ecef1d58b..478b0acb954 100644 --- a/modules/gdscript/gdscript_byte_codegen.cpp +++ b/modules/gdscript/gdscript_byte_codegen.cpp @@ -116,8 +116,9 @@ uint32_t GDScriptByteCodeGenerator::add_temporary(const GDScriptDataType &p_type case Variant::PACKED_VECTOR3_ARRAY: case Variant::PACKED_COLOR_ARRAY: case Variant::PACKED_VECTOR4_ARRAY: + case Variant::STRUCT: case Variant::VARIANT_MAX: - // Arrays, dictionaries, and objects are reference counted, so we don't use the pool for them. + // These types use generic Variant temporary storage rather than the typed pool. temp_type = Variant::NIL; break; } @@ -553,6 +554,7 @@ void GDScriptByteCodeGenerator::write_type_adjust(const Address &p_target, Varia append_opcode(GDScriptFunction::OPCODE_TYPE_ADJUST_PACKED_VECTOR4_ARRAY); break; case Variant::NIL: + case Variant::STRUCT: case Variant::VARIANT_MAX: return; } @@ -851,6 +853,11 @@ void GDScriptByteCodeGenerator::write_get(const Address &p_target, const Address } void GDScriptByteCodeGenerator::write_set_named(const Address &p_target, const StringName &p_name, const Address &p_source) { + if (HAS_BUILTIN_TYPE(p_target) && p_target.type.builtin_type == Variant::STRUCT) { + // Struct fields are reached by name through the validated keyed setter. + write_set(p_target, get_name_constant_address(p_name), p_source); + return; + } if (HAS_BUILTIN_TYPE(p_target) && Variant::get_member_validated_setter(p_target.type.builtin_type, p_name) && IS_BUILTIN_TYPE(p_source, Variant::get_member_type(p_target.type.builtin_type, p_name))) { Variant::ValidatedSetter setter = Variant::get_member_validated_setter(p_target.type.builtin_type, p_name); @@ -870,6 +877,11 @@ void GDScriptByteCodeGenerator::write_set_named(const Address &p_target, const S } void GDScriptByteCodeGenerator::write_get_named(const Address &p_target, const StringName &p_name, const Address &p_source) { + if (HAS_BUILTIN_TYPE(p_source) && p_source.type.builtin_type == Variant::STRUCT) { + // Struct fields are reached by name through the validated keyed getter. + write_get(p_target, get_name_constant_address(p_name), p_source); + return; + } if (HAS_BUILTIN_TYPE(p_source) && Variant::get_member_validated_getter(p_source.type.builtin_type, p_name)) { Variant::ValidatedGetter getter = Variant::get_member_validated_getter(p_source.type.builtin_type, p_name); append_opcode(GDScriptFunction::OPCODE_GET_NAMED_VALIDATED); @@ -1510,6 +1522,12 @@ void GDScriptByteCodeGenerator::write_construct_typed_dictionary(const Address & ct.cleanup(); } +void GDScriptByteCodeGenerator::write_construct_struct(const Address &p_target, const Address &p_template) { + append_opcode(GDScriptFunction::OPCODE_CONSTRUCT_STRUCT); + append(p_target); + append(p_template); +} + void GDScriptByteCodeGenerator::write_await(const Address &p_target, const Address &p_operand) { append_opcode(GDScriptFunction::OPCODE_AWAIT); append(p_operand); diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h index 3d40174143a..5b47da5bcee 100644 --- a/modules/gdscript/gdscript_byte_codegen.h +++ b/modules/gdscript/gdscript_byte_codegen.h @@ -245,6 +245,14 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { return pos; } + Address get_name_constant_address(const StringName &p_name) { + GDScriptDataType type; + type.has_type = true; + type.kind = GDScriptDataType::BUILTIN; + type.builtin_type = Variant::STRING_NAME; + return Address(Address::CONSTANT, get_constant_pos(p_name), type); + } + int get_operation_pos(const Variant::ValidatedOperatorEvaluator p_operation) { if (operator_func_map.has(p_operation)) { return operator_func_map[p_operation]; @@ -544,6 +552,7 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { virtual void write_construct_typed_array(const Address &p_target, const GDScriptDataType &p_element_type, const Vector
&p_arguments) override; virtual void write_construct_dictionary(const Address &p_target, const Vector
&p_arguments) override; virtual void write_construct_typed_dictionary(const Address &p_target, const GDScriptDataType &p_key_type, const GDScriptDataType &p_value_type, const Vector
&p_arguments) override; + virtual void write_construct_struct(const Address &p_target, const Address &p_template) override; virtual void write_await(const Address &p_target, const Address &p_operand) override; virtual void write_if(const Address &p_condition) override; virtual void write_else() override; diff --git a/modules/gdscript/gdscript_codegen.h b/modules/gdscript/gdscript_codegen.h index 8d736c302a6..92697714db2 100644 --- a/modules/gdscript/gdscript_codegen.h +++ b/modules/gdscript/gdscript_codegen.h @@ -150,6 +150,7 @@ class GDScriptCodeGenerator { virtual void write_construct_typed_array(const Address &p_target, const GDScriptDataType &p_element_type, const Vector
&p_arguments) = 0; virtual void write_construct_dictionary(const Address &p_target, const Vector
&p_arguments) = 0; virtual void write_construct_typed_dictionary(const Address &p_target, const GDScriptDataType &p_key_type, const GDScriptDataType &p_value_type, const Vector
&p_arguments) = 0; + virtual void write_construct_struct(const Address &p_target, const Address &p_template) = 0; virtual void write_await(const Address &p_target, const Address &p_operand) = 0; virtual void write_if(const Address &p_condition) = 0; virtual void write_else() = 0; diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index eef9045bd68..93a331ff322 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -45,6 +45,8 @@ #include "core/config/engine.h" #include "core/config/project_settings.h" +#include "core/variant/struct.h" +#include "core/variant/struct_info.h" #include "scene/scene_string_names.h" @@ -628,7 +630,29 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code arguments.push_back(arg); } - if (!call->is_super && call->callee->type == GDScriptParser::Node::IDENTIFIER && GDScriptParser::get_builtin_type(call->function_name) < Variant::VARIANT_MAX) { + const GDScriptParser::StructNode *struct_construct = nullptr; + if (!call->is_super && call->function_name == SNAME("new") && call->callee->type == GDScriptParser::Node::SUBSCRIPT) { + const GDScriptParser::SubscriptNode *sub = static_cast(call->callee); + const GDScriptParser::DataType base_dt = sub->base->get_datatype(); + if (base_dt.is_meta_type && base_dt.kind == GDScriptParser::DataType::BUILTIN && base_dt.builtin_type == Variant::STRUCT && base_dt.struct_type != nullptr) { + if (!base_dt.struct_type->struct_info.is_valid()) { + _set_error("Compiler bug (please report): struct schema was not resolved for constructor.", call); + r_error = ERR_COMPILATION_FAILED; + return GDScriptCodeGenerator::Address(); + } + struct_construct = base_dt.struct_type; + } + } + + if (struct_construct != nullptr) { + if (result.mode != GDScriptCodeGenerator::Address::NIL) { + GDScriptCodeGenerator::Address template_addr = codegen.add_constant(Variant(Struct(struct_construct->struct_info))); + gen->write_construct_struct(result, template_addr); + for (int i = 0; i < arguments.size(); i++) { + gen->write_set(result, codegen.add_constant(i), arguments[i]); + } + } + } else if (!call->is_super && call->callee->type == GDScriptParser::Node::IDENTIFIER && GDScriptParser::get_builtin_type(call->function_name) < Variant::VARIANT_MAX) { gen->write_construct(result, GDScriptParser::get_builtin_type(call->function_name), arguments); } else if (!call->is_super && call->callee->type == GDScriptParser::Node::IDENTIFIER && Variant::has_utility_function(call->function_name)) { // Variant utility function. diff --git a/modules/gdscript/gdscript_disassembler.cpp b/modules/gdscript/gdscript_disassembler.cpp index 5c0e0fb0519..084da4a951d 100644 --- a/modules/gdscript/gdscript_disassembler.cpp +++ b/modules/gdscript/gdscript_disassembler.cpp @@ -683,6 +683,14 @@ void GDScriptFunction::disassemble(const Vector &p_code_lines) const { incr += 9 + argc * 2; } break; + case OPCODE_CONSTRUCT_STRUCT: { + text += "make_struct "; + text += DADDR(1); + text += " = "; + text += DADDR(2); + + incr += 3; + } break; case OPCODE_CALL: case OPCODE_CALL_RETURN: case OPCODE_CALL_ASYNC: { diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 3ca66ef3146..95717673f78 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -1225,6 +1225,12 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, } option = ScriptLanguage::CodeCompletionOption(member.signal->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); break; + case GDScriptParser::ClassNode::Member::STRUCT: + if (p_only_functions) { + continue; + } + option = ScriptLanguage::CodeCompletionOption(member.m_struct->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_CLASS, location); + break; case GDScriptParser::ClassNode::Member::GROUP: break; // No-op, but silences warnings. case GDScriptParser::ClassNode::Member::UNDEFINED: @@ -1458,6 +1464,28 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base return; } + if (base_type.builtin_type == Variant::STRUCT && base_type.struct_type != nullptr) { + if (base_type.is_meta_type) { + if (!p_only_functions) { + ScriptLanguage::CodeCompletionOption option("new", ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION, ScriptLanguage::LOCATION_LOCAL); + if (p_add_braces) { + option.insert_text += "("; + option.display += U"(…)"; + } + r_result.insert(option.display, option); + } + } else if (!p_only_functions) { + for (const GDScriptParser::VariableNode *field : base_type.struct_type->fields) { + if (field->identifier == nullptr) { + continue; + } + ScriptLanguage::CodeCompletionOption option(field->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, ScriptLanguage::LOCATION_LOCAL); + r_result.insert(option.display, option); + } + } + return; + } + Callable::CallError err; Variant tmp; Variant::construct(base_type.builtin_type, tmp, nullptr, 0, err); @@ -2613,6 +2641,9 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext & r_type.type.class_type = member.m_class; r_type.type.is_meta_type = true; return true; + case GDScriptParser::ClassNode::Member::STRUCT: + r_type.type = member.get_datatype(); + return true; case GDScriptParser::ClassNode::Member::GROUP: return false; // No-op, but silences warnings. case GDScriptParser::ClassNode::Member::UNDEFINED: @@ -3966,6 +3997,9 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co const GDScriptParser::ClassNode::Member &member = base_type.class_type->get_member(name); switch (member.type) { + case GDScriptParser::ClassNode::Member::STRUCT: + r_result.type = ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION; + break; case GDScriptParser::ClassNode::Member::UNDEFINED: case GDScriptParser::ClassNode::Member::GROUP: return ERR_BUG; @@ -3997,7 +4031,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co break; } - if (member.type != GDScriptParser::ClassNode::Member::CLASS) { + if (member.type != GDScriptParser::ClassNode::Member::CLASS && member.type != GDScriptParser::ClassNode::Member::STRUCT) { String doc_type_name; String doc_enum_name; GDScriptDocGen::doctype_from_gdtype(GDScriptAnalyzer::type_from_metatype(base_type), doc_type_name, doc_enum_name); diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index 6b6cccff16a..64a4b521b28 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -314,6 +314,7 @@ class GDScriptFunction { OPCODE_CONSTRUCT_TYPED_ARRAY, OPCODE_CONSTRUCT_DICTIONARY, OPCODE_CONSTRUCT_TYPED_DICTIONARY, + OPCODE_CONSTRUCT_STRUCT, OPCODE_CALL, OPCODE_CALL_RETURN, OPCODE_CALL_ASYNC, diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index c33a332620c..cdd8878811f 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -899,6 +899,74 @@ GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) { return n_class; } +GDScriptParser::StructNode *GDScriptParser::parse_struct(bool p_is_static) { + StructNode *n_struct = alloc_node(); + n_struct->outer = current_class; + + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the struct name after "struct".)")) { + n_struct->identifier = parse_identifier(); + String fqcn = current_class != nullptr ? current_class->fqcn : String(); + if (fqcn.is_empty()) { + fqcn = GDScript::canonicalize_path(script_path); + } + n_struct->fqcn = fqcn + "::" + n_struct->identifier->name; + } + + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after struct declaration.)"); + + bool multiline = match(GDScriptTokenizer::Token::NEWLINE); + if (multiline && !consume(GDScriptTokenizer::Token::INDENT, R"(Expected indented block after struct declaration.)")) { + complete_extents(n_struct); + return n_struct; + } + + bool struct_end = false; + while (!struct_end && !is_at_end()) { + switch (current.type) { + case GDScriptTokenizer::Token::VAR: { + advance(); + VariableNode *field = parse_variable(false, false); + if (field != nullptr && field->identifier != nullptr) { + if (n_struct->fields_indices.has(field->identifier->name)) { + push_error(vformat(R"(Struct field "%s" is declared more than once.)", field->identifier->name), field->identifier); + } else { + n_struct->fields_indices[field->identifier->name] = n_struct->fields.size(); + n_struct->fields.push_back(field); + } + } + } break; + case GDScriptTokenizer::Token::DEDENT: + struct_end = true; + break; + case GDScriptTokenizer::Token::NEWLINE: + advance(); + break; + case GDScriptTokenizer::Token::PASS: + advance(); + end_statement(R"("pass")"); + break; + default: + push_error(R"(Only "var" field declarations are allowed inside a struct.)"); + advance(); + break; + } + if (panic_mode) { + synchronize(); + } + if (!multiline) { + break; + } + } + + complete_extents(n_struct); + + if (multiline) { + consume(GDScriptTokenizer::Token::DEDENT, R"(Missing unindent at the end of the struct body.)"); + } + + return n_struct; +} + void GDScriptParser::parse_class_name() { if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the global class name after "class_name".)")) { current_class->identifier = parse_identifier(); @@ -1044,6 +1112,9 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { case GDScriptTokenizer::Token::ENUM: parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum"); break; + case GDScriptTokenizer::Token::STRUCT: + parse_class_member(&GDScriptParser::parse_struct, AnnotationInfo::NONE, "struct"); + break; case GDScriptTokenizer::Token::STATIC: { advance(); next_is_static = true; @@ -4202,6 +4273,7 @@ GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Ty { &GDScriptParser::parse_self, nullptr, PREC_NONE }, // SELF, { nullptr, nullptr, PREC_NONE }, // SIGNAL, { nullptr, nullptr, PREC_NONE }, // STATIC, + { nullptr, nullptr, PREC_NONE }, // STRUCT, { &GDScriptParser::parse_call, nullptr, PREC_NONE }, // SUPER, { nullptr, nullptr, PREC_NONE }, // TRAIT, { nullptr, nullptr, PREC_NONE }, // VAR, @@ -5224,6 +5296,9 @@ String GDScriptParser::DataType::to_string() const { if (builtin_type == Variant::DICTIONARY && has_container_element_types()) { return vformat("Dictionary[%s, %s]", get_container_element_type_or_variant(0).to_string(), get_container_element_type_or_variant(1).to_string()); } + if (builtin_type == Variant::STRUCT && struct_type != nullptr && struct_type->identifier != nullptr) { + return struct_type->identifier->name.operator String(); + } return Variant::get_type_name(builtin_type); case NATIVE: if (is_meta_type) { @@ -5825,6 +5900,8 @@ void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) { break; case ClassNode::Member::ENUM_VALUE: break; // Nothing. Will be printed by enum. + case ClassNode::Member::STRUCT: + break; case ClassNode::Member::GROUP: break; // Nothing. Groups are only used by inspector. case ClassNode::Member::UNDEFINED: diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 50d0757d054..524c7ffb328 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -53,6 +53,7 @@ #include "core/templates/hash_map.h" #include "core/templates/list.h" #include "core/templates/vector.h" +#include "core/variant/struct_info.h" #include "core/variant/variant.h" #ifdef DEBUG_ENABLED @@ -97,6 +98,7 @@ class GDScriptParser { struct ReturnNode; struct SelfNode; struct SignalNode; + struct StructNode; struct SubscriptNode; struct SuiteNode; struct TernaryOpNode; @@ -142,6 +144,7 @@ class GDScriptParser { Ref