Repository: godotengine/godot (language: cpp)

Issue:
Attaching a script to a scene does not use its uid
### Tested versions

v4.4.beta.custom_build [4ce466d7f]

### System information

Linux v4.4.beta.custom_build [4ce466d7f]

### Issue description

When attaching a script to a scene file, the scene file references the script file by its path, not its uid.
If I move the script externally (moving the uid file along with it) my scene is broken when I get back in the editor.

### Steps to reproduce

https://github.com/user-attachments/assets/d253b86e-e9e6-4236-9052-2a6e0be93f51

In the above video, I attach a script to my scene. I then move the script to another directory, in vscode, bringing its uid along with it.

As you can see I use a feature of vscode called "file nesting", which allows me to unclutter my IDE and makes vscode automatically move, rename and delete uids when I change the associated script file. (maybe this should be in the godot documentation, it's a nice workflow)

![Image](https://github.com/user-attachments/assets/57d459d5-48b8-437d-87d1-cd6b70089bf2)

But, since uids are not used in the scene file, the editor does not pick up on the change.
As soon as I go back to the editor, my scene becomes broken as it still references the old path.

### Minimal reproduction project (MRP)

- I downloaded the editor from the latest gh action build (https://github.com/godotengine/godot/actions/runs/12773670530). I picked `linux-editor-mono` for my test
- I added a `_sc_` file inside the editor directory to launch it in self-contained mode to avoid any configuration issue (see [the doc](https://docs.godotengine.org/en/stable/tutorials/io/data_paths.html#self-contained-mode))
- I deleted the `.godot` file from the existing project
- I opened the project with the editor
- Added a new script to the scene
- saved the scene
- used `cat main.tscn` to get the content:

```tscn
[gd_scene load_steps=2 format=3 uid="uid://bl5d2erorek5n"]

[ext_resource type="Script" path="res://main.gd" id="1_ig7tw"]

[node name="Main" type="Node3D"]
script = ExtResource("1_ig7tw")
```

https://github.com/user-attachments/assets/fdb92a0a-f267-4c55-8c74-9fc0a4598bd6

The project I opened just contains a scene file to make the repro a bit shorter. Here is the zip file, but it really is nothing special:

[test-uids.zip](https://github.com/user-attachments/files/18429830/test-uids.zip)


Discussion / hints:
Can't replicate, scripts have UIDs correctly even after moving, even if moving the files with the editor open, this would then be specific to VSCode or something (which I don't use so can't test)
@AThousandShips Really? Did I mess up somewhere?

Help me understand. Here is my tscn file, right after I attached the script:

```tscn
[gd_scene load_steps=2 format=3 uid="uid://bl5d2erorek5n"]

[ext_resource type="Script" path="res://main.gd" id="1_ig7tw"]

[node name="Main" type="Node3D"]
script = ExtResource("1_ig7tw")
```

As far as I know, it is using the script path so there is no way it can pick up moving the script outside of godot, be it vscode, rider, or in the file explorer. Right?
For me it just adds the UID as well, like so:
```
[ext_resource type="Script" uid="uid://khd0w6gqbome" path="res://test.gd" id="1_a202f"]
```
From the start
That's weird.

I have no uid in my tscn file.

Here is what I did:

- I downloaded the editor from the latest gh action build (https://github.com/godotengine/godot/actions/runs/12773670530). I picked `linux-editor-mono` for my test
- I added a `_sc_` file inside the editor directory to launch it in self-contained mode to avoid any configuration issue (see [the doc](https://docs.godotengine.org/en/stable/tutorials/io/data_paths.html#self-contained-mode))
- I deleted the `.godot` file from the existing project
- I opened the project with the editor
- Added a new script to the scene
- saved the scene
- used `cat main.tscn` to get the content:

```tscn
[gd_scene load_steps=2 format=3 uid="uid://bl5d2erorek5n"]

[ext_resource type="Script" path="res://main.gd" id="1_ig7tw"]

[node name="Main" type="Node3D"]
script = ExtResource("1_ig7tw")
```

So... no uid!

Here is a recording of me doing that:

https://github.com/user-attachments/assets/fdb92a0a-f267-4c55-8c74-9fc0a4598bd6
I can reproduce the issue on Linux too with a build from the latest `master` branch (4ce466d7fa79e351d4295d5bb47e3266089c3a59).

@geowarin's steps to reproduce are clear but just to be extra explicit, here's what I did:
- Create a new project
- Add Node2D root, save scene
- Git init and stage everything at that point, to see the diff on next step
- Add script to that Node2D, save scene
- Check diff:
```diff
diff --git a/node_2d.tscn b/node_2d.tscn
index 1a4db51..154c0ed 100644
--- a/node_2d.tscn
+++ b/node_2d.tscn
@@ -1,3 +1,6 @@
-[gd_scene format=3 uid="uid://dq6l04usaey8m"]
+[gd_scene load_steps=2 format=3 uid="uid://dq6l04usaey8m"]
+
+[ext_resource type="Script" path="res://node_2d.gd" id="1_wtcfe"]
 
 [node name="Node2D" type="Node2D"]
+script = ExtResource("1_wtcfe")
```

No UID is written for the `ext_resource`.

Closing the editor, committing the changes, and then reopening the editor and saving the scene, then fixes the issue:
```diff
diff --git a/node_2d.tscn b/node_2d.tscn
index 154c0ed..25d019e 100644
--- a/node_2d.tscn
+++ b/node_2d.tscn
@@ -1,6 +1,6 @@
 [gd_scene load_steps=2 format=3 uid="uid://dq6l04usaey8m"]
 
-[ext_resource type="Script" path="res://node_2d.gd" id="1_wtcfe"]
+[ext_resource type="Script" uid="uid://cka75sbnaxuto" path="res://node_2d.gd" id="1_wtcfe"]
 
 [node name="Node2D" type="Node2D"]
 script = ExtResource("1_wtcfe")
```

I don't think it's a regression though, for me this has always been hit and miss and one of my main gripes with UIDs since 4.0. I hoped we had fixed this as part of the push for generalizing UIDs but it seems we're still missing some cases.

I suspect it's an issue of the open scene not being "notified" somehow of the UID of its external resource once it's created. While when it's loaded from disk, it properly parses the `.uid` file and it's part of the scene state I suppose.
The UID for script is probably generated too late or not properly picked up by the editor. As a workaround you can restart the project and the UID should load and save properly.

Fix patch:
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index e5e27a11c613..c6baad905172 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -2348,16 +2348,17 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) {
 				_save_late_updated_files(); //files need to be updated in the re-scan
 			}
 
-			const String old_script_class_icon_path = fs->files[cpos]->script_class_icon_path;
-			const String old_class_name = fs->files[cpos]->script_class_name;
-			fs->files[cpos]->type = type;
-			fs->files[cpos]->resource_script_class = script_class;
-			fs->files[cpos]->uid = uid;
-			fs->files[cpos]->script_class_name = _get_global_script_class(type, file, &fs->files[cpos]->script_class_extends, &fs->files[cpos]->script_class_icon_path);
-			fs->files[cpos]->import_group_file = ResourceLoader::get_import_group_file(file);
-			fs->files[cpos]->modified_time = FileAccess::get_modified_time(file);
-			fs->files[cpos]->deps = _get_dependencies(file);
-			fs->files[cpos]->import_valid = (type == "TextFile" || type == "OtherFile") ? true : ResourceLoader::is_import_valid(file);
+			EditorFileSystemDirectory::FileInfo *fi = fs->files[cpos];
+			const String old_script_class_icon_path = fi->script_class_icon_path;
+			const String old_class_name = fi->script_class_name;
+			fi->type = type;
+			fi->resource_script_class = script_class;
+			fi->uid = uid;
+			fi->script_class_name = _get_global_script_class(type, file, &fi->script_class_extends, &fi->script_class_icon_path);
+			fi->import_group_file = ResourceLoader::get_import_group_file(file);
+			fi->modified_time = FileAccess::get_modified_time(file);
+			fi->deps = _get_dependencies(file);
+			fi->import_valid = type == "TextFile" || type == "OtherFile" || ResourceLoader::is_import_valid(file);
 
 			if (uid != ResourceUID::INVALID_ID) {
 				if (ResourceUID::get_singleton()->has_id(uid)) {
@@ -2367,25 +2368,36 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) {
 				}
 
 				ResourceUID::get_singleton()->update_cache();
+			} else {
+				if (ResourceLoader::exists(file) && !ResourceLoader::has_custom_uid_support(file) && !FileAccess::exists(file + ".uid")) {
+					Ref<FileAccess> f = FileAccess::open(file + ".uid", FileAccess::WRITE);
+					if (f.is_valid()) {
+						const ResourceUID::ID id = ResourceUID::get_singleton()->create_id();
+						ResourceUID::get_singleton()->add_id(id, file);
+						f->store_line(ResourceUID::get_singleton()->id_to_text(id));
+						fi->uid = id;
+					}
+				}
 			}
+
 			// Update preview
 			EditorResourcePreview::get_singleton()->check_for_invalidation(file);
 
-			if (ClassDB::is_parent_class(fs->files[cpos]->type, SNAME("Script"))) {
-				_queue_update_script_class(file, fs->files[cpos]->type, fs->files[cpos]->script_class_name, fs->files[cpos]->script_class_extends, fs->files[cpos]->script_class_icon_path);
+			if (ClassDB::is_parent_class(fi->type, SNAME("Script"))) {
+				_queue_update_script_class(file, fi->type, fi->script_class_name, fi->script_class_extends, fi->script_class_icon_path);
 			}
-			if (fs->files[cpos]->type == SNAME("PackedScene")) {
+			if (fi->type == SNAME("PackedScene")) {
 				_queue_update_scene_groups(file);
 			}
 
-			if (ClassDB::is_parent_class(fs->files[cpos]->type, SNAME("Resource"))) {
-				files_to_update_icon_path.push_back(fs->files[cpos]);
-			} else if (old_script_class_icon_path != fs->files[cpos]->script_class_icon_path) {
+			if (ClassDB::is_parent_class(fi->type, SNAME("Resource"))) {
+				files_to_update_icon_path.push_back(fi);
+			} else if (old_script_class_icon_path != fi->script_class_icon_path) {
 				update_files_icon_cache = true;
 			}
 
 			// Restore another script as the global class name if multiple scripts had the same old class name.
-			if (!old_class_name.is_empty() && fs->files[cpos]->script_class_name != old_class_name && ClassDB::is_parent_class(type, SNAME("Script"))) {
+			if (!old_class_name.is_empty() && fi->script_class_name != old_class_name && ClassDB::is_parent_class(type, SNAME("Script"))) {
 				EditorFileSystemDirectory::FileInfo *old_fi = nullptr;
 				String old_file = _get_file_by_class_name(filesystem, old_class_name, old_fi);
 				if (!old_file.is_empty() && old_fi) {
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 1aad74c8b187..4b7abdf2b072 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -1482,16 +1482,6 @@ void EditorNode::save_resource_as(const Ref<Resource> &p_resource, const String
 	file->popup_file_dialog();
 }
 
-void EditorNode::ensure_uid_file(const String &p_new_resource_path) {
-	if (ResourceLoader::exists(p_new_resource_path) && !ResourceLoader::has_custom_uid_support(p_new_resource_path) && !FileAccess::exists(p_new_resource_path + ".uid")) {
-		Ref<FileAccess> f = FileAccess::open(p_new_resource_path + ".uid", FileAccess::WRITE);
-		if (f.is_valid()) {
-			const ResourceUID::ID id = ResourceUID::get_singleton()->create_id();
-			f->store_line(ResourceUID::get_singleton()->id_to_text(id));
-		}
-	}
-}
-
 void EditorNode::_menu_option(int p_option) {
 	_menu_option_confirm(p_option, false);
 }
@@ -2233,11 +2223,6 @@ void EditorNode::_dialog_action(String p_file) {
 			ERR_FAIL_COND(saving_resource.is_null());
 			save_resource_in_path(saving_resource, p_file);
 
-			if (current_menu_option == RESOURCE_SAVE_AS) {
-				// Create .uid file when making new Resource.
-				ensure_uid_file(p_file);
-			}
-
 			saving_resource = Ref<Resource>();
 			ObjectID current_id = editor_history.get_current();
 			Object *current_obj = current_id.is_valid() ? ObjectDB::get_instance(current_id) : nullptr;
diff --git a/editor/editor_node.h b/editor/editor_node.h
index e9ad3492fef2..b77f6553fe96 100644
--- a/editor/editor_node.h
+++ b/editor/editor_node.h
@@ -746,7 +746,6 @@ class EditorNode : public Node {
 	void save_resource_in_path(const Ref<Resource> &p_resource, const String &p_path);
 	void save_resource(const Ref<Resource> &p_resource);
 	void save_resource_as(const Ref<Resource> &p_resource, const String &p_at_path = String());
-	void ensure_uid_file(const String &p_new_resource_path);
 
 	void show_about() { _menu_option_confirm(HELP_ABOUT, false); }
 
diff --git a/editor/script_create_dialog.cpp b/editor/script_create_dialog.cpp
index ecea72d80d3f..5001e5189de2 100644
--- a/editor/script_create_dialog.cpp
+++ b/editor/script_create_dialog.cpp
@@ -374,7 +374,6 @@ void ScriptCreateDialog::_create_new() {
 			alert->popup_centered();
 			return;
 		}
-		EditorNode::get_singleton()->ensure_uid_file(lpath);
 	}
 
 	emit_signal(SNAME("script_created"), scr);
diff --git a/editor/shader_create_dialog.cpp b/editor/shader_create_dialog.cpp
index da619d54d165..9423a8308a52 100644
--- a/editor/shader_create_dialog.cpp
+++ b/editor/shader_create_dialog.cpp
@@ -241,7 +241,6 @@ void fog() {
 			alert->popup_centered();
 			return;
 		}
-		EditorNode::get_singleton()->ensure_uid_file(lpath);
 
 		emit_signal(SNAME("shader_include_created"), shader_inc);
 	} else {
@@ -260,7 +259,6 @@ void fog() {
 				alert->popup_centered();
 				return;
 			}
-			EditorNode::get_singleton()->ensure_uid_file(lpath);
 		}
 
 		emit_signal(SNAME("shader_created"), shader);


Test patch: