Repository: godotengine/godot (language: cpp)

Issue:
[4.4.dev7] The `Default Environment` setting is reset after the "cold" project loading (`res://` | `uid://` issue)
### Tested versions

- reproducible in 4.4.dev7 (maybe 4.4.x)
- not reproducible: 4.3.stable

### System information

Godot v4.4.dev7 - Windows 11 (build 26100) - Multi-window, 1 monitor - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 3060 Ti (NVIDIA; 32.0.15.6636) - 12th Gen Intel(R) Core(TM) i5-12400 (12 threads)

### Issue description

The default environment setting (`rendering/environment/defaults/default_environment`) is reset after the **"cold"** project loading (deleting the `.godot` folder and loading project for the first time). However, it stays on place if it was saved as `res://...` path, not as `uid://...` value. Seems like `uid` related bug.

### Steps to reproduce

Steps for 4.4.dev7: 
- create new project;
- create an `Environment` asset;
- set it as default in `Project Settings`;
- close the project;
- delete the `.godot` folder;
- open the project.

Not reproduced with 4.3 -> 4.4 migration because its value will still be saved as `res://`, not `uid://`.

### Minimal reproduction project (MRP)

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



Discussion / hints:
Seems like the `Main Scene` (`run/main_scene`) setting has a similar issue: Godot will not open the `Main Scene` after the "cold" loading if it is saved as `uid://` value, but open perfectly fine if it is saved as `res://`. Shoud I create a separate issue for that? Pretty sure it is the same, but in theory can require a fix in several places.
P.S.: The `Main Scene` setting is automatically replaced with `uid` after manually changing it to `res`.
This maybe due to running scan of filesystem after loading environment and main scene, or something like what. Needs investigating
The environment is loaded in SceneTree constructor, which happens before any filesystem scan that could load the UID. And interestingly, the code deletes the settting if file can't be found.
https://github.com/godotengine/godot/blob/9630d4e2fc1d0fdef6f46f24e236548549f31d49/scene/main/scene_tree.cpp#L1970-L1984
After analyzing the problem further, I have identified a few related issues and concerns:

1. Before the first scan, the only source to link a UID to a path is the UID cache, which presents several issues:
   - CI/CD will not function properly without the cache file being included in source control.
   - If a file is moved outside the editor (e.g., due to a Git pull while the editor is closed), the correct paths will not be available when the editor starts.
   - Any attempt to access a UID before the first scan will fail.

2. There is a missing `ResourceUID::ensure_path(` call in the comparison within `main.cpp`:
   The line `game_path != String(GLOBAL_GET("application/run/main_scene"))` should include this call to ensure the UID is converted in the same way as when `game_path` is initialized.

I seriously believe that the only way to ensure the UID cache is up to date is to reload the UID files before the first call to `ResourceUID::path_to_uid`. One possible solution is to make `EditorFileSystem::_scan_new_dir` a static method and invoke it in `main.cpp`. By storing the `ScannedDirectory`, we could reuse it in `EditorFileSystem::_first_scan_filesystem`, thus preventing a double scan. 

However, the main challenge with this approach is that it will be difficult to display a progress bar during the first scan, as the main window is created after the initial call to `ResourceUID::path_to_uid`. Maybe creating a temporaire small loading window could be an idea.
After a bit more investigating, this approach will not work perfectly. The main issue is the possibility to have custom `ResourceLoader` with custom `get_resource_uid` methods from plugins and GDExtensions. I guess it would be difficult to load these plugins sooner in the editor startup. So the best approach should probably be to refactor the code the load the resource after the `EditorFileSystem` first scan when in the Editor. It's just very difficult to find every places the access `ResourceUid` before the first scan.

Fix patch:
diff --git a/core/io/resource_uid.cpp b/core/io/resource_uid.cpp
index 8a8c86087d45..a73adf9d7478 100644
--- a/core/io/resource_uid.cpp
+++ b/core/io/resource_uid.cpp
@@ -154,6 +154,19 @@ String ResourceUID::get_id_path(ID p_id) const {
 	ERR_FAIL_COND_V_MSG(p_id == INVALID_ID, String(), "Invalid UID.");
 	MutexLock l(mutex);
 	const ResourceUID::Cache *cache = unique_ids.getptr(p_id);
+
+#if TOOLS_ENABLED
+	// On startup, the scan_for_uid_on_startup callback should be set and will
+	// execute EditorFileSystem::scan_for_uid, which scans all project files
+	// to reload the UID cache before the first scan.
+	// Note: EditorFileSystem::scan_for_uid sets scan_for_uid_on_startup to nullptr
+	//       once the first scan_for_uid is complete.
+	if (!cache && scan_for_uid_on_startup) {
+		scan_for_uid_on_startup();
+		cache = unique_ids.getptr(p_id);
+	}
+#endif
+
 	ERR_FAIL_COND_V_MSG(!cache, String(), vformat("Unrecognized UID: \"%s\".", id_to_text(p_id)));
 	const CharString &cs = cache->cs;
 	return String::utf8(cs.ptr());
diff --git a/core/io/resource_uid.h b/core/io/resource_uid.h
index 6d96953fcc1d..2912168dcb4b 100644
--- a/core/io/resource_uid.h
+++ b/core/io/resource_uid.h
@@ -37,6 +37,8 @@
 
 class FileAccess;
 
+typedef void (*ResourceUIDScanForUIDOnStartup)();
+
 class ResourceUID : public Object {
 	GDCLASS(ResourceUID, Object)
 public:
@@ -63,6 +65,8 @@ class ResourceUID : public Object {
 	static void _bind_methods();
 
 public:
+	inline static ResourceUIDScanForUIDOnStartup scan_for_uid_on_startup = nullptr;
+
 	String id_to_text(ID p_id) const;
 	ID text_to_id(const String &p_text) const;
 
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index 2d10e4305eaa..4c5663c3b99f 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -48,6 +48,9 @@
 #include "scene/resources/packed_scene.h"
 
 EditorFileSystem *EditorFileSystem::singleton = nullptr;
+int EditorFileSystem::nb_files_total = 0;
+EditorFileSystem::ScannedDirectory *EditorFileSystem::first_scan_root_dir = nullptr;
+
 //the name is the version, to keep compatibility with different versions of Godot
 #define CACHE_FILE_NAME "filesystem_cache10"
 
@@ -237,16 +240,72 @@ EditorFileSystem::ScannedDirectory::~ScannedDirectory() {
 	}
 }
 
-void EditorFileSystem::_first_scan_filesystem() {
-	EditorProgress ep = EditorProgress("first_scan_filesystem", TTR("Project initialization"), 5);
+void EditorFileSystem::_load_first_scan_root_dir() {
 	Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);
 	first_scan_root_dir = memnew(ScannedDirectory);
 	first_scan_root_dir->full_path = "res://";
+
+	nb_files_total = _scan_new_dir(first_scan_root_dir, d);
+}
+
+void EditorFileSystem::scan_for_uid() {
+	// Load file structure into memory.
+	_load_first_scan_root_dir();
+
+	// Load extensions for which an .import should exists.
+	List<String> extensionsl;
+	HashSet<String> import_extensions;
+	ResourceFormatImporter::get_singleton()->get_recognized_extensions(&extensionsl);
+	for (const String &E : extensionsl) {
+		import_extensions.insert(E);
+	}
+
+	// Scan the file system to load uid.
+	_scan_for_uid_directory(first_scan_root_dir, import_extensions);
+
+	// It's done, resetting the callback method to prevent a second scan.
+	ResourceUID::scan_for_uid_on_startup = nullptr;
+}
+
+void EditorFileSystem::_scan_for_uid_directory(const ScannedDirectory *p_scan_dir, const HashSet<String> &p_import_extensions) {
+	for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) {
+		_scan_for_uid_directory(scan_sub_dir, p_import_extensions);
+	}
+
+	for (const String &scan_file : p_scan_dir->files) {
+		const String ext = scan_file.get_extension().to_lower();
+
+		if (ext == "uid" || ext == "import") {
+			continue;
+		}
+
+		const String path = p_scan_dir->full_path.path_join(scan_file);
+		ResourceUID::ID uid = ResourceUID::INVALID_ID;
+		if (p_import_extensions.has(ext)) {
+			if (FileAccess::exists(path + ".import")) {
+				uid = ResourceFormatImporter::get_singleton()->get_resource_uid(path);
+			}
+		} else {
+			uid = ResourceLoader::get_resource_uid(path);
+		}
+
+		if (uid != ResourceUID::INVALID_ID) {
+			if (!ResourceUID::get_singleton()->has_id(uid)) {
+				ResourceUID::get_singleton()->add_id(uid, path);
+			}
+		}
+	}
+}
+
+void EditorFileSystem::_first_scan_filesystem() {
+	EditorProgress ep = EditorProgress("first_scan_filesystem", TTR("Project initialization"), 5);
 	HashSet<String> existing_class_names;
 	HashSet<String> extensions;
 
-	ep.step(TTR("Scanning file structure..."), 0, true);
-	nb_files_total = _scan_new_dir(first_scan_root_dir, d);
+	if (!first_scan_root_dir) {
+		ep.step(TTR("Scanning file structure..."), 0, true);
+		_load_first_scan_root_dir();
+	}
 
 	// Preloading GDExtensions file extensions to prevent looping on all the resource loaders
 	// for each files in _first_scan_process_scripts.
@@ -440,6 +499,7 @@ void EditorFileSystem::_scan_filesystem() {
 		sd = first_scan_root_dir;
 		// Will be updated on scan.
 		ResourceUID::get_singleton()->clear();
+		ResourceUID::scan_for_uid_on_startup = nullptr;
 		processed_files = memnew(HashSet<String>());
 	} else {
 		Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);
diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h
index 5b78b86a3f3f..4f9302e045f8 100644
--- a/editor/editor_file_system.h
+++ b/editor/editor_file_system.h
@@ -182,7 +182,7 @@ class EditorFileSystem : public Node {
 	static void _thread_func(void *_userdata);
 
 	EditorFileSystemDirectory *new_filesystem = nullptr;
-	ScannedDirectory *first_scan_root_dir = nullptr;
+	static ScannedDirectory *first_scan_root_dir;
 
 	bool filesystem_changed_queued = false;
 	bool scanning = false;
@@ -192,13 +192,17 @@ class EditorFileSystem : public Node {
 	float scan_total;
 	String filesystem_settings_version_for_import;
 	bool revalidate_import_files = false;
-	int nb_files_total = 0;
+	static int nb_files_total;
 
 	void _notify_filesystem_changed();
 	void _scan_filesystem();
 	void _first_scan_filesystem();
 	void _first_scan_process_scripts(const ScannedDirectory *p_scan_dir, List<String> &p_gdextension_extensions, HashSet<String> &p_existing_class_names, HashSet<String> &p_extensions);
 
+	static void _scan_for_uid_directory(const ScannedDirectory *p_scan_dir, const HashSet<String> &p_import_extensions);
+
+	static void _load_first_scan_root_dir();
+
 	HashSet<String> late_update_files;
 
 	void _save_late_updated_files();
@@ -255,7 +259,7 @@ class EditorFileSystem : public Node {
 	HashSet<String> valid_extensions;
 	HashSet<String> import_extensions;
 
-	int _scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da);
+	static int _scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da);
 	void _process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress, HashSet<String> *p_processed_files);
 
 	Thread thread_sources;
@@ -412,6 +416,8 @@ class EditorFileSystem : public Node {
 
 	static bool _should_skip_directory(const String &p_path);
 
+	static void scan_for_uid();
+
 	void add_import_format_support_query(Ref<EditorFileSystemImportFormatSupportQuery> p_query);
 	void remove_import_format_support_query(Ref<EditorFileSystemImportFormatSupportQuery> p_query);
 	EditorFileSystem();
diff --git a/main/main.cpp b/main/main.cpp
index c65e6ba8acb4..6cd9c89fa8b7 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -3444,6 +3444,17 @@ Error Main::setup2(bool p_show_boot_logo) {
 	// This loads global classes, so it must happen before custom loaders and savers are registered
 	ScriptServer::init_languages();
 
+#if TOOLS_ENABLED
+
+	// Setting up the callback to execute a scan for UIDs on disk when a UID
+	// does not exist in the UID cache on startup. This prevents invalid UID errors
+	// when opening a project without a UID cache file or with an invalid cache.
+	if (editor) {
+		ResourceUID::scan_for_uid_on_startup = EditorFileSystem::scan_for_uid;
+	}
+
+#endif
+
 	theme_db->initialize_theme();
 	audio_server->load_default_bus_layout();
 
@@ -3496,9 +3507,21 @@ void Main::setup_boot_logo() {
 
 	if (show_logo) { //boot logo!
 		const bool boot_logo_image = GLOBAL_DEF_BASIC("application/boot_splash/show_image", true);
-		const String boot_logo_path = ResourceUID::ensure_path(GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/boot_splash/image", PROPERTY_HINT_FILE, "*.png"), String())).strip_edges();
 		const bool boot_logo_scale = GLOBAL_DEF_BASIC("application/boot_splash/fullsize", true);
 		const bool boot_logo_filter = GLOBAL_DEF_BASIC("application/boot_splash/use_filter", true);
+		String boot_logo_path = GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/boot_splash/image", PROPERTY_HINT_FILE, "*.png"), String());
+
+		// If the UID cache is missing or invalid, it could be 'normal' for the UID to not exist in memory.
+		// It's too soon to scan the project files since the ResourceFormatImporter is not loaded yet,
+		// so to prevent printing errors, we will just skip the custom boot logo this time.
+		if (boot_logo_path.begins_with("uid://")) {
+			const ResourceUID::ID logo_id = ResourceUID::get_singleton()->text_to_id(boot_logo_path);
+			if (ResourceUID::get_singleton()->has_id(logo_id)) {
+				boot_logo_path = ResourceUID::get_singleton()->get_id_path(logo_id).strip_edges();
+			} else {
+				boot_logo_path = String();
+			}
+		}
 
 		Ref<Image> boot_logo;
 
@@ -4215,7 +4238,7 @@ int Main::start() {
 
 #ifdef TOOLS_ENABLED
 			if (editor) {
-				if (!recovery_mode && (game_path != String(GLOBAL_GET("application/run/main_scene")) || !editor_node->has_scenes_in_session())) {
+				if (!recovery_mode && (game_path != ResourceUID::ensure_path(String(GLOBAL_GET("application/run/main_scene"))) || !editor_node->has_scenes_in_session())) {
 					Error serr = editor_node->load_scene(local_game_path);
 					if (serr != OK) {
 						ERR_PRINT("Failed to load scene");


Test patch: