From 6aa9e1b986372d73a3c5d5965447f978588f5659 Mon Sep 17 00:00:00 2001 From: Dubhghlas McLaughlin <103212704+mcdubhghlas@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:25:32 -0500 Subject: [PATCH 1/3] now? --- editor/inspector/editor_properties.cpp | 4 + .../editor_properties_array_dict.cpp | 233 ++++++++++++++++++ .../inspector/editor_properties_array_dict.h | 43 ++++ modules/gdscript/gdscript_analyzer.cpp | 39 ++- 4 files changed, 306 insertions(+), 13 deletions(-) diff --git a/editor/inspector/editor_properties.cpp b/editor/inspector/editor_properties.cpp index ae217012aaf..75b85d1bdb6 100644 --- a/editor/inspector/editor_properties.cpp +++ b/editor/inspector/editor_properties.cpp @@ -4051,6 +4051,10 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_ editor->setup(Variant::ARRAY, p_hint_text); return editor; } break; + case Variant::STRUCT: { + EditorPropertyStruct *editor = memnew(EditorPropertyStruct); + return editor; + } break; case Variant::PACKED_BYTE_ARRAY: { EditorPropertyArray *editor = memnew(EditorPropertyArray); editor->setup(Variant::PACKED_BYTE_ARRAY, p_hint_text); diff --git a/editor/inspector/editor_properties_array_dict.cpp b/editor/inspector/editor_properties_array_dict.cpp index 8f9d69f5f16..8c3e5f3c6aa 100644 --- a/editor/inspector/editor_properties_array_dict.cpp +++ b/editor/inspector/editor_properties_array_dict.cpp @@ -40,6 +40,8 @@ #include "core/input/input.h" #include "core/io/marshalls.h" +#include "core/variant/struct.h" +#include "core/variant/struct_info.h" #include "editor/docks/inspector_dock.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" @@ -1535,6 +1537,237 @@ EditorPropertyDictionary::EditorPropertyDictionary() { value_subtype_hint_string = ""; } +///////////////////// STRUCT /////////////////////////// + +// Synthetic per-field property paths exposed by the adapter object take the form `field/`. +static constexpr char STRUCT_FIELD_PREFIX[] = "field/"; + +// Resolve the type an editor should be built for. Typed fields use their declared type; untyped +// fields fall back to the current value's type (so an untyped field still gets a sensible editor). +static Variant::Type _resolve_struct_field_type(const Ref &p_info, const Struct &p_struct, int p_index) { + if (p_info->is_field_typed(p_index)) { + const Variant::Type declared = p_info->get_field_type(p_index); + if (declared != Variant::NIL) { + return declared; + } + } + return p_struct.get_member(p_index).get_type(); +} + +// Extract `` from a `field/` synthetic path, or an empty name if it isn't one. +static StringName _struct_field_name(const StringName &p_property) { + const String property = p_property; + if (!property.begins_with(STRUCT_FIELD_PREFIX)) { + return StringName(); + } + return property.trim_prefix(STRUCT_FIELD_PREFIX); +} + +bool EditorPropertyStructObject::_set(const StringName &p_name, const Variant &p_value) { + const StringName field = _struct_field_name(p_name); + if (field == StringName() || struct_value.get_type() != Variant::STRUCT) { + return false; + } + + bool valid = false; + struct_value.set_named(field, p_value, valid); + return valid; +} + +bool EditorPropertyStructObject::_get(const StringName &p_name, Variant &r_ret) const { + const StringName field = _struct_field_name(p_name); + if (field == StringName() || struct_value.get_type() != Variant::STRUCT) { + return false; + } + + bool valid = false; + r_ret = struct_value.get_named(field, valid); + return valid; +} + +void EditorPropertyStructObject::set_struct(const Variant &p_struct) { + struct_value = p_struct; +} + +Variant EditorPropertyStructObject::get_struct() const { + return struct_value; +} + +void EditorPropertyStruct::_property_changed(const String &p_property, Variant p_value, const String &p_name, bool p_changing) { + const StringName field = _struct_field_name(p_property); + if (field == StringName()) { + return; + } + + if (p_value.get_type() == Variant::OBJECT && p_value.is_null()) { + p_value = Variant(); // `EditorResourcePicker` resets to `Ref()`. See GH-82716. + } + + // Structs have value semantics: edit an independent copy and write it back whole. The adapter is + // the single source of truth, so update it before emitting to keep sibling fields consistent. + Variant struct_value = object->get_struct(); + bool valid = false; + struct_value.set_named(field, p_value, valid); + if (!valid) { + return; + } + + object->set_struct(struct_value); + emit_changed(get_edited_property(), struct_value, p_name, p_changing); +} + +void EditorPropertyStruct::_object_id_selected(const StringName &p_property, ObjectID p_id) { + emit_signal(SNAME("object_id_selected"), p_property, p_id); +} + +void EditorPropertyStruct::_resource_selected(const String &p_path, Ref p_resource) { + emit_signal(SNAME("resource_selected"), get_edited_property(), p_resource); +} + +void EditorPropertyStruct::_edit_pressed() { + get_edited_object()->editor_set_section_unfold(get_edited_property(), edit->is_pressed()); + update_property(); +} + +void EditorPropertyStruct::_clear_property_editors() { + if (!container) { + return; + } + + set_bottom_editor(nullptr); + memdelete(container); + container = nullptr; + property_vbox = nullptr; + slots.clear(); + built_layout_hash = 0; +} + +void EditorPropertyStruct::_rebuild_property_editors(const Variant &p_value) { + _clear_property_editors(); + + const Struct s = p_value; + const Ref info = s.get_info(); + + container = memnew(PanelContainer); + add_child(container); + set_bottom_editor(container); + + property_vbox = memnew(VBoxContainer); + property_vbox->set_h_size_flags(SIZE_EXPAND_FILL); + container->add_child(property_vbox); + + const int field_count = info.is_valid() ? info->get_field_count() : 0; + for (int i = 0; i < field_count; i++) { + const StringName field_name = info->get_field_name(i); + const Variant::Type field_type = _resolve_struct_field_type(info, s, i); + + PropertyHint hint = PROPERTY_HINT_NONE; + String hint_string; + bool is_resource = false; + if (field_type == Variant::OBJECT) { + // Only Resource fields get a typed picker. A Node reference is not meaningfully editable + // through a value-semantics struct (a NODE_TYPE editor edits a NodePath, not an object), + // so non-resource objects fall back to the generic object editor. + const StringName class_name = info->get_field_class_name(i); + if (class_name != StringName() && ClassDB::is_parent_class(class_name, SNAME("Resource"))) { + hint = PROPERTY_HINT_RESOURCE_TYPE; + hint_string = class_name; + is_resource = true; + } + } + + EditorProperty *prop = EditorInspector::instantiate_property_editor(this, field_type, "", hint, hint_string, PROPERTY_USAGE_NONE); + if (!prop) { + continue; + } + prop->set_object_and_property(object.ptr(), STRUCT_FIELD_PREFIX + String(field_name)); + prop->set_label(String(field_name).capitalize()); + prop->set_selectable(false); + prop->set_use_folding(is_using_folding()); + prop->set_h_size_flags(SIZE_EXPAND_FILL); + prop->set_read_only(is_read_only()); + prop->connect(SNAME("property_changed"), callable_mp(this, &EditorPropertyStruct::_property_changed)); + prop->connect(SNAME("object_id_selected"), callable_mp(this, &EditorPropertyStruct::_object_id_selected)); + if (is_resource) { + prop->connect("resource_selected", callable_mp(this, &EditorPropertyStruct::_resource_selected), CONNECT_DEFERRED); + } + property_vbox->add_child(prop); + + Slot slot; + slot.prop = prop; + slot.field_index = i; + slot.type = field_type; + slots.push_back(slot); + } + + built_layout_hash = info.is_valid() ? info->get_layout_hash() : 0; +} + +void EditorPropertyStruct::update_property() { + Variant value = get_edited_property_value(); + + if (value.get_type() != Variant::STRUCT) { + edit->set_text(TTR("(unset)")); + edit->set_disabled(true); + edit->set_pressed(false); + _clear_property_editors(); + return; + } + edit->set_disabled(false); + + const Struct s = value; + const Ref info = s.get_info(); + edit->set_text(info.is_valid() ? String(info->get_logical_type_id()) : TTR("Struct")); + + const bool unfolded = get_edited_object()->editor_is_section_unfolded(get_edited_property()); + if (edit->is_pressed() != unfolded) { + edit->set_pressed(unfolded); + } + + object->set_struct(value); + + if (!unfolded) { + _clear_property_editors(); + return; + } + + // Rebuild the field editors when the struct layout changes (layout hash covers declared fields, + // types, and object class names), or when an untyped field's runtime type no longer matches the + // editor built for it (the layout hash can't capture that). + const uint64_t layout_hash = info.is_valid() ? info->get_layout_hash() : 0; + bool needs_rebuild = !container || built_layout_hash != layout_hash; + if (!needs_rebuild) { + for (const Slot &slot : slots) { + if (slot.type != _resolve_struct_field_type(info, s, slot.field_index)) { + needs_rebuild = true; + break; + } + } + } + if (needs_rebuild) { + _rebuild_property_editors(value); + } + + for (const Slot &slot : slots) { + slot.prop->update_property(); + } +} + +EditorPropertyStruct::EditorPropertyStruct() { + object.instantiate(); + + edit = memnew(Button); + edit->set_accessibility_name(TTRC("Edit")); + edit->set_h_size_flags(SIZE_EXPAND_FILL); + edit->set_clip_text(true); + edit->connect(SceneStringName(pressed), callable_mp(this, &EditorPropertyStruct::_edit_pressed)); + edit->set_toggle_mode(true); + add_child(edit); + add_focusable(edit); + + has_borders = true; +} + ///////////////////// LOCALIZABLE STRING /////////////////////////// void EditorPropertyLocalizableString::_property_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) { diff --git a/editor/inspector/editor_properties_array_dict.h b/editor/inspector/editor_properties_array_dict.h index 2c21e160acd..6f1819c7bff 100644 --- a/editor/inspector/editor_properties_array_dict.h +++ b/editor/inspector/editor_properties_array_dict.h @@ -280,6 +280,49 @@ class EditorPropertyDictionary : public EditorProperty { EditorPropertyDictionary(); }; +class EditorPropertyStructObject : public RefCounted { + GDCLASS(EditorPropertyStructObject, RefCounted); + + Variant struct_value; + +protected: + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + +public: + void set_struct(const Variant &p_struct); + Variant get_struct() const; +}; + +class EditorPropertyStruct : public EditorProperty { + GDCLASS(EditorPropertyStruct, EditorProperty); + + struct Slot { + EditorProperty *prop = nullptr; + int field_index = -1; // Index into StructInfo; not every field necessarily gets an editor. + Variant::Type type = Variant::NIL; // Resolved type the editor was built for (matters for untyped fields). + }; + + Ref object; + Button *edit = nullptr; + PanelContainer *container = nullptr; + VBoxContainer *property_vbox = nullptr; + uint64_t built_layout_hash = 0; + LocalVector slots; + + void _clear_property_editors(); + void _rebuild_property_editors(const Variant &p_value); + + void _edit_pressed(); + void _property_changed(const String &p_property, Variant p_value, const String &p_name = "", bool p_changing = false); + void _object_id_selected(const StringName &p_property, ObjectID p_id); + void _resource_selected(const String &p_path, Ref p_resource); + +public: + virtual void update_property() override; + EditorPropertyStruct(); +}; + class EditorPropertyLocalizableString : public EditorProperty { GDCLASS(EditorPropertyLocalizableString, EditorProperty); diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index b2f716e2910..f0142975a65 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -50,6 +50,7 @@ #include "core/object/class_db.h" #include "core/object/script_language.h" #include "core/templates/hash_map.h" +#include "core/variant/struct.h" #include "scene/main/node.h" #if defined(TOOLS_ENABLED) && !defined(DISABLE_DEPRECATED) @@ -6588,27 +6589,39 @@ Dictionary GDScriptAnalyzer::make_dictionary_from_element_datatype(const GDScrip Variant GDScriptAnalyzer::make_variable_default_value(GDScriptParser::VariableNode *p_variable) { Variant result = Variant(); + bool is_initializer_value_reduced = false; if (p_variable->initializer) { - bool is_initializer_value_reduced = false; Variant initializer_value = make_expression_reduced_value(p_variable->initializer, is_initializer_value_reduced); if (is_initializer_value_reduced) { result = initializer_value; } - } else { + } + + if (!is_initializer_value_reduced) { GDScriptParser::DataType datatype = p_variable->get_datatype(); if (datatype.is_hard_type() && !datatype.is_nullable) { - if (datatype.kind == GDScriptParser::DataType::BUILTIN && datatype.builtin_type != Variant::OBJECT) { - if (datatype.builtin_type == Variant::ARRAY && datatype.has_container_element_type(0)) { - result = make_array_from_element_datatype(datatype.get_container_element_type(0)); - } else if (datatype.builtin_type == Variant::DICTIONARY && datatype.has_container_element_types()) { - GDScriptParser::DataType key = datatype.get_container_element_type_or_variant(0); - GDScriptParser::DataType value = datatype.get_container_element_type_or_variant(1); - result = make_dictionary_from_element_datatype(key, value); - } else { - VariantInternal::initialize(&result, datatype.builtin_type); + if (datatype.kind == GDScriptParser::DataType::BUILTIN && datatype.builtin_type == Variant::STRUCT && datatype.struct_type != nullptr) { + // A struct-typed export can't be constant-folded from its `T.new()` initializer, so + // materialize the schema default (all fields at their declared defaults) instead. + // Without this the export value is NIL and the Inspector shows nothing to edit. + resolve_struct(datatype.struct_type); + if (datatype.struct_type->struct_info.is_valid()) { + result = Variant(Struct(datatype.struct_type->struct_info)); + } + } else if (!p_variable->initializer) { + if (datatype.kind == GDScriptParser::DataType::BUILTIN && datatype.builtin_type != Variant::OBJECT) { + if (datatype.builtin_type == Variant::ARRAY && datatype.has_container_element_type(0)) { + result = make_array_from_element_datatype(datatype.get_container_element_type(0)); + } else if (datatype.builtin_type == Variant::DICTIONARY && datatype.has_container_element_types()) { + GDScriptParser::DataType key = datatype.get_container_element_type_or_variant(0); + GDScriptParser::DataType value = datatype.get_container_element_type_or_variant(1); + result = make_dictionary_from_element_datatype(key, value); + } else { + VariantInternal::initialize(&result, datatype.builtin_type); + } + } else if (datatype.kind == GDScriptParser::DataType::ENUM) { + result = 0; } - } else if (datatype.kind == GDScriptParser::DataType::ENUM) { - result = 0; } } } From b0d6c127fdceb84ab35e586eeda22120064c37a1 Mon Sep 17 00:00:00 2001 From: Dubhghlas McLaughlin <103212704+mcdubhghlas@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:01:28 -0500 Subject: [PATCH 2/3] Fixes #1424 --- .../editor_properties_array_dict.cpp | 4 +- modules/gdscript/gdscript_analyzer.cpp | 80 ++++++++++++------- modules/gdscript/gdscript_analyzer.h | 1 + 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/editor/inspector/editor_properties_array_dict.cpp b/editor/inspector/editor_properties_array_dict.cpp index 8c3e5f3c6aa..74bb54c6238 100644 --- a/editor/inspector/editor_properties_array_dict.cpp +++ b/editor/inspector/editor_properties_array_dict.cpp @@ -1603,8 +1603,8 @@ void EditorPropertyStruct::_property_changed(const String &p_property, Variant p p_value = Variant(); // `EditorResourcePicker` resets to `Ref()`. See GH-82716. } - // Structs have value semantics: edit an independent copy and write it back whole. The adapter is - // the single source of truth, so update it before emitting to keep sibling fields consistent. + // Structs have value semantics: edit an independent copy and write it back whole. Keep the + // adapter's working copy synchronized before emitting so sibling fields observe the update. Variant struct_value = object->get_struct(); bool valid = false; struct_value.set_named(field, p_value, valid); diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index f0142975a65..75d05c16551 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -6586,43 +6586,65 @@ Dictionary GDScriptAnalyzer::make_dictionary_from_element_datatype(const GDScrip return dictionary; } +// True when `p_initializer` is a no-argument construction `T.new()` of the given struct type. Such a +// call can't be constant-folded, but it is exactly the struct's schema default, so an export can +// still show a meaningful default. Arg'd constructors and other unreducible initializers are left +// out, matching how other types treat an initializer they can't fold (the export value stays unset). +static bool _is_default_struct_constructor(const GDScriptParser::ExpressionNode *p_initializer, const GDScriptParser::DataType &p_struct_type) { + if (p_initializer == nullptr || p_initializer->type != GDScriptParser::Node::CALL) { + return false; + } + const GDScriptParser::CallNode *call = static_cast(p_initializer); + if (!call->arguments.is_empty() || call->function_name != SNAME("new")) { + return false; + } + const GDScriptParser::DataType call_type = call->get_datatype(); + return call_type.is_set() && !call_type.is_meta_type && + call_type.kind == GDScriptParser::DataType::BUILTIN && call_type.builtin_type == Variant::STRUCT && + call_type.struct_type == p_struct_type.struct_type; +} + +Variant GDScriptAnalyzer::make_struct_schema_default(GDScriptParser::StructNode *p_struct) { + resolve_struct(p_struct); + if (p_struct->struct_info.is_valid()) { + return Variant(Struct(p_struct->struct_info)); + } + return Variant(); +} + Variant GDScriptAnalyzer::make_variable_default_value(GDScriptParser::VariableNode *p_variable) { Variant result = Variant(); - bool is_initializer_value_reduced = false; + GDScriptParser::DataType datatype = p_variable->get_datatype(); + const bool is_struct = datatype.is_hard_type() && !datatype.is_nullable && + datatype.kind == GDScriptParser::DataType::BUILTIN && datatype.builtin_type == Variant::STRUCT && + datatype.struct_type != nullptr; + if (p_variable->initializer) { + bool is_initializer_value_reduced = false; Variant initializer_value = make_expression_reduced_value(p_variable->initializer, is_initializer_value_reduced); if (is_initializer_value_reduced) { result = initializer_value; - } - } - - if (!is_initializer_value_reduced) { - GDScriptParser::DataType datatype = p_variable->get_datatype(); - if (datatype.is_hard_type() && !datatype.is_nullable) { - if (datatype.kind == GDScriptParser::DataType::BUILTIN && datatype.builtin_type == Variant::STRUCT && datatype.struct_type != nullptr) { - // A struct-typed export can't be constant-folded from its `T.new()` initializer, so - // materialize the schema default (all fields at their declared defaults) instead. - // Without this the export value is NIL and the Inspector shows nothing to edit. - resolve_struct(datatype.struct_type); - if (datatype.struct_type->struct_info.is_valid()) { - result = Variant(Struct(datatype.struct_type->struct_info)); - } - } else if (!p_variable->initializer) { - if (datatype.kind == GDScriptParser::DataType::BUILTIN && datatype.builtin_type != Variant::OBJECT) { - if (datatype.builtin_type == Variant::ARRAY && datatype.has_container_element_type(0)) { - result = make_array_from_element_datatype(datatype.get_container_element_type(0)); - } else if (datatype.builtin_type == Variant::DICTIONARY && datatype.has_container_element_types()) { - GDScriptParser::DataType key = datatype.get_container_element_type_or_variant(0); - GDScriptParser::DataType value = datatype.get_container_element_type_or_variant(1); - result = make_dictionary_from_element_datatype(key, value); - } else { - VariantInternal::initialize(&result, datatype.builtin_type); - } - } else if (datatype.kind == GDScriptParser::DataType::ENUM) { - result = 0; - } + } else if (is_struct && _is_default_struct_constructor(p_variable->initializer, datatype)) { + // `@export var s: T = T.new()`: not constant-foldable, but equal to the schema default. + result = make_struct_schema_default(datatype.struct_type); + } + } else if (datatype.is_hard_type() && !datatype.is_nullable) { + if (is_struct) { + // No initializer: use the struct's schema default (parallel to `int` -> 0, `Array` -> []). + result = make_struct_schema_default(datatype.struct_type); + } else if (datatype.kind == GDScriptParser::DataType::BUILTIN && datatype.builtin_type != Variant::OBJECT) { + if (datatype.builtin_type == Variant::ARRAY && datatype.has_container_element_type(0)) { + result = make_array_from_element_datatype(datatype.get_container_element_type(0)); + } else if (datatype.builtin_type == Variant::DICTIONARY && datatype.has_container_element_types()) { + GDScriptParser::DataType key = datatype.get_container_element_type_or_variant(0); + GDScriptParser::DataType value = datatype.get_container_element_type_or_variant(1); + result = make_dictionary_from_element_datatype(key, value); + } else { + VariantInternal::initialize(&result, datatype.builtin_type); } + } else if (datatype.kind == GDScriptParser::DataType::ENUM) { + result = 0; } } diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h index b40c3cf2832..c51bd3b663f 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -216,6 +216,7 @@ class GDScriptAnalyzer { Error analyze(); Variant make_variable_default_value(GDScriptParser::VariableNode *p_variable); + Variant make_struct_schema_default(GDScriptParser::StructNode *p_struct); static bool check_type_compatibility(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false, const GDScriptParser::Node *p_source_node = nullptr); static GDScriptParser::DataType type_from_metatype(const GDScriptParser::DataType &p_meta_type); From 6516f7c8d005776d7a9b0fdad465ebad49152234 Mon Sep 17 00:00:00 2001 From: Dubhghlas McLaughlin <103212704+mcdubhghlas@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:26:56 -0500 Subject: [PATCH 3/3] rabbit mad. --- editor/inspector/editor_properties.cpp | 29 +++++++++++++++++-- editor/inspector/editor_properties.h | 5 ++++ .../editor_properties_array_dict.cpp | 11 +++++++ modules/gdscript/gdscript_analyzer.cpp | 5 ++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/editor/inspector/editor_properties.cpp b/editor/inspector/editor_properties.cpp index 75b85d1bdb6..ef753247968 100644 --- a/editor/inspector/editor_properties.cpp +++ b/editor/inspector/editor_properties.cpp @@ -40,6 +40,7 @@ #include "core/config/project_settings.h" #include "core/input/input_map.h" +#include "core/variant/struct.h" #include "editor/docks/inspector_dock.h" #include "editor/docks/scene_tree_dock.h" #include "editor/editor_node.h" @@ -3186,7 +3187,7 @@ static bool _find_recursive_resources(const Variant &v, HashSet &res Array a = v; for (int i = 0; i < a.size(); i++) { Variant v2 = a[i]; - if (v2.get_type() != Variant::ARRAY && v2.get_type() != Variant::DICTIONARY && v2.get_type() != Variant::OBJECT) { + if (v2.get_type() != Variant::ARRAY && v2.get_type() != Variant::DICTIONARY && v2.get_type() != Variant::OBJECT && v2.get_type() != Variant::STRUCT) { continue; } if (_find_recursive_resources(v2, resources_found)) { @@ -3199,18 +3200,31 @@ static bool _find_recursive_resources(const Variant &v, HashSet &res for (const KeyValue &kv : d) { const Variant &k = kv.key; const Variant &v2 = kv.value; - if (k.get_type() == Variant::ARRAY || k.get_type() == Variant::DICTIONARY || k.get_type() == Variant::OBJECT) { + if (k.get_type() == Variant::ARRAY || k.get_type() == Variant::DICTIONARY || k.get_type() == Variant::OBJECT || k.get_type() == Variant::STRUCT) { if (_find_recursive_resources(k, resources_found)) { return true; } } - if (v2.get_type() == Variant::ARRAY || v2.get_type() == Variant::DICTIONARY || v2.get_type() == Variant::OBJECT) { + if (v2.get_type() == Variant::ARRAY || v2.get_type() == Variant::DICTIONARY || v2.get_type() == Variant::OBJECT || v2.get_type() == Variant::STRUCT) { if (_find_recursive_resources(v2, resources_found)) { return true; } } } } break; + case Variant::STRUCT: { + const Struct s = v; + const int count = s.get_field_count(); + for (int i = 0; i < count; i++) { + Variant v2 = s.get_member(i); + if (v2.get_type() != Variant::ARRAY && v2.get_type() != Variant::DICTIONARY && v2.get_type() != Variant::OBJECT && v2.get_type() != Variant::STRUCT) { + continue; + } + if (_find_recursive_resources(v2, resources_found)) { + return true; + } + } + } break; case Variant::OBJECT: { Ref r = v; @@ -3247,6 +3261,15 @@ static bool _find_recursive_resources(const Variant &v, HashSet &res return false; } +bool editor_property_has_recursive_resource(Resource *p_owner, const Variant &p_value) { + if (p_owner == nullptr) { + return false; + } + HashSet resources_found; + resources_found.insert(p_owner); + return _find_recursive_resources(p_value, resources_found); +} + void EditorPropertyResource::_resource_changed(const Ref &p_resource) { Resource *r = Object::cast_to(get_edited_object()); if (r) { diff --git a/editor/inspector/editor_properties.h b/editor/inspector/editor_properties.h index 30a4c9cdfed..9930a075f04 100644 --- a/editor/inspector/editor_properties.h +++ b/editor/inspector/editor_properties.h @@ -50,10 +50,15 @@ class EditorSpinSlider; class EditorVariantTypePopupMenu; class MenuButton; class PropertySelector; +class Resource; class SceneTreeDialog; class TextEdit; class TextureButton; +// Returns true if assigning `p_value` to a property of `p_owner` would create a resource storage +// cycle (a resource that transitively contains itself), traversing arrays, dictionaries and structs. +bool editor_property_has_recursive_resource(Resource *p_owner, const Variant &p_value); + class EditorPropertyNil : public EditorProperty { GDCLASS(EditorPropertyNil, EditorProperty); LineEdit *text = nullptr; diff --git a/editor/inspector/editor_properties_array_dict.cpp b/editor/inspector/editor_properties_array_dict.cpp index 74bb54c6238..b9ecd2ccc6d 100644 --- a/editor/inspector/editor_properties_array_dict.cpp +++ b/editor/inspector/editor_properties_array_dict.cpp @@ -1612,6 +1612,17 @@ void EditorPropertyStruct::_property_changed(const String &p_property, Variant p return; } + // The inner field editor edits this adapter, not the owning Resource, so its own recursion guard + // never runs. Reject a field value that would store the edited Resource inside itself. + if (p_value.get_type() == Variant::OBJECT) { + Resource *edited_resource = Object::cast_to(get_edited_object()); + if (edited_resource && editor_property_has_recursive_resource(edited_resource, struct_value)) { + EditorNode::get_singleton()->show_warning(TTR("Recursion detected, unable to assign resource to property.")); + update_property(); + return; + } + } + object->set_struct(struct_value); emit_changed(get_edited_property(), struct_value, p_name, p_changing); } diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 75d05c16551..44a66f940e9 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -2925,6 +2925,11 @@ void GDScriptAnalyzer::resolve_struct(GDScriptParser::StructNode *p_struct) { if (field->initializer != nullptr && field->initializer->is_constant) { f.default_value = field->initializer->reduced_value; + } else if (!field_type.is_nullable && field_type.kind == GDScriptParser::DataType::BUILTIN && + field_type.builtin_type == Variant::STRUCT && field_type.struct_type != nullptr) { + // A non-nullable nested struct field defaults to its own schema default, so a required + // field is never left null (cyclic value dependencies are already rejected above). + f.default_value = make_struct_schema_default(field_type.struct_type); } else if (!field_type.is_nullable && f.is_typed && f.type != Variant::NIL && f.type != Variant::STRUCT) { Callable::CallError err; Variant zero;