Repository: godotengine/godot (language: cpp)

Issue:
Deserializing custom resource created through inspector results in parser error
### Tested versions

- Reproducible in: 4.4.beta3
- Not reproducible in: 4.3.stable

### System information

Godot v4.4.beta3 - Windows 11 (build 26100) - Multi-window, 3 monitors - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 4070 Laptop GPU (NVIDIA; 32.0.15.6636) - AMD Ryzen 9 7940HS w/ Radeon 780M Graphics (16 threads)

### Issue description

When doing `bytes_to_var_with_objects(var_to_bytes_with_objects(some_custom_resource))` where `some_custom_resource` was specifically created through the editor inspector, and not through something like `MyResource.new()`, you end up with a debugger break saying:

```
Parser Error: Class "MyResource" hides a global script class.
```

The exact same setup seemingly worked in 4.3-stable.

### Steps to reproduce

1. Open the MRP.
2. Run the main scene.
3. Note error.

### Minimal reproduction project (MRP)

[resource-serialization-error.zip](https://github.com/user-attachments/files/18710656/resource-serialization-error.zip)


Discussion / hints:
Can confirm, but happens for me in 4.3.stable and even in 4.2.2.stable, at least in my testing using the MRP
If you recreate that same setup in 4.3 (i.e. don't use the MRP project) the error won't show.

It might be related to the `metadata/_custom_type_script` property added to the `sub_resource` as of 4.4.

I was planning on providing a 4.3-stable MRP instead, but there's some other strange issue happening in the upgrade process (that I need to report as well) which forced me to provide a 4.4 repro instead.
That makes sense when clarified!

With this added information I can pinpoint it as being introduced between dev3 and dev4
I just realized I could have just provided two MRPs in the first place, so here's one for 4.3, with no error: [resource-serialization-error-4.3.zip](https://github.com/user-attachments/files/18711303/resource-serialization-error-4.3.zip)
Will bisect now that I know the span
Bisected to:
* https://github.com/godotengine/godot/pull/91341

CC @bjornmp @KoBeWi 

Change was to close:
* https://github.com/godotengine/godot/issues/37479
* https://github.com/godotengine/godot-proposals/issues/7905

---

Related issue:
* https://github.com/godotengine/godot/issues/100154

Which was supposed to be solved by:
* https://github.com/godotengine/godot/pull/101001
`bytes_to_var_with_objects` will for some reason embed resources instead of referencing them. Custom type resources have hidden `_custom_type_script` metadata, which gets duplicated. The same would happen if you export a variable of type Script.

To fix it there needs to be an exception, similar to #100177.
Though a more comprehensive fix would be changing `_custom_type_script` to String. It would fix all duplication problems.

Fix patch:
diff --git a/editor/editor_data.cpp b/editor/editor_data.cpp
index 8135a9b96644..aec082791ef5 100644
--- a/editor/editor_data.cpp
+++ b/editor/editor_data.cpp
@@ -39,6 +39,7 @@
 #include "editor/multi_node_edit.h"
 #include "editor/plugins/editor_context_menu_plugin.h"
 #include "editor/plugins/editor_plugin.h"
+#include "scene/property_utils.h"
 #include "scene/resources/packed_scene.h"
 
 void EditorSelectionHistory::cleanup_history() {
@@ -536,15 +537,18 @@ Variant EditorData::instantiate_custom_type(const String &p_type, const String &
 			if (get_custom_types()[p_inherits][i].name == p_type) {
 				Ref<Script> script = get_custom_types()[p_inherits][i].script;
 
-				Variant ob = ClassDB::instantiate(p_inherits);
-				ERR_FAIL_COND_V(!ob, Variant());
+				// Store in a variant to initialize the refcount if needed.
+				Variant v = ClassDB::instantiate(p_inherits);
+				ERR_FAIL_COND_V(!v, Variant());
+				Object *ob = v;
+
 				Node *n = Object::cast_to<Node>(ob);
 				if (n) {
 					n->set_name(p_type);
 				}
-				n->set_meta(SceneStringName(_custom_type_script), script);
-				((Object *)ob)->set_script(script);
-				return ob;
+				PropertyUtils::assign_custom_type_script(ob, script);
+				ob->set_script(script);
+				return v;
 			}
 		}
 	}
@@ -988,12 +992,13 @@ Variant EditorData::script_class_instance(const String &p_class) {
 		Ref<Script> script = script_class_load_script(p_class);
 		if (script.is_valid()) {
 			// Store in a variant to initialize the refcount if needed.
-			Variant obj = ClassDB::instantiate(script->get_instance_base_type());
-			if (obj) {
-				Object::cast_to<Object>(obj)->set_meta(SceneStringName(_custom_type_script), script);
-				obj.operator Object *()->set_script(script);
+			Variant v = ClassDB::instantiate(script->get_instance_base_type());
+			if (v) {
+				Object *obj = v;
+				PropertyUtils::assign_custom_type_script(obj, script);
+				obj->set_script(script);
 			}
-			return obj;
+			return v;
 		}
 	}
 	return Variant();
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 38839a8bbc61..a733e1ce4e1d 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -4751,7 +4751,7 @@ Ref<Script> EditorNode::get_object_custom_type_base(const Object *p_object) cons
 
 	const Node *node = Object::cast_to<const Node>(p_object);
 	if (node && node->has_meta(SceneStringName(_custom_type_script))) {
-		return node->get_meta(SceneStringName(_custom_type_script));
+		return PropertyUtils::get_custom_type_script(node);
 	}
 
 	Ref<Script> scr = p_object->get_script();
diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp
index b46cda63c70b..2bc022f1d1cb 100644
--- a/editor/scene_tree_dock.cpp
+++ b/editor/scene_tree_dock.cpp
@@ -2834,7 +2834,7 @@ void SceneTreeDock::_update_script_button() {
 			Ref<Script> cts;
 
 			if (n->has_meta(SceneStringName(_custom_type_script))) {
-				cts = n->get_meta(SceneStringName(_custom_type_script));
+				cts = PropertyUtils::get_custom_type_script(n);
 			}
 
 			if (selection.size() == 1) {
@@ -3114,7 +3114,7 @@ void SceneTreeDock::_replace_node(Node *p_node, Node *p_by_node, bool p_keep_pro
 
 		// If we're dealing with a custom node type, we need to create a default instance of the custom type instead of the native type for property comparison.
 		if (oldnode->has_meta(SceneStringName(_custom_type_script))) {
-			Ref<Script> cts = oldnode->get_meta(SceneStringName(_custom_type_script));
+			Ref<Script> cts = PropertyUtils::get_custom_type_script(oldnode);
 			default_oldnode = Object::cast_to<Node>(get_editor_data()->script_class_instance(cts->get_global_name()));
 			if (default_oldnode) {
 				default_oldnode->set_name(cts->get_global_name());
@@ -3618,7 +3618,7 @@ void SceneTreeDock::_script_dropped(const String &p_file, NodePath p_to) {
 	} else {
 		// Check if dropped script is compatible.
 		if (n->has_meta(SceneStringName(_custom_type_script))) {
-			Ref<Script> ct_scr = n->get_meta(SceneStringName(_custom_type_script));
+			Ref<Script> ct_scr = PropertyUtils::get_custom_type_script(n);
 			if (!scr->inherits_script(ct_scr)) {
 				String custom_type_name = ct_scr->get_global_name();
 
diff --git a/scene/property_utils.cpp b/scene/property_utils.cpp
index 9cae7d2a3a8a..dc9a56a294a9 100644
--- a/scene/property_utils.cpp
+++ b/scene/property_utils.cpp
@@ -92,7 +92,7 @@ Variant PropertyUtils::get_property_default_value(const Object *p_object, const
 	// Handle special case "script" property, where the default value is either null or the custom type script.
 	// Do this only if there's no states stack cache to trace for default values.
 	if (!p_states_stack_cache && p_property == CoreStringName(script) && p_object->has_meta(SceneStringName(_custom_type_script))) {
-		Ref<Script> ct_scr = p_object->get_meta(SceneStringName(_custom_type_script));
+		Ref<Script> ct_scr = get_custom_type_script(p_object);
 		if (r_is_valid) {
 			*r_is_valid = true;
 		}
@@ -282,3 +282,29 @@ Vector<SceneState::PackState> PropertyUtils::get_node_states_stack(const Node *p
 	}
 	return states_stack_ret;
 }
+
+void PropertyUtils::assign_custom_type_script(Object *p_object, const Ref<Script> &p_script) {
+	ERR_FAIL_NULL(p_object);
+	ERR_FAIL_COND(p_script.is_null());
+
+	const String &path = p_script->get_path();
+	ERR_FAIL_COND(!path.is_resource_file());
+
+	ResourceUID::ID script_uid = ResourceLoader::get_resource_uid(path);
+	if (script_uid != ResourceUID::INVALID_ID) {
+		p_object->set_meta(SceneStringName(_custom_type_script), ResourceUID::get_singleton()->id_to_text(script_uid));
+	}
+}
+
+Ref<Script> PropertyUtils::get_custom_type_script(const Object *p_object) {
+	Variant custom_script = p_object->get_meta(SceneStringName(_custom_type_script));
+#ifndef DISABLE_DEPRECATED
+	if (custom_script.get_type() == Variant::OBJECT) {
+		// Convert old script meta.
+		Ref<Script> script_object(custom_script);
+		assign_custom_type_script(const_cast<Object *>(p_object), script_object);
+		return script_object;
+	}
+#endif
+	return ResourceLoader::load(custom_script);
+}
diff --git a/scene/property_utils.h b/scene/property_utils.h
index 0bf040ad267a..0fa532440413 100644
--- a/scene/property_utils.h
+++ b/scene/property_utils.h
@@ -46,6 +46,9 @@ class PropertyUtils {
 	// in the tree, since every owner found while traversing towards the root gets a chance
 	// to override property values.)
 	static Vector<SceneState::PackState> get_node_states_stack(const Node *p_node, const Node *p_owner = nullptr, bool *r_instantiated_by_owner = nullptr);
+
+	static void assign_custom_type_script(Object *p_object, const Ref<Script> &p_script);
+	static Ref<Script> get_custom_type_script(const Object *p_object);
 };
 
 #endif // PROPERTY_UTILS_H


Test patch: