/*************************************************************************/ /* editor_file_system.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* 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 "editor_file_system.h" #include "core/config/project_settings.h" #include "core/io/file_access.h" #include "core/io/resource_importer.h" #include "core/io/resource_loader.h" #include "core/io/resource_saver.h" #include "core/os/os.h" #include "core/variant/variant_parser.h" #include "editor_node.h" #include "editor_resource_preview.h" #include "editor_settings.h" EditorFileSystem *EditorFileSystem::singleton = nullptr; //the name is the version, to keep compatibility with different versions of Godot #define CACHE_FILE_NAME "filesystem_cache7" void EditorFileSystemDirectory::sort_files() { files.sort_custom(); } int EditorFileSystemDirectory::find_file_index(const String &p_file) const { for (int i = 0; i < files.size(); i++) { if (files[i]->file == p_file) { return i; } } return -1; } int EditorFileSystemDirectory::find_dir_index(const String &p_dir) const { for (int i = 0; i < subdirs.size(); i++) { if (subdirs[i]->name == p_dir) { return i; } } return -1; } void EditorFileSystemDirectory::force_update() { // We set modified_time to 0 to force `EditorFileSystem::_scan_fs_changes` to search changes in the directory modified_time = 0; } int EditorFileSystemDirectory::get_subdir_count() const { return subdirs.size(); } EditorFileSystemDirectory *EditorFileSystemDirectory::get_subdir(int p_idx) { ERR_FAIL_INDEX_V(p_idx, subdirs.size(), nullptr); return subdirs[p_idx]; } int EditorFileSystemDirectory::get_file_count() const { return files.size(); } String EditorFileSystemDirectory::get_file(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, files.size(), ""); return files[p_idx]->file; } String EditorFileSystemDirectory::get_path() const { String p; const EditorFileSystemDirectory *d = this; while (d->parent) { p = d->name.plus_file(p); d = d->parent; } return "res://" + p; } String EditorFileSystemDirectory::get_file_path(int p_idx) const { String file = get_file(p_idx); const EditorFileSystemDirectory *d = this; while (d->parent) { file = d->name.plus_file(file); d = d->parent; } return "res://" + file; } Vector EditorFileSystemDirectory::get_file_deps(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, files.size(), Vector()); Vector deps; for (int i = 0; i < files[p_idx]->deps.size(); i++) { String dep = files[p_idx]->deps[i]; int sep_idx = dep.find("::"); //may contain type information, unwanted if (sep_idx != -1) { dep = dep.substr(0, sep_idx); } ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(dep); if (uid != ResourceUID::INVALID_ID) { //return proper dependency resoure from uid if (ResourceUID::get_singleton()->has_id(uid)) { dep = ResourceUID::get_singleton()->get_id_path(uid); } else { continue; } } deps.push_back(dep); } return deps; } bool EditorFileSystemDirectory::get_file_import_is_valid(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, files.size(), false); return files[p_idx]->import_valid; } uint64_t EditorFileSystemDirectory::get_file_modified_time(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, files.size(), 0); return files[p_idx]->modified_time; } String EditorFileSystemDirectory::get_file_script_class_name(int p_idx) const { return files[p_idx]->script_class_name; } String EditorFileSystemDirectory::get_file_script_class_extends(int p_idx) const { return files[p_idx]->script_class_extends; } String EditorFileSystemDirectory::get_file_script_class_icon_path(int p_idx) const { return files[p_idx]->script_class_icon_path; } StringName EditorFileSystemDirectory::get_file_type(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, files.size(), ""); return files[p_idx]->type; } String EditorFileSystemDirectory::get_name() { return name; } EditorFileSystemDirectory *EditorFileSystemDirectory::get_parent() { return parent; } void EditorFileSystemDirectory::_bind_methods() { ClassDB::bind_method(D_METHOD("get_subdir_count"), &EditorFileSystemDirectory::get_subdir_count); ClassDB::bind_method(D_METHOD("get_subdir", "idx"), &EditorFileSystemDirectory::get_subdir); ClassDB::bind_method(D_METHOD("get_file_count"), &EditorFileSystemDirectory::get_file_count); ClassDB::bind_method(D_METHOD("get_file", "idx"), &EditorFileSystemDirectory::get_file); ClassDB::bind_method(D_METHOD("get_file_path", "idx"), &EditorFileSystemDirectory::get_file_path); ClassDB::bind_method(D_METHOD("get_file_type", "idx"), &EditorFileSystemDirectory::get_file_type); ClassDB::bind_method(D_METHOD("get_file_script_class_name", "idx"), &EditorFileSystemDirectory::get_file_script_class_name); ClassDB::bind_method(D_METHOD("get_file_script_class_extends", "idx"), &EditorFileSystemDirectory::get_file_script_class_extends); ClassDB::bind_method(D_METHOD("get_file_import_is_valid", "idx"), &EditorFileSystemDirectory::get_file_import_is_valid); ClassDB::bind_method(D_METHOD("get_name"), &EditorFileSystemDirectory::get_name); ClassDB::bind_method(D_METHOD("get_path"), &EditorFileSystemDirectory::get_path); ClassDB::bind_method(D_METHOD("get_parent"), &EditorFileSystemDirectory::get_parent); ClassDB::bind_method(D_METHOD("find_file_index", "name"), &EditorFileSystemDirectory::find_file_index); ClassDB::bind_method(D_METHOD("find_dir_index", "name"), &EditorFileSystemDirectory::find_dir_index); } EditorFileSystemDirectory::EditorFileSystemDirectory() { modified_time = 0; parent = nullptr; verified = false; } EditorFileSystemDirectory::~EditorFileSystemDirectory() { for (int i = 0; i < files.size(); i++) { memdelete(files[i]); } for (int i = 0; i < subdirs.size(); i++) { memdelete(subdirs[i]); } } void EditorFileSystem::_scan_filesystem() { ERR_FAIL_COND(!scanning || new_filesystem); //read .fscache String cpath; sources_changed.clear(); file_cache.clear(); String project = ProjectSettings::get_singleton()->get_resource_path(); String fscache = EditorSettings::get_singleton()->get_project_settings_dir().plus_file(CACHE_FILE_NAME); FileAccess *f = FileAccess::open(fscache, FileAccess::READ); bool first = true; if (f) { //read the disk cache while (!f->eof_reached()) { String l = f->get_line().strip_edges(); if (first) { if (first_scan) { // only use this on first scan, afterwards it gets ignored // this is so on first reimport we synchronize versions, then // we don't care until editor restart. This is for usability mainly so // your workflow is not killed after changing a setting by forceful reimporting // everything there is. filesystem_settings_version_for_import = l.strip_edges(); if (filesystem_settings_version_for_import != ResourceFormatImporter::get_singleton()->get_import_settings_hash()) { revalidate_import_files = true; } } first = false; continue; } if (l == String()) { continue; } if (l.begins_with("::")) { Vector split = l.split("::"); ERR_CONTINUE(split.size() != 3); String name = split[1]; cpath = name; } else { Vector split = l.split("::"); ERR_CONTINUE(split.size() != 9); String name = split[0]; String file; file = name; name = cpath.plus_file(name); FileCache fc; fc.type = split[1]; fc.uid = split[2].to_int(); fc.modification_time = split[3].to_int(); fc.import_modification_time = split[4].to_int(); fc.import_valid = split[5].to_int() != 0; fc.import_group_file = split[6].strip_edges(); fc.script_class_name = split[7].get_slice("<>", 0); fc.script_class_extends = split[7].get_slice("<>", 1); fc.script_class_icon_path = split[7].get_slice("<>", 2); String deps = split[8].strip_edges(); if (deps.length()) { Vector dp = deps.split("<>"); for (int i = 0; i < dp.size(); i++) { String path = dp[i]; fc.deps.push_back(path); } } file_cache[name] = fc; } } f->close(); memdelete(f); } String update_cache = EditorSettings::get_singleton()->get_project_settings_dir().plus_file("filesystem_update4"); if (FileAccess::exists(update_cache)) { { FileAccessRef f2 = FileAccess::open(update_cache, FileAccess::READ); String l = f2->get_line().strip_edges(); while (l != String()) { file_cache.erase(l); //erase cache for this, so it gets updated l = f2->get_line().strip_edges(); } } DirAccessRef d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); d->remove(update_cache); //bye bye update cache } EditorProgressBG scan_progress("efs", "ScanFS", 1000); ScanProgress sp; sp.low = 0; sp.hi = 1; sp.progress = &scan_progress; new_filesystem = memnew(EditorFileSystemDirectory); new_filesystem->parent = nullptr; DirAccess *d = DirAccess::create(DirAccess::ACCESS_RESOURCES); d->change_dir("res://"); _scan_new_dir(new_filesystem, d, sp); file_cache.clear(); //clear caches, no longer needed memdelete(d); if (!first_scan) { //on the first scan this is done from the main thread after re-importing _save_filesystem_cache(); } scanning = false; } void EditorFileSystem::_save_filesystem_cache() { group_file_cache.clear(); String fscache = EditorSettings::get_singleton()->get_project_settings_dir().plus_file(CACHE_FILE_NAME); FileAccess *f = FileAccess::open(fscache, FileAccess::WRITE); ERR_FAIL_COND_MSG(!f, "Cannot create file '" + fscache + "'. Check user write permissions."); f->store_line(filesystem_settings_version_for_import); _save_filesystem_cache(filesystem, f); f->close(); memdelete(f); } void EditorFileSystem::_thread_func(void *_userdata) { EditorFileSystem *sd = (EditorFileSystem *)_userdata; sd->_scan_filesystem(); } bool EditorFileSystem::_test_for_reimport(const String &p_path, bool p_only_imported_files) { if (!reimport_on_missing_imported_files && p_only_imported_files) { return false; } if (!FileAccess::exists(p_path + ".import")) { return true; } if (!ResourceFormatImporter::get_singleton()->are_import_settings_valid(p_path)) { //reimport settings are not valid, reimport return true; } Error err; FileAccess *f = FileAccess::open(p_path + ".import", FileAccess::READ, &err); if (!f) { //no import file, do reimport return true; } VariantParser::StreamFile stream; stream.f = f; String assign; Variant value; VariantParser::Tag next_tag; int lines = 0; String error_text; List to_check; String importer_name; String source_file = ""; String source_md5 = ""; Vector dest_files; String dest_md5 = ""; int version = 0; bool found_uid = false; while (true) { assign = Variant(); next_tag.fields.clear(); next_tag.name = String(); err = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true); if (err == ERR_FILE_EOF) { break; } else if (err != OK) { ERR_PRINT("ResourceFormatImporter::load - '" + p_path + ".import:" + itos(lines) + "' error '" + error_text + "'."); memdelete(f); return false; //parse error, try reimport manually (Avoid reimport loop on broken file) } if (assign != String()) { if (assign.begins_with("path")) { to_check.push_back(value); } else if (assign == "files") { Array fa = value; for (int i = 0; i < fa.size(); i++) { to_check.push_back(fa[i]); } } else if (assign == "importer_version") { version = value; } else if (assign == "importer") { importer_name = value; } else if (assign == "uid") { found_uid = true; } else if (!p_only_imported_files) { if (assign == "source_file") { source_file = value; } else if (assign == "dest_files") { dest_files = value; } } } else if (next_tag.name != "remap" && next_tag.name != "deps") { break; } } memdelete(f); if (importer_name == "keep") { return false; //keep mode, do not reimport } if (!found_uid) { return true; //UUID not found, old format, reimport. } Ref importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(importer_name); if (importer->get_format_version() > version) { return true; // version changed, reimport } // Read the md5's from a separate file (so the import parameters aren't dependent on the file version String base_path = ResourceFormatImporter::get_singleton()->get_import_base_path(p_path); FileAccess *md5s = FileAccess::open(base_path + ".md5", FileAccess::READ, &err); if (!md5s) { // No md5's stored for this resource return true; } VariantParser::StreamFile md5_stream; md5_stream.f = md5s; while (true) { assign = Variant(); next_tag.fields.clear(); next_tag.name = String(); err = VariantParser::parse_tag_assign_eof(&md5_stream, lines, error_text, next_tag, assign, value, nullptr, true); if (err == ERR_FILE_EOF) { break; } else if (err != OK) { ERR_PRINT("ResourceFormatImporter::load - '" + p_path + ".import.md5:" + itos(lines) + "' error '" + error_text + "'."); memdelete(md5s); return false; // parse error } if (assign != String()) { if (!p_only_imported_files) { if (assign == "source_md5") { source_md5 = value; } else if (assign == "dest_md5") { dest_md5 = value; } } } } memdelete(md5s); //imported files are gone, reimport for (String &E : to_check) { if (!FileAccess::exists(E)) { return true; } } //check source md5 matching if (!p_only_imported_files) { if (source_file != String() && source_file != p_path) { return true; //file was moved, reimport } if (source_md5 == String()) { return true; //lacks md5, so just reimport } String md5 = FileAccess::get_md5(p_path); if (md5 != source_md5) { return true; } if (dest_files.size() && dest_md5 != String()) { md5 = FileAccess::get_multiple_md5(dest_files); if (md5 != dest_md5) { return true; } } } return false; //nothing changed } bool EditorFileSystem::_update_scan_actions() { sources_changed.clear(); bool fs_changed = false; Vector reimports; Vector reloads; for (ItemAction &ia : scan_actions) { switch (ia.action) { case ItemAction::ACTION_NONE: { } break; case ItemAction::ACTION_DIR_ADD: { int idx = 0; for (int i = 0; i < ia.dir->subdirs.size(); i++) { if (ia.new_dir->name.naturalnocasecmp_to(ia.dir->subdirs[i]->name) < 0) { break; } idx++; } if (idx == ia.dir->subdirs.size()) { ia.dir->subdirs.push_back(ia.new_dir); } else { ia.dir->subdirs.insert(idx, ia.new_dir); } fs_changed = true; } break; case ItemAction::ACTION_DIR_REMOVE: { ERR_CONTINUE(!ia.dir->parent); ia.dir->parent->subdirs.erase(ia.dir); memdelete(ia.dir); fs_changed = true; } break; case ItemAction::ACTION_FILE_ADD: { int idx = 0; for (int i = 0; i < ia.dir->files.size(); i++) { if (ia.new_file->file.naturalnocasecmp_to(ia.dir->files[i]->file) < 0) { break; } idx++; } if (idx == ia.dir->files.size()) { ia.dir->files.push_back(ia.new_file); } else { ia.dir->files.insert(idx, ia.new_file); } fs_changed = true; } break; case ItemAction::ACTION_FILE_REMOVE: { int idx = ia.dir->find_file_index(ia.file); ERR_CONTINUE(idx == -1); _delete_internal_files(ia.dir->files[idx]->file); memdelete(ia.dir->files[idx]); ia.dir->files.remove(idx); fs_changed = true; } break; case ItemAction::ACTION_FILE_TEST_REIMPORT: { int idx = ia.dir->find_file_index(ia.file); ERR_CONTINUE(idx == -1); String full_path = ia.dir->get_file_path(idx); if (_test_for_reimport(full_path, false)) { //must reimport reimports.push_back(full_path); reimports.append_array(_get_dependencies(full_path)); } else { //must not reimport, all was good //update modified times, to avoid reimport ia.dir->files[idx]->modified_time = FileAccess::get_modified_time(full_path); ia.dir->files[idx]->import_modified_time = FileAccess::get_modified_time(full_path + ".import"); } fs_changed = true; } break; case ItemAction::ACTION_FILE_RELOAD: { int idx = ia.dir->find_file_index(ia.file); ERR_CONTINUE(idx == -1); String full_path = ia.dir->get_file_path(idx); reloads.push_back(full_path); } break; } } if (reimports.size()) { reimport_files(reimports); } else { //reimport files will update the uid cache file so if nothing was reimported, update it manually ResourceUID::get_singleton()->update_cache(); } if (first_scan) { //only on first scan this is valid and updated, then settings changed. revalidate_import_files = false; filesystem_settings_version_for_import = ResourceFormatImporter::get_singleton()->get_import_settings_hash(); _save_filesystem_cache(); } if (reloads.size()) { emit_signal(SNAME("resources_reload"), reloads); } scan_actions.clear(); return fs_changed; } void EditorFileSystem::scan() { if (false /*&& bool(Globals::get_singleton()->get("debug/disable_scan"))*/) { return; } if (scanning || scanning_changes || thread.is_started()) { return; } _update_extensions(); abort_scan = false; if (!use_threads) { scanning = true; scan_total = 0; _scan_filesystem(); if (filesystem) { memdelete(filesystem); } //file_type_cache.clear(); filesystem = new_filesystem; new_filesystem = nullptr; _update_scan_actions(); scanning = false; emit_signal(SNAME("filesystem_changed")); emit_signal(SNAME("sources_changed"), sources_changed.size() > 0); _queue_update_script_classes(); first_scan = false; } else { ERR_FAIL_COND(thread.is_started()); set_process(true); Thread::Settings s; scanning = true; scan_total = 0; s.priority = Thread::PRIORITY_LOW; thread.start(_thread_func, this, s); //tree->hide(); //progress->show(); } } void EditorFileSystem::ScanProgress::update(int p_current, int p_total) const { float ratio = low + ((hi - low) / p_total) * p_current; progress->step(ratio * 1000); EditorFileSystem::singleton->scan_total = ratio; } EditorFileSystem::ScanProgress EditorFileSystem::ScanProgress::get_sub(int p_current, int p_total) const { ScanProgress sp = *this; float slice = (sp.hi - sp.low) / p_total; sp.low += slice * p_current; sp.hi = slice; return sp; } void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, DirAccess *da, const ScanProgress &p_progress) { List dirs; List files; String cd = da->get_current_dir(); p_dir->modified_time = FileAccess::get_modified_time(cd); da->list_dir_begin(); while (true) { String f = da->get_next(); if (f == "") { break; } if (da->current_is_hidden()) { continue; } if (da->current_is_dir()) { if (f.begins_with(".")) { // Ignore special and . / .. continue; } if (_should_skip_directory(cd.plus_file(f))) { continue; } dirs.push_back(f); } else { files.push_back(f); } } da->list_dir_end(); dirs.sort_custom(); files.sort_custom(); int total = dirs.size() + files.size(); int idx = 0; for (List::Element *E = dirs.front(); E; E = E->next(), idx++) { if (da->change_dir(E->get()) == OK) { String d = da->get_current_dir(); if (d == cd || !d.begins_with(cd)) { da->change_dir(cd); //avoid recursion } else { EditorFileSystemDirectory *efd = memnew(EditorFileSystemDirectory); efd->parent = p_dir; efd->name = E->get(); _scan_new_dir(efd, da, p_progress.get_sub(idx, total)); int idx2 = 0; for (int i = 0; i < p_dir->subdirs.size(); i++) { if (efd->name.naturalnocasecmp_to(p_dir->subdirs[i]->name) < 0) { break; } idx2++; } if (idx2 == p_dir->subdirs.size()) { p_dir->subdirs.push_back(efd); } else { p_dir->subdirs.insert(idx2, efd); } da->change_dir(".."); } } else { ERR_PRINT("Cannot go into subdir '" + E->get() + "'."); } p_progress.update(idx, total); } for (List::Element *E = files.front(); E; E = E->next(), idx++) { String ext = E->get().get_extension().to_lower(); if (!valid_extensions.has(ext)) { continue; //invalid } EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo); fi->file = E->get(); String path = cd.plus_file(fi->file); FileCache *fc = file_cache.getptr(path); uint64_t mt = FileAccess::get_modified_time(path); if (import_extensions.has(ext)) { //is imported uint64_t import_mt = 0; if (FileAccess::exists(path + ".import")) { import_mt = FileAccess::get_modified_time(path + ".import"); } if (fc && fc->modification_time == mt && fc->import_modification_time == import_mt && !_test_for_reimport(path, true)) { fi->type = fc->type; fi->uid = fc->uid; fi->deps = fc->deps; fi->modified_time = fc->modification_time; fi->import_modified_time = fc->import_modification_time; fi->import_valid = fc->import_valid; fi->script_class_name = fc->script_class_name; fi->import_group_file = fc->import_group_file; fi->script_class_extends = fc->script_class_extends; fi->script_class_icon_path = fc->script_class_icon_path; if (revalidate_import_files && !ResourceFormatImporter::get_singleton()->are_import_settings_valid(path)) { ItemAction ia; ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT; ia.dir = p_dir; ia.file = E->get(); scan_actions.push_back(ia); } if (fc->type == String()) { fi->type = ResourceLoader::get_resource_type(path); fi->import_group_file = ResourceLoader::get_import_group_file(path); //there is also the chance that file type changed due to reimport, must probably check this somehow here (or kind of note it for next time in another file?) //note: I think this should not happen any longer.. } if (fc->uid == ResourceUID::INVALID_ID) { // imported files should always have a UUID, so attempt to fetch it. fi->uid = ResourceLoader::get_resource_uid(path); } } else { fi->type = ResourceFormatImporter::get_singleton()->get_resource_type(path); fi->uid = ResourceFormatImporter::get_singleton()->get_resource_uid(path); fi->import_group_file = ResourceFormatImporter::get_singleton()->get_import_group_file(path); fi->script_class_name = _get_global_script_class(fi->type, path, &fi->script_class_extends, &fi->script_class_icon_path); fi->modified_time = 0; fi->import_modified_time = 0; fi->import_valid = ResourceLoader::is_import_valid(path); ItemAction ia; ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT; ia.dir = p_dir; ia.file = E->get(); scan_actions.push_back(ia); } } else { if (fc && fc->modification_time == mt) { //not imported, so just update type if changed fi->type = fc->type; fi->uid = fc->uid; fi->modified_time = fc->modification_time; fi->deps = fc->deps; fi->import_modified_time = 0; fi->import_valid = true; fi->script_class_name = fc->script_class_name; fi->script_class_extends = fc->script_class_extends; fi->script_class_icon_path = fc->script_class_icon_path; } else { //new or modified time fi->type = ResourceLoader::get_resource_type(path); fi->uid = ResourceLoader::get_resource_uid(path); fi->script_class_name = _get_global_script_class(fi->type, path, &fi->script_class_extends, &fi->script_class_icon_path); fi->deps = _get_dependencies(path); fi->modified_time = mt; fi->import_modified_time = 0; fi->import_valid = true; } } if (fi->uid != ResourceUID::INVALID_ID) { if (ResourceUID::get_singleton()->has_id(fi->uid)) { ResourceUID::get_singleton()->set_id(fi->uid, path); } else { ResourceUID::get_singleton()->add_id(fi->uid, path); } } for (int i = 0; i < ScriptServer::get_language_count(); i++) { ScriptLanguage *lang = ScriptServer::get_language(i); if (lang->supports_documentation() && fi->type == lang->get_type()) { Ref