Path: blob/master/editor/file_system/editor_file_system.cpp
20831 views
/**************************************************************************/1/* editor_file_system.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "editor_file_system.h"3132#include "core/config/project_settings.h"33#include "core/extension/gdextension_manager.h"34#include "core/io/dir_access.h"35#include "core/io/file_access.h"36#include "core/io/resource_saver.h"37#include "core/object/worker_thread_pool.h"38#include "core/os/os.h"39#include "core/variant/variant_parser.h"40#include "editor/doc/editor_help.h"41#include "editor/editor_node.h"42#include "editor/file_system/editor_paths.h"43#include "editor/inspector/editor_resource_preview.h"44#include "editor/script/script_editor_plugin.h"45#include "editor/settings/editor_settings.h"46#include "editor/settings/project_settings_editor.h"47#include "scene/resources/packed_scene.h"4849EditorFileSystem *EditorFileSystem::singleton = nullptr;50int EditorFileSystem::nb_files_total = 0;51EditorFileSystem::ScannedDirectory *EditorFileSystem::first_scan_root_dir = nullptr;5253//the name is the version, to keep compatibility with different versions of Godot54#define CACHE_FILE_NAME "filesystem_cache10"5556int EditorFileSystemDirectory::find_file_index(const String &p_file) const {57for (int i = 0; i < files.size(); i++) {58if (files[i]->file == p_file) {59return i;60}61}62return -1;63}6465int EditorFileSystemDirectory::find_dir_index(const String &p_dir) const {66for (int i = 0; i < subdirs.size(); i++) {67if (subdirs[i]->name == p_dir) {68return i;69}70}7172return -1;73}7475void EditorFileSystemDirectory::force_update() {76// We set modified_time to 0 to force `EditorFileSystem::_scan_fs_changes` to search changes in the directory77modified_time = 0;78}7980int EditorFileSystemDirectory::get_subdir_count() const {81return subdirs.size();82}8384EditorFileSystemDirectory *EditorFileSystemDirectory::get_subdir(int p_idx) {85ERR_FAIL_INDEX_V(p_idx, subdirs.size(), nullptr);86return subdirs[p_idx];87}8889int EditorFileSystemDirectory::get_file_count() const {90return files.size();91}9293String EditorFileSystemDirectory::get_file(int p_idx) const {94ERR_FAIL_INDEX_V(p_idx, files.size(), "");9596return files[p_idx]->file;97}9899String EditorFileSystemDirectory::get_path() const {100int parents = 0;101const EditorFileSystemDirectory *efd = this;102// Determine the level of nesting.103while (efd->parent) {104parents++;105efd = efd->parent;106}107108if (parents == 0) {109return "res://";110}111112// Using PackedStringArray, because the path is built in reverse order.113PackedStringArray path_bits;114// Allocate an array based on nesting. It will store path bits.115path_bits.resize(parents + 2); // Last String is empty, so paths end with /.116String *path_write = path_bits.ptrw();117path_write[0] = "res:/";118119efd = this;120for (int i = parents; i > 0; i--) {121path_write[i] = efd->name;122efd = efd->parent;123}124return String("/").join(path_bits);125}126127String EditorFileSystemDirectory::get_file_path(int p_idx) const {128return get_path().path_join(get_file(p_idx));129}130131ResourceUID::ID EditorFileSystemDirectory::get_file_uid(int p_idx) const {132ERR_FAIL_INDEX_V(p_idx, files.size(), ResourceUID::INVALID_ID);133return files[p_idx]->uid;134}135136Vector<String> EditorFileSystemDirectory::get_file_deps(int p_idx) const {137ERR_FAIL_INDEX_V(p_idx, files.size(), Vector<String>());138Vector<String> deps;139140for (int i = 0; i < files[p_idx]->deps.size(); i++) {141String dep = files[p_idx]->deps[i];142int sep_idx = dep.find("::"); //may contain type information, unwanted143if (sep_idx != -1) {144dep = dep.substr(0, sep_idx);145}146ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(dep);147if (uid != ResourceUID::INVALID_ID) {148//return proper dependency resource from uid149if (ResourceUID::get_singleton()->has_id(uid)) {150dep = ResourceUID::get_singleton()->get_id_path(uid);151} else {152continue;153}154}155deps.push_back(dep);156}157return deps;158}159160bool EditorFileSystemDirectory::get_file_import_is_valid(int p_idx) const {161ERR_FAIL_INDEX_V(p_idx, files.size(), false);162return files[p_idx]->import_valid;163}164165uint64_t EditorFileSystemDirectory::get_file_modified_time(int p_idx) const {166ERR_FAIL_INDEX_V(p_idx, files.size(), 0);167return files[p_idx]->modified_time;168}169170uint64_t EditorFileSystemDirectory::get_file_import_modified_time(int p_idx) const {171ERR_FAIL_INDEX_V(p_idx, files.size(), 0);172return files[p_idx]->import_modified_time;173}174175String EditorFileSystemDirectory::get_file_script_class_name(int p_idx) const {176return files[p_idx]->class_info.name;177}178179String EditorFileSystemDirectory::get_file_script_class_extends(int p_idx) const {180return files[p_idx]->class_info.extends;181}182183String EditorFileSystemDirectory::get_file_script_class_icon_path(int p_idx) const {184return files[p_idx]->class_info.icon_path;185}186187String EditorFileSystemDirectory::get_file_icon_path(int p_idx) const {188return files[p_idx]->class_info.icon_path;189}190191StringName EditorFileSystemDirectory::get_file_type(int p_idx) const {192ERR_FAIL_INDEX_V(p_idx, files.size(), "");193return files[p_idx]->type;194}195196StringName EditorFileSystemDirectory::get_file_resource_script_class(int p_idx) const {197ERR_FAIL_INDEX_V(p_idx, files.size(), "");198return files[p_idx]->resource_script_class;199}200201String EditorFileSystemDirectory::get_name() {202return name;203}204205EditorFileSystemDirectory *EditorFileSystemDirectory::get_parent() {206return parent;207}208209void EditorFileSystemDirectory::_bind_methods() {210ClassDB::bind_method(D_METHOD("get_subdir_count"), &EditorFileSystemDirectory::get_subdir_count);211ClassDB::bind_method(D_METHOD("get_subdir", "idx"), &EditorFileSystemDirectory::get_subdir);212ClassDB::bind_method(D_METHOD("get_file_count"), &EditorFileSystemDirectory::get_file_count);213ClassDB::bind_method(D_METHOD("get_file", "idx"), &EditorFileSystemDirectory::get_file);214ClassDB::bind_method(D_METHOD("get_file_path", "idx"), &EditorFileSystemDirectory::get_file_path);215ClassDB::bind_method(D_METHOD("get_file_type", "idx"), &EditorFileSystemDirectory::get_file_type);216ClassDB::bind_method(D_METHOD("get_file_script_class_name", "idx"), &EditorFileSystemDirectory::get_file_script_class_name);217ClassDB::bind_method(D_METHOD("get_file_script_class_extends", "idx"), &EditorFileSystemDirectory::get_file_script_class_extends);218ClassDB::bind_method(D_METHOD("get_file_import_is_valid", "idx"), &EditorFileSystemDirectory::get_file_import_is_valid);219ClassDB::bind_method(D_METHOD("get_name"), &EditorFileSystemDirectory::get_name);220ClassDB::bind_method(D_METHOD("get_path"), &EditorFileSystemDirectory::get_path);221ClassDB::bind_method(D_METHOD("get_parent"), &EditorFileSystemDirectory::get_parent);222ClassDB::bind_method(D_METHOD("find_file_index", "name"), &EditorFileSystemDirectory::find_file_index);223ClassDB::bind_method(D_METHOD("find_dir_index", "name"), &EditorFileSystemDirectory::find_dir_index);224}225226EditorFileSystemDirectory::EditorFileSystemDirectory() {227modified_time = 0;228parent = nullptr;229}230231EditorFileSystemDirectory::~EditorFileSystemDirectory() {232for (EditorFileSystemDirectory::FileInfo *fi : files) {233memdelete(fi);234}235236for (EditorFileSystemDirectory *dir : subdirs) {237memdelete(dir);238}239}240241EditorFileSystem::ScannedDirectory::~ScannedDirectory() {242for (ScannedDirectory *dir : subdirs) {243memdelete(dir);244}245}246247void EditorFileSystem::_load_first_scan_root_dir() {248Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);249first_scan_root_dir = memnew(ScannedDirectory);250first_scan_root_dir->full_path = "res://";251252nb_files_total = _scan_new_dir(first_scan_root_dir, d);253}254255void EditorFileSystem::scan_for_uid() {256// Load file structure into memory.257_load_first_scan_root_dir();258259// Load extensions for which an .import should exists.260List<String> extensionsl;261HashSet<String> import_extensions;262ResourceFormatImporter::get_singleton()->get_recognized_extensions(&extensionsl);263for (const String &E : extensionsl) {264import_extensions.insert(E);265}266267// Scan the file system to load uid.268_scan_for_uid_directory(first_scan_root_dir, import_extensions);269270// It's done, resetting the callback method to prevent a second scan.271ResourceUID::scan_for_uid_on_startup = nullptr;272}273274void EditorFileSystem::_scan_for_uid_directory(const ScannedDirectory *p_scan_dir, const HashSet<String> &p_import_extensions) {275for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) {276_scan_for_uid_directory(scan_sub_dir, p_import_extensions);277}278279for (const String &scan_file : p_scan_dir->files) {280const String ext = scan_file.get_extension().to_lower();281282if (ext == "uid" || ext == "import") {283continue;284}285286const String path = p_scan_dir->full_path.path_join(scan_file);287ResourceUID::ID uid = ResourceUID::INVALID_ID;288if (p_import_extensions.has(ext)) {289if (FileAccess::exists(path + ".import")) {290uid = ResourceFormatImporter::get_singleton()->get_resource_uid(path);291}292} else {293uid = ResourceLoader::get_resource_uid(path);294}295296if (uid != ResourceUID::INVALID_ID) {297if (!ResourceUID::get_singleton()->has_id(uid)) {298ResourceUID::get_singleton()->add_id(uid, path);299}300}301}302}303304void EditorFileSystem::_first_scan_filesystem() {305EditorProgress ep = EditorProgress("first_scan_filesystem", TTR("Project initialization"), 5);306HashSet<String> existing_class_names;307HashSet<String> extensions;308309if (!first_scan_root_dir) {310ep.step(TTR("Scanning file structure..."), 0, true);311_load_first_scan_root_dir();312}313314// Preloading GDExtensions file extensions to prevent looping on all the resource loaders315// for each files in _first_scan_process_scripts.316List<String> gdextension_extensions;317ResourceLoader::get_recognized_extensions_for_type("GDExtension", &gdextension_extensions);318319// This loads the global class names from the scripts and ensures that even if the320// global_script_class_cache.cfg was missing or invalid, the global class names are valid in ScriptServer.321// At the same time, to prevent looping multiple times in all files, it looks for extensions.322ep.step(TTR("Loading global class names..."), 1, true);323_first_scan_process_scripts(first_scan_root_dir, gdextension_extensions, existing_class_names, extensions);324325// Removing invalid global class to prevent having invalid paths in ScriptServer.326bool save_scripts = _remove_invalid_global_class_names(existing_class_names);327328// If a global class is found or removed, we sync global_script_class_cache.cfg with the ScriptServer329if (!existing_class_names.is_empty() || save_scripts) {330EditorNode::get_editor_data().script_class_save_global_classes();331}332333// Processing extensions to add new extensions or remove invalid ones.334// Important to do it in the first scan so custom types, new class names, custom importers, etc...335// from extensions are ready to go before plugins, autoloads and resources validation/importation.336// At this point, a restart of the editor should not be needed so we don't use the return value.337ep.step(TTR("Verifying GDExtensions..."), 2, true);338GDExtensionManager::get_singleton()->ensure_extensions_loaded(extensions);339340// Now that all the global class names should be loaded, create autoloads and plugins.341// This is done after loading the global class names because autoloads and plugins can use342// global class names.343ep.step(TTR("Creating autoload scripts..."), 3, true);344ProjectSettingsEditor::get_singleton()->init_autoloads();345346ep.step(TTR("Initializing plugins..."), 4, true);347EditorNode::get_singleton()->init_plugins();348349ep.step(TTR("Starting file scan..."), 5, true);350}351352void EditorFileSystem::_first_scan_process_scripts(const ScannedDirectory *p_scan_dir, List<String> &p_gdextension_extensions, HashSet<String> &p_existing_class_names, HashSet<String> &p_extensions) {353for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) {354_first_scan_process_scripts(scan_sub_dir, p_gdextension_extensions, p_existing_class_names, p_extensions);355}356357for (const String &scan_file : p_scan_dir->files) {358// Optimization to skip the ResourceLoader::get_resource_type for files359// that are not scripts. Some loader get_resource_type methods read the file360// which can be very slow on large projects.361const String ext = scan_file.get_extension().to_lower();362bool is_script = false;363for (int i = 0; i < ScriptServer::get_language_count(); i++) {364if (ScriptServer::get_language(i)->get_extension() == ext) {365is_script = true;366break;367}368}369if (is_script) {370const String path = p_scan_dir->full_path.path_join(scan_file);371const String type = ResourceLoader::get_resource_type(path);372373if (ClassDB::is_parent_class(type, SNAME("Script"))) {374const ScriptClassInfo &info = _get_global_script_class(type, path);375ScriptClassInfoUpdate update(info);376update.type = type;377_register_global_class_script(path, path, update);378379if (!info.name.is_empty()) {380p_existing_class_names.insert(info.name);381}382}383}384385// Check for GDExtensions.386if (p_gdextension_extensions.find(ext)) {387const String path = p_scan_dir->full_path.path_join(scan_file);388const String type = ResourceLoader::get_resource_type(path);389if (type == SNAME("GDExtension")) {390p_extensions.insert(path);391}392}393}394}395396void EditorFileSystem::_scan_filesystem() {397// On the first scan, the first_scan_root_dir is created in _first_scan_filesystem.398ERR_FAIL_COND(!scanning || new_filesystem || (first_scan && !first_scan_root_dir));399400//read .fscache401String cpath;402403sources_changed.clear();404file_cache.clear();405406String project = ProjectSettings::get_singleton()->get_resource_path();407408String fscache = EditorPaths::get_singleton()->get_project_settings_dir().path_join(CACHE_FILE_NAME);409{410Ref<FileAccess> f = FileAccess::open(fscache, FileAccess::READ);411412bool first = true;413if (f.is_valid()) {414//read the disk cache415while (!f->eof_reached()) {416String l = f->get_line().strip_edges();417if (first) {418if (first_scan) {419// only use this on first scan, afterwards it gets ignored420// this is so on first reimport we synchronize versions, then421// we don't care until editor restart. This is for usability mainly so422// your workflow is not killed after changing a setting by forceful reimporting423// everything there is.424filesystem_settings_version_for_import = l.strip_edges();425if (filesystem_settings_version_for_import != ResourceFormatImporter::get_singleton()->get_import_settings_hash()) {426revalidate_import_files = true;427}428}429first = false;430continue;431}432if (l.is_empty()) {433continue;434}435436if (l.begins_with("::")) {437Vector<String> split = l.split("::");438ERR_CONTINUE(split.size() != 3);439const String &name = split[1];440441cpath = name;442443} else {444// The last section (deps) may contain the same splitter, so limit the maxsplit to 8 to get the complete deps.445Vector<String> split = l.split("::", true, 8);446ERR_CONTINUE(split.size() < 9);447String name = split[0];448String file;449450file = name;451name = cpath.path_join(name);452453FileCache fc;454fc.type = split[1].get_slicec('/', 0);455fc.resource_script_class = split[1].get_slicec('/', 1);456fc.uid = split[2].to_int();457fc.modification_time = split[3].to_int();458fc.import_modification_time = split[4].to_int();459fc.import_valid = split[5].to_int() != 0;460fc.import_group_file = split[6].strip_edges();461{462const Vector<String> &slices = split[7].split("<>");463ERR_CONTINUE(slices.size() < 7);464fc.class_info.name = slices[0];465fc.class_info.extends = slices[1];466fc.class_info.icon_path = slices[2];467fc.class_info.is_abstract = slices[3].to_int();468fc.class_info.is_tool = slices[4].to_int();469fc.import_md5 = slices[5];470fc.import_dest_paths = slices[6].split("<*>");471}472fc.deps = split[8].strip_edges().split("<>", false);473474file_cache[name] = fc;475}476}477}478}479480const String update_cache = EditorPaths::get_singleton()->get_project_settings_dir().path_join("filesystem_update4");481if (first_scan && FileAccess::exists(update_cache)) {482{483Ref<FileAccess> f2 = FileAccess::open(update_cache, FileAccess::READ);484String l = f2->get_line().strip_edges();485while (!l.is_empty()) {486dep_update_list.insert(l);487file_cache.erase(l); // Erase cache for this, so it gets updated.488l = f2->get_line().strip_edges();489}490}491492Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);493d->remove(update_cache); // Bye bye update cache.494}495496EditorProgressBG scan_progress("efs", "ScanFS", 1000);497ScanProgress sp;498sp.hi = nb_files_total;499sp.progress = &scan_progress;500501new_filesystem = memnew(EditorFileSystemDirectory);502new_filesystem->parent = nullptr;503504ScannedDirectory *sd;505HashSet<String> *processed_files = nullptr;506// On the first scan, the first_scan_root_dir is created in _first_scan_filesystem.507if (first_scan) {508sd = first_scan_root_dir;509// Will be updated on scan.510ResourceUID::get_singleton()->clear();511ResourceUID::scan_for_uid_on_startup = nullptr;512processed_files = memnew(HashSet<String>());513} else {514Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);515sd = memnew(ScannedDirectory);516sd->full_path = "res://";517nb_files_total = _scan_new_dir(sd, d);518}519520_process_file_system(sd, new_filesystem, sp, processed_files);521522if (first_scan) {523_process_removed_files(*processed_files);524}525dep_update_list.clear();526file_cache.clear(); //clear caches, no longer needed527528if (first_scan) {529memdelete(first_scan_root_dir);530first_scan_root_dir = nullptr;531memdelete(processed_files);532} else {533//on the first scan this is done from the main thread after re-importing534_save_filesystem_cache();535}536537scanning = false;538}539540void EditorFileSystem::_save_filesystem_cache() {541group_file_cache.clear();542543String fscache = EditorPaths::get_singleton()->get_project_settings_dir().path_join(CACHE_FILE_NAME);544545Ref<FileAccess> f = FileAccess::open(fscache, FileAccess::WRITE);546ERR_FAIL_COND_MSG(f.is_null(), "Cannot create file '" + fscache + "'. Check user write permissions.");547548f->store_line(filesystem_settings_version_for_import);549_save_filesystem_cache(filesystem, f);550}551552void EditorFileSystem::_thread_func(void *_userdata) {553EditorFileSystem *sd = (EditorFileSystem *)_userdata;554sd->_scan_filesystem();555}556557bool EditorFileSystem::_is_test_for_reimport_needed(const String &p_path, uint64_t p_last_modification_time, uint64_t p_modification_time, uint64_t p_last_import_modification_time, uint64_t p_import_modification_time, const Vector<String> &p_import_dest_paths) {558// The idea here is to trust the cache. If the last modification times in the cache correspond559// to the last modification times of the files on disk, it means the files have not changed since560// the last import, and the files in .godot/imported (p_import_dest_paths) should all be valid.561if (p_last_modification_time != p_modification_time) {562return true;563}564if (p_last_import_modification_time != p_import_modification_time) {565return true;566}567if (reimport_on_missing_imported_files) {568for (const String &path : p_import_dest_paths) {569if (!FileAccess::exists(path)) {570return true;571}572}573}574return false;575}576577bool EditorFileSystem::_test_for_reimport(const String &p_path, const String &p_expected_import_md5) {578if (p_expected_import_md5.is_empty()) {579// Marked as reimportation needed.580return true;581}582String new_md5 = FileAccess::get_md5(p_path + ".import");583if (p_expected_import_md5 != new_md5) {584return true;585}586587Error err;588Ref<FileAccess> f = FileAccess::open(p_path + ".import", FileAccess::READ, &err);589590if (f.is_null()) { // No import file, reimport.591return true;592}593594VariantParser::StreamFile stream;595stream.f = f;596597String assign;598Variant value;599VariantParser::Tag next_tag;600601int lines = 0;602String error_text;603604Vector<String> to_check;605606String importer_name;607String source_file = "";608String source_md5 = "";609Vector<String> dest_files;610String dest_md5 = "";611int version = 0;612bool found_uid = false;613Variant meta;614615while (true) {616assign = Variant();617next_tag.fields.clear();618next_tag.name = String();619620err = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true);621if (err == ERR_FILE_EOF) {622break;623} else if (err != OK) {624ERR_PRINT("ResourceFormatImporter::load - '" + p_path + ".import:" + itos(lines) + "' error '" + error_text + "'.");625// Parse error, skip and let user attempt manual reimport to avoid reimport loop.626return false;627}628629if (!assign.is_empty()) {630if (assign == "valid" && value.operator bool() == false) {631// Invalid import (failed previous import), skip and let user attempt manual reimport to avoid reimport loop.632return false;633}634if (assign.begins_with("path")) {635to_check.push_back(value);636} else if (assign == "files") {637Array fa = value;638for (const Variant &check_path : fa) {639to_check.push_back(check_path);640}641} else if (assign == "importer_version") {642version = value;643} else if (assign == "importer") {644importer_name = value;645} else if (assign == "uid") {646found_uid = true;647} else if (assign == "source_file") {648source_file = value;649} else if (assign == "dest_files") {650dest_files = value;651} else if (assign == "metadata") {652meta = value;653}654655} else if (next_tag.name != "remap" && next_tag.name != "deps") {656break;657}658}659660if (importer_name == "keep" || importer_name == "skip") {661return false; // Keep mode, do not reimport.662}663664if (!found_uid) {665return true; // UID not found, old format, reimport.666}667668// Imported files are gone, reimport.669for (const String &E : to_check) {670if (!FileAccess::exists(E)) {671return true;672}673}674675Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(importer_name);676677if (importer.is_null()) {678return true; // The importer has possibly changed, try to reimport.679}680681if (importer->get_format_version() > version) {682return true; // Version changed, reimport.683}684685if (!importer->are_import_settings_valid(p_path, meta)) {686// Reimport settings are out of sync with project settings, reimport.687return true;688}689690// Read the md5's from a separate file (so the import parameters aren't dependent on the file version).691String base_path = ResourceFormatImporter::get_singleton()->get_import_base_path(p_path);692Ref<FileAccess> md5s = FileAccess::open(base_path + ".md5", FileAccess::READ, &err);693if (md5s.is_null()) { // No md5's stored for this resource.694return true;695}696697VariantParser::StreamFile md5_stream;698md5_stream.f = md5s;699700while (true) {701assign = Variant();702next_tag.fields.clear();703next_tag.name = String();704705err = VariantParser::parse_tag_assign_eof(&md5_stream, lines, error_text, next_tag, assign, value, nullptr, true);706707if (err == ERR_FILE_EOF) {708break;709} else if (err != OK) {710ERR_PRINT("ResourceFormatImporter::load - '" + p_path + ".import.md5:" + itos(lines) + "' error '" + error_text + "'.");711return false; // Parse error.712}713if (!assign.is_empty()) {714if (assign == "source_md5") {715source_md5 = value;716} else if (assign == "dest_md5") {717dest_md5 = value;718}719}720}721722// Check source md5 matching.723if (!source_file.is_empty() && source_file != p_path) {724return true; // File was moved, reimport.725}726727if (source_md5.is_empty()) {728return true; // Lacks md5, so just reimport.729}730731String md5 = FileAccess::get_md5(p_path);732if (md5 != source_md5) {733return true;734}735736if (!dest_files.is_empty() && !dest_md5.is_empty()) {737md5 = FileAccess::get_multiple_md5(dest_files);738if (md5 != dest_md5) {739return true;740}741}742743return false; // Nothing changed.744}745746Vector<String> EditorFileSystem::_get_import_dest_paths(const String &p_path) {747Error err;748Ref<FileAccess> f = FileAccess::open(p_path + ".import", FileAccess::READ, &err);749750if (f.is_null()) { // No import file, reimport.751return Vector<String>();752}753754VariantParser::StreamFile stream;755stream.f = f;756757String assign;758Variant value;759VariantParser::Tag next_tag;760761int lines = 0;762String error_text;763764Vector<String> dest_paths;765String importer_name;766767while (true) {768assign = Variant();769next_tag.fields.clear();770next_tag.name = String();771772err = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true);773if (err == ERR_FILE_EOF) {774break;775} else if (err != OK) {776ERR_PRINT("ResourceFormatImporter::load - '" + p_path + ".import:" + itos(lines) + "' error '" + error_text + "'.");777// Parse error, skip and let user attempt manual reimport to avoid reimport loop.778return Vector<String>();779}780781if (!assign.is_empty()) {782if (assign == "valid" && value.operator bool() == false) {783// Invalid import (failed previous import), skip and let user attempt manual reimport to avoid reimport loop.784return Vector<String>();785}786if (assign.begins_with("path")) {787dest_paths.push_back(value);788} else if (assign == "files") {789Array fa = value;790for (const Variant &dest_path : fa) {791dest_paths.push_back(dest_path);792}793} else if (assign == "importer") {794importer_name = value;795}796} else if (next_tag.name != "remap" && next_tag.name != "deps") {797break;798}799}800801if (importer_name == "keep" || importer_name == "skip") {802return Vector<String>();803}804805return dest_paths;806}807808bool EditorFileSystem::_scan_import_support(const Vector<String> &reimports) {809if (import_support_queries.is_empty()) {810return false;811}812HashMap<String, int> import_support_test;813Vector<bool> import_support_tested;814import_support_tested.resize(import_support_queries.size());815for (int i = 0; i < import_support_queries.size(); i++) {816import_support_tested.write[i] = false;817if (import_support_queries[i]->is_active()) {818Vector<String> extensions = import_support_queries[i]->get_file_extensions();819for (int j = 0; j < extensions.size(); j++) {820import_support_test.insert(extensions[j], i);821}822}823}824825if (import_support_test.is_empty()) {826return false; //well nothing to do827}828829for (int i = 0; i < reimports.size(); i++) {830HashMap<String, int>::Iterator E = import_support_test.find(reimports[i].get_extension().to_lower());831if (E) {832import_support_tested.write[E->value] = true;833}834}835836for (int i = 0; i < import_support_tested.size(); i++) {837if (import_support_tested[i]) {838if (import_support_queries.write[i]->query()) {839return true;840}841}842}843844return false;845}846847bool EditorFileSystem::_update_scan_actions() {848sources_changed.clear();849850// We need to update the script global class names before the reimports to be sure that851// all the importer classes that depends on class names will work.852_update_script_classes();853854bool fs_changed = false;855856Vector<String> reimports;857Vector<String> reloads;858859EditorProgress *ep = nullptr;860if (scan_actions.size() > 1) {861ep = memnew(EditorProgress("_update_scan_actions", TTR("Scanning actions..."), scan_actions.size()));862}863864int step_count = 0;865for (const ItemAction &ia : scan_actions) {866switch (ia.action) {867case ItemAction::ACTION_NONE: {868} break;869case ItemAction::ACTION_DIR_ADD: {870int idx = 0;871for (int i = 0; i < ia.dir->subdirs.size(); i++) {872if (ia.new_dir->name.filenocasecmp_to(ia.dir->subdirs[i]->name) < 0) {873break;874}875idx++;876}877if (idx == ia.dir->subdirs.size()) {878ia.dir->subdirs.push_back(ia.new_dir);879} else {880ia.dir->subdirs.insert(idx, ia.new_dir);881}882883fs_changed = true;884} break;885case ItemAction::ACTION_DIR_REMOVE: {886ERR_CONTINUE(!ia.dir->parent);887ia.dir->parent->subdirs.erase(ia.dir);888memdelete(ia.dir);889fs_changed = true;890} break;891case ItemAction::ACTION_FILE_ADD: {892int idx = 0;893for (int i = 0; i < ia.dir->files.size(); i++) {894if (ia.new_file->file.filenocasecmp_to(ia.dir->files[i]->file) < 0) {895break;896}897idx++;898}899if (idx == ia.dir->files.size()) {900ia.dir->files.push_back(ia.new_file);901} else {902ia.dir->files.insert(idx, ia.new_file);903}904905fs_changed = true;906907const String new_file_path = ia.dir->get_file_path(idx);908const ResourceUID::ID existing_id = ResourceLoader::get_resource_uid(new_file_path);909if (existing_id != ResourceUID::INVALID_ID) {910const String old_path = ResourceUID::get_singleton()->get_id_path(existing_id);911if (old_path != new_file_path && FileAccess::exists(old_path)) {912const ResourceUID::ID new_id = ResourceUID::get_singleton()->create_id_for_path(new_file_path);913ResourceUID::get_singleton()->add_id(new_id, new_file_path);914ResourceSaver::set_uid(new_file_path, new_id);915WARN_PRINT(vformat("Duplicate UID detected for Resource at \"%s\".\nOld Resource path: \"%s\". The new file UID was changed automatically.", new_file_path, old_path));916} else {917// Re-assign the UID to file, just in case it was pulled from cache.918ResourceSaver::set_uid(new_file_path, existing_id);919}920} else if (ResourceLoader::should_create_uid_file(new_file_path)) {921Ref<FileAccess> f = FileAccess::open(new_file_path + ".uid", FileAccess::WRITE);922if (f.is_valid()) {923ia.new_file->uid = ResourceUID::get_singleton()->create_id_for_path(new_file_path);924f->store_line(ResourceUID::get_singleton()->id_to_text(ia.new_file->uid));925}926}927928if (ClassDB::is_parent_class(ia.new_file->type, SNAME("Script"))) {929_queue_update_script_class(new_file_path, ScriptClassInfoUpdate::from_file_info(ia.new_file));930}931if (ia.new_file->type == SNAME("PackedScene")) {932_queue_update_scene_groups(new_file_path);933}934935} break;936case ItemAction::ACTION_FILE_REMOVE: {937int idx = ia.dir->find_file_index(ia.file);938ERR_CONTINUE(idx == -1);939940const String file_path = ia.dir->get_file_path(idx);941const String class_name = ia.dir->files[idx]->class_info.name;942if (ClassDB::is_parent_class(ia.dir->files[idx]->type, SNAME("Script"))) {943_queue_update_script_class(file_path, ScriptClassInfoUpdate());944}945if (ia.dir->files[idx]->type == SNAME("PackedScene")) {946_queue_update_scene_groups(file_path);947}948949_delete_internal_files(file_path);950memdelete(ia.dir->files[idx]);951ia.dir->files.remove_at(idx);952953// Restore another script with the same global class name if it exists.954if (!class_name.is_empty()) {955EditorFileSystemDirectory::FileInfo *old_fi = nullptr;956String old_file = _get_file_by_class_name(filesystem, class_name, old_fi);957if (!old_file.is_empty() && old_fi) {958_queue_update_script_class(old_file, ScriptClassInfoUpdate::from_file_info(old_fi));959}960}961962fs_changed = true;963964} break;965case ItemAction::ACTION_FILE_TEST_REIMPORT: {966int idx = ia.dir->find_file_index(ia.file);967ERR_CONTINUE(idx == -1);968String full_path = ia.dir->get_file_path(idx);969970bool need_reimport = _test_for_reimport(full_path, ia.dir->files[idx]->import_md5);971if (need_reimport) {972// Must reimport.973reimports.push_back(full_path);974Vector<String> dependencies = _get_dependencies(full_path);975for (const String &dep : dependencies) {976const String &dependency_path = dep.contains("::") ? dep.get_slice("::", 0) : dep;977if (_can_import_file(dep)) {978reimports.push_back(dependency_path);979}980}981} else {982// Must not reimport, all was good.983// Update modified times, md5 and destination paths, to avoid reimport.984ia.dir->files[idx]->modified_time = FileAccess::get_modified_time(full_path);985ia.dir->files[idx]->import_modified_time = FileAccess::get_modified_time(full_path + ".import");986if (ia.dir->files[idx]->import_md5.is_empty()) {987ia.dir->files[idx]->import_md5 = FileAccess::get_md5(full_path + ".import");988}989ia.dir->files[idx]->import_dest_paths = _get_import_dest_paths(full_path);990}991992fs_changed = true;993} break;994case ItemAction::ACTION_FILE_RELOAD: {995int idx = ia.dir->find_file_index(ia.file);996ERR_CONTINUE(idx == -1);997998// Only reloads the resources that are already loaded.999if (ResourceCache::has(ia.dir->get_file_path(idx))) {1000reloads.push_back(ia.dir->get_file_path(idx));1001}1002} break;1003}10041005if (ep) {1006ep->step(ia.file, step_count++, false);1007}1008}10091010memdelete_notnull(ep);10111012if (_scan_extensions()) {1013//needs editor restart1014//extensions also may provide filetypes to be imported, so they must run before importing1015if (EditorNode::immediate_confirmation_dialog(TTR("Some extensions need the editor to restart to take effect."), first_scan ? TTR("Restart") : TTR("Save & Restart"), TTR("Continue"))) {1016if (!first_scan) {1017EditorNode::get_singleton()->save_all_scenes();1018}1019EditorNode::get_singleton()->restart_editor();1020//do not import1021return true;1022}1023}10241025if (!reimports.is_empty()) {1026if (_scan_import_support(reimports)) {1027return true;1028}10291030reimport_files(reimports);1031} else {1032//reimport files will update the uid cache file so if nothing was reimported, update it manually1033ResourceUID::get_singleton()->update_cache();1034}10351036if (!reloads.is_empty()) {1037// Update global class names, dependencies, etc...1038update_files(reloads);1039}10401041if (first_scan) {1042//only on first scan this is valid and updated, then settings changed.1043revalidate_import_files = false;1044filesystem_settings_version_for_import = ResourceFormatImporter::get_singleton()->get_import_settings_hash();1045_save_filesystem_cache();1046}10471048// Moving the processing of pending updates before the resources_reload event to be sure all global class names1049// are updated. Script.cpp listens on resources_reload and reloads updated scripts.1050_process_update_pending();10511052if (reloads.size()) {1053emit_signal(SNAME("resources_reload"), reloads);1054}1055scan_actions.clear();10561057return fs_changed;1058}10591060void EditorFileSystem::scan() {1061if (false /*&& bool(Globals::get_singleton()->get("debug/disable_scan"))*/) {1062return;1063}10641065if (scanning || scanning_changes || thread.is_started()) {1066return;1067}10681069// The first scan must be on the main thread because, after the first scan and update1070// of global class names, we load the plugins and autoloads. These need to1071// be added on the main thread because they are nodes, and we need to wait for them1072// to be loaded to continue the scan and reimportations.1073if (first_scan) {1074_first_scan_filesystem();1075#ifdef ANDROID_ENABLED1076// Create a .nomedia file to hide assets from media apps on Android.1077// Android 11 has some issues with nomedia files, so it's disabled there. See GH-106479 and GH-105399 for details.1078// NOTE: Nomedia file is also handled in project manager. See project_dialog.cpp -> ProjectDialog::ok_pressed().1079String sdk_version = OS::get_singleton()->get_version().get_slicec('.', 0);1080if (sdk_version != "30") {1081const String nomedia_file_path = ProjectSettings::get_singleton()->get_resource_path().path_join(".nomedia");1082if (!FileAccess::exists(nomedia_file_path)) {1083Ref<FileAccess> f = FileAccess::open(nomedia_file_path, FileAccess::WRITE);1084if (f.is_null()) {1085// .nomedia isn't so critical.1086ERR_PRINT("Couldn't create .nomedia in project path.");1087} else {1088f->close();1089}1090}1091}1092#endif1093}10941095_update_extensions();10961097if (!use_threads) {1098scanning = true;1099scan_total = 0;1100_scan_filesystem();1101if (filesystem) {1102memdelete(filesystem);1103}1104//file_type_cache.clear();1105filesystem = new_filesystem;1106new_filesystem = nullptr;1107_update_scan_actions();1108// Update all icons so they are loaded for the FileSystemDock.1109_update_files_icon_path();1110scanning = false;1111// Set first_scan to false before the signals so the function doing_first_scan can return false1112// in editor_node to start the export if needed.1113first_scan = false;1114ResourceImporter::load_on_startup = nullptr;1115emit_signal(SNAME("filesystem_changed"));1116emit_signal(SNAME("sources_changed"), sources_changed.size() > 0);1117} else {1118ERR_FAIL_COND(thread.is_started());1119set_process(true);1120Thread::Settings s;1121scanning = true;1122scan_total = 0;1123s.priority = Thread::PRIORITY_LOW;1124thread.start(_thread_func, this, s);1125}1126}11271128void EditorFileSystem::ScanProgress::increment() {1129current++;1130float ratio = current / MAX(hi, 1.0f);1131if (progress) {1132progress->step(ratio * 1000.0f);1133}1134EditorFileSystem::singleton->scan_total = ratio;1135}11361137int EditorFileSystem::_scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da) {1138List<String> dirs;1139List<String> files;11401141String cd = da->get_current_dir();11421143da->list_dir_begin();1144while (true) {1145String f = da->get_next();1146if (f.is_empty()) {1147break;1148}11491150if (da->current_is_hidden()) {1151continue;1152}11531154if (da->current_is_dir()) {1155if (f.begins_with(".")) { // Ignore special and . / ..1156continue;1157}11581159if (_should_skip_directory(cd.path_join(f))) {1160continue;1161}11621163dirs.push_back(f);11641165} else {1166files.push_back(f);1167}1168}11691170da->list_dir_end();11711172dirs.sort_custom<FileNoCaseComparator>();1173files.sort_custom<FileNoCaseComparator>();11741175int nb_files_total_scan = 0;11761177for (const String &dir : dirs) {1178if (da->change_dir(dir) == OK) {1179String d = da->get_current_dir();11801181if (d == cd || !d.begins_with(cd)) {1182da->change_dir(cd); //avoid recursion1183} else {1184ScannedDirectory *sd = memnew(ScannedDirectory);1185sd->name = dir;1186sd->full_path = p_dir->full_path.path_join(sd->name);11871188nb_files_total_scan += _scan_new_dir(sd, da);11891190p_dir->subdirs.push_back(sd);11911192da->change_dir("..");1193}1194} else {1195ERR_PRINT("Cannot go into subdir '" + dir + "'.");1196}1197}11981199p_dir->files = files;1200nb_files_total_scan += files.size();12011202return nb_files_total_scan;1203}12041205void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress, HashSet<String> *r_processed_files) {1206p_dir->modified_time = FileAccess::get_modified_time(p_scan_dir->full_path);12071208for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) {1209EditorFileSystemDirectory *sub_dir = memnew(EditorFileSystemDirectory);1210sub_dir->parent = p_dir;1211sub_dir->name = scan_sub_dir->name;1212p_dir->subdirs.push_back(sub_dir);1213_process_file_system(scan_sub_dir, sub_dir, p_progress, r_processed_files);1214}12151216for (const String &scan_file : p_scan_dir->files) {1217String ext = scan_file.get_extension().to_lower();1218if (!valid_extensions.has(ext)) {1219p_progress.increment();1220continue; //invalid1221}12221223String path = p_scan_dir->full_path.path_join(scan_file);12241225EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo);1226fi->file = scan_file;1227p_dir->files.push_back(fi);12281229if (r_processed_files) {1230r_processed_files->insert(path);1231}12321233FileCache *fc = file_cache.getptr(path);1234uint64_t mt = FileAccess::get_modified_time(path);12351236if (_can_import_file(scan_file)) {1237//is imported1238uint64_t import_mt = FileAccess::get_modified_time(path + ".import");12391240if (fc) {1241fi->type = fc->type;1242fi->resource_script_class = fc->resource_script_class;1243fi->uid = fc->uid;1244fi->deps = fc->deps;1245fi->modified_time = mt;1246fi->import_modified_time = import_mt;1247fi->import_md5 = fc->import_md5;1248fi->import_dest_paths = fc->import_dest_paths;1249fi->import_valid = fc->import_valid;1250fi->import_group_file = fc->import_group_file;1251fi->class_info = fc->class_info;12521253// Ensures backward compatibility when the project is loaded for the first time with the added import_md51254// and import_dest_paths properties in the file cache.1255if (fc->import_md5.is_empty()) {1256fi->import_md5 = FileAccess::get_md5(path + ".import");1257fi->import_dest_paths = _get_import_dest_paths(path);1258}12591260// The method _is_test_for_reimport_needed checks if the files were modified and ensures that1261// all the destination files still exist without reading the .import file.1262// If something is different, we will queue a test for reimportation that will check1263// the md5 of all files and import settings and, if necessary, execute a reimportation.1264if (_is_test_for_reimport_needed(path, fc->modification_time, mt, fc->import_modification_time, import_mt, fi->import_dest_paths) ||1265(revalidate_import_files && !ResourceFormatImporter::get_singleton()->are_import_settings_valid(path))) {1266ItemAction ia;1267ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;1268ia.dir = p_dir;1269ia.file = fi->file;1270scan_actions.push_back(ia);1271}12721273if (fc->type.is_empty()) {1274fi->type = ResourceLoader::get_resource_type(path);1275fi->resource_script_class = ResourceLoader::get_resource_script_class(path);1276fi->import_group_file = ResourceLoader::get_import_group_file(path);1277//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?)1278//note: I think this should not happen any longer..1279}12801281if (fc->uid == ResourceUID::INVALID_ID) {1282// imported files should always have a UID, so attempt to fetch it.1283fi->uid = ResourceLoader::get_resource_uid(path);1284}12851286} else {1287// Using get_resource_import_info() to prevent calling 3 times ResourceFormatImporter::_get_path_and_type.1288ResourceFormatImporter::get_singleton()->get_resource_import_info(path, fi->type, fi->uid, fi->import_group_file);1289fi->class_info = _get_global_script_class(fi->type, path);1290fi->modified_time = 0;1291fi->import_modified_time = 0;1292fi->import_md5 = FileAccess::get_md5(path + ".import");1293fi->import_dest_paths = Vector<String>();1294fi->import_valid = (fi->type == "TextFile" || fi->type == "OtherFile") ? true : ResourceLoader::is_import_valid(path);12951296ItemAction ia;1297ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;1298ia.dir = p_dir;1299ia.file = fi->file;1300scan_actions.push_back(ia);1301}1302} else {1303if (fc && fc->modification_time == mt) {1304//not imported, so just update type if changed1305fi->type = fc->type;1306fi->resource_script_class = fc->resource_script_class;1307fi->uid = fc->uid;1308fi->modified_time = mt;1309fi->deps = fc->deps;1310fi->import_modified_time = 0;1311fi->import_md5 = "";1312fi->import_dest_paths = Vector<String>();1313fi->import_valid = true;1314fi->class_info = fc->class_info;13151316if (first_scan && ClassDB::is_parent_class(fi->type, SNAME("Script"))) {1317bool update_script = false;1318String old_class_name = fi->class_info.name;1319fi->class_info = _get_global_script_class(fi->type, path);1320if (old_class_name != fi->class_info.name) {1321update_script = true;1322} else if (!fi->class_info.name.is_empty() && (!ScriptServer::is_global_class(fi->class_info.name) || ScriptServer::get_global_class_path(fi->class_info.name) != path)) {1323// This script has a class name but is not in the global class names or the path of the class has changed.1324update_script = true;1325}1326if (update_script) {1327_queue_update_script_class(path, ScriptClassInfoUpdate::from_file_info(fi));1328}1329}1330} else {1331//new or modified time1332fi->type = ResourceLoader::get_resource_type(path);1333fi->resource_script_class = ResourceLoader::get_resource_script_class(path);1334if (fi->type == "" && textfile_extensions.has(ext)) {1335fi->type = "TextFile";1336}1337if (fi->type == "" && other_file_extensions.has(ext)) {1338fi->type = "OtherFile";1339}1340fi->uid = ResourceLoader::get_resource_uid(path);1341fi->class_info = _get_global_script_class(fi->type, path);1342fi->deps = _get_dependencies(path);1343fi->modified_time = mt;1344fi->import_modified_time = 0;1345fi->import_md5 = "";1346fi->import_dest_paths = Vector<String>();1347fi->import_valid = true;13481349// Files in dep_update_list are forced for rescan to update dependencies. They don't need other updates.1350if (!dep_update_list.has(path)) {1351if (ClassDB::is_parent_class(fi->type, SNAME("Script"))) {1352_queue_update_script_class(path, ScriptClassInfoUpdate::from_file_info(fi));1353}1354if (fi->type == SNAME("PackedScene")) {1355_queue_update_scene_groups(path);1356}1357}1358}13591360if (ResourceLoader::should_create_uid_file(path)) {1361// Create a UID file and new UID, if it's invalid.1362Ref<FileAccess> f = FileAccess::open(path + ".uid", FileAccess::WRITE);1363if (f.is_valid()) {1364if (fi->uid == ResourceUID::INVALID_ID) {1365fi->uid = ResourceUID::get_singleton()->create_id_for_path(path);1366} else {1367WARN_PRINT(vformat("Missing .uid file for path \"%s\". The file was re-created from cache.", path));1368}1369f->store_line(ResourceUID::get_singleton()->id_to_text(fi->uid));1370}1371}1372}13731374if (fi->uid != ResourceUID::INVALID_ID) {1375if (ResourceUID::get_singleton()->has_id(fi->uid)) {1376// Restrict UID dupe warning to first-scan since we know there are no file moves going on yet.1377if (first_scan) {1378// Warn if we detect files with duplicate UIDs.1379const String other_path = ResourceUID::get_singleton()->get_id_path(fi->uid);1380if (other_path != path) {1381WARN_PRINT(vformat("UID duplicate detected between %s and %s.", path, other_path));1382}1383}1384ResourceUID::get_singleton()->set_id(fi->uid, path);1385} else {1386ResourceUID::get_singleton()->add_id(fi->uid, path);1387}1388}13891390p_progress.increment();1391}1392}13931394void EditorFileSystem::_process_removed_files(const HashSet<String> &p_processed_files) {1395for (const KeyValue<String, EditorFileSystem::FileCache> &kv : file_cache) {1396if (!p_processed_files.has(kv.key)) {1397if (ClassDB::is_parent_class(kv.value.type, SNAME("Script")) || ClassDB::is_parent_class(kv.value.type, SNAME("PackedScene"))) {1398// A script has been removed from disk since the last startup. The documentation needs to be updated.1399// There's no need to add the path in update_script_paths since that is exclusively for updating global class names,1400// which is handled in _first_scan_filesystem before the full scan to ensure plugins and autoloads can be created.1401MutexLock update_script_lock(update_script_mutex);1402update_script_paths_documentation.insert(kv.key);1403}1404}1405}1406}14071408void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanProgress &p_progress, bool p_recursive) {1409uint64_t current_mtime = FileAccess::get_modified_time(p_dir->get_path());14101411bool updated_dir = false;1412String cd = p_dir->get_path();1413int diff_nb_files = 0;14141415if (current_mtime != p_dir->modified_time || using_fat32_or_exfat) {1416updated_dir = true;1417p_dir->modified_time = current_mtime;1418//ooooops, dir changed, see what's going on14191420//first mark everything as verified14211422for (int i = 0; i < p_dir->files.size(); i++) {1423p_dir->files[i]->verified = false;1424}14251426for (int i = 0; i < p_dir->subdirs.size(); i++) {1427p_dir->get_subdir(i)->verified = false;1428}14291430diff_nb_files -= p_dir->files.size();14311432//then scan files and directories and check what's different14331434Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);14351436Error ret = da->change_dir(cd);1437ERR_FAIL_COND_MSG(ret != OK, "Cannot change to '" + cd + "' folder.");14381439da->list_dir_begin();1440while (true) {1441String f = da->get_next();1442if (f.is_empty()) {1443break;1444}14451446if (da->current_is_hidden()) {1447continue;1448}14491450if (da->current_is_dir()) {1451if (f.begins_with(".")) { // Ignore special and . / ..1452continue;1453}14541455int idx = p_dir->find_dir_index(f);1456if (idx == -1) {1457String dir_path = cd.path_join(f);1458if (_should_skip_directory(dir_path)) {1459continue;1460}14611462ScannedDirectory sd;1463sd.name = f;1464sd.full_path = dir_path;14651466EditorFileSystemDirectory *efd = memnew(EditorFileSystemDirectory);1467efd->parent = p_dir;1468efd->name = f;14691470Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);1471d->change_dir(dir_path);1472int nb_files_dir = _scan_new_dir(&sd, d);1473p_progress.hi += nb_files_dir;1474diff_nb_files += nb_files_dir;1475_process_file_system(&sd, efd, p_progress, nullptr);14761477ItemAction ia;1478ia.action = ItemAction::ACTION_DIR_ADD;1479ia.dir = p_dir;1480ia.file = f;1481ia.new_dir = efd;1482scan_actions.push_back(ia);1483} else {1484p_dir->subdirs[idx]->verified = true;1485}14861487} else {1488String ext = f.get_extension().to_lower();1489if (!valid_extensions.has(ext)) {1490continue; //invalid1491}14921493int idx = p_dir->find_file_index(f);14941495if (idx == -1) {1496//never seen this file, add actition to add it1497EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo);1498fi->file = f;14991500String path = cd.path_join(fi->file);1501fi->modified_time = FileAccess::get_modified_time(path);1502fi->import_modified_time = 0;1503fi->import_md5 = "";1504fi->import_dest_paths = Vector<String>();1505fi->type = ResourceLoader::get_resource_type(path);1506fi->resource_script_class = ResourceLoader::get_resource_script_class(path);1507if (fi->type == "" && textfile_extensions.has(ext)) {1508fi->type = "TextFile";1509}1510if (fi->type == "" && other_file_extensions.has(ext)) {1511fi->type = "OtherFile";1512}1513fi->class_info = _get_global_script_class(fi->type, path);1514fi->import_valid = (fi->type == "TextFile" || fi->type == "OtherFile") ? true : ResourceLoader::is_import_valid(path);1515fi->import_group_file = ResourceLoader::get_import_group_file(path);15161517{1518ItemAction ia;1519ia.action = ItemAction::ACTION_FILE_ADD;1520ia.dir = p_dir;1521ia.file = f;1522ia.new_file = fi;1523scan_actions.push_back(ia);1524}15251526if (_can_import_file(f)) {1527//if it can be imported, and it was added, it needs to be reimported1528ItemAction ia;1529ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;1530ia.dir = p_dir;1531ia.file = f;1532scan_actions.push_back(ia);1533}1534diff_nb_files++;1535} else {1536p_dir->files[idx]->verified = true;1537}1538}1539}15401541da->list_dir_end();1542}15431544for (int i = 0; i < p_dir->files.size(); i++) {1545if (updated_dir && !p_dir->files[i]->verified) {1546//this file was removed, add action to remove it1547ItemAction ia;1548ia.action = ItemAction::ACTION_FILE_REMOVE;1549ia.dir = p_dir;1550ia.file = p_dir->files[i]->file;1551scan_actions.push_back(ia);1552diff_nb_files--;1553continue;1554}15551556String path = cd.path_join(p_dir->files[i]->file);15571558if (_can_import_file(p_dir->files[i]->file)) {1559// Check here if file must be imported or not.1560// Same logic as in _process_file_system, the last modifications dates1561// needs to be trusted to prevent reading all the .import files and the md51562// each time the user switch back to Godot.1563uint64_t mt = FileAccess::get_modified_time(path);1564uint64_t import_mt = FileAccess::get_modified_time(path + ".import");1565if (_is_test_for_reimport_needed(path, p_dir->files[i]->modified_time, mt, p_dir->files[i]->import_modified_time, import_mt, p_dir->files[i]->import_dest_paths)) {1566ItemAction ia;1567ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;1568ia.dir = p_dir;1569ia.file = p_dir->files[i]->file;1570scan_actions.push_back(ia);1571}1572} else {1573uint64_t mt = FileAccess::get_modified_time(path);15741575if (mt != p_dir->files[i]->modified_time) {1576p_dir->files[i]->modified_time = mt; //save new time, but test for reload15771578ItemAction ia;1579ia.action = ItemAction::ACTION_FILE_RELOAD;1580ia.dir = p_dir;1581ia.file = p_dir->files[i]->file;1582scan_actions.push_back(ia);1583}1584}15851586p_progress.increment();1587}15881589for (int i = 0; i < p_dir->subdirs.size(); i++) {1590if ((updated_dir && !p_dir->subdirs[i]->verified) || _should_skip_directory(p_dir->subdirs[i]->get_path())) {1591// Add all the files of the folder to be sure _update_scan_actions process the removed files1592// for global class names.1593diff_nb_files += _insert_actions_delete_files_directory(p_dir->subdirs[i]);15941595//this directory was removed or ignored, add action to remove it1596ItemAction ia;1597ia.action = ItemAction::ACTION_DIR_REMOVE;1598ia.dir = p_dir->subdirs[i];1599scan_actions.push_back(ia);1600continue;1601}1602if (p_recursive) {1603_scan_fs_changes(p_dir->get_subdir(i), p_progress);1604}1605}16061607nb_files_total = MAX(nb_files_total + diff_nb_files, 0);1608}16091610void EditorFileSystem::_delete_internal_files(const String &p_file) {1611if (FileAccess::exists(p_file + ".import")) {1612List<String> paths;1613ResourceFormatImporter::get_singleton()->get_internal_resource_path_list(p_file, &paths);1614Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);1615for (const String &E : paths) {1616da->remove(E);1617}1618da->remove(p_file + ".import");1619}1620if (FileAccess::exists(p_file + ".uid")) {1621DirAccess::remove_absolute(p_file + ".uid");1622}1623}16241625int EditorFileSystem::_insert_actions_delete_files_directory(EditorFileSystemDirectory *p_dir) {1626int nb_files = 0;1627for (EditorFileSystemDirectory::FileInfo *fi : p_dir->files) {1628ItemAction ia;1629ia.action = ItemAction::ACTION_FILE_REMOVE;1630ia.dir = p_dir;1631ia.file = fi->file;1632scan_actions.push_back(ia);1633nb_files++;1634}16351636for (EditorFileSystemDirectory *sub_dir : p_dir->subdirs) {1637nb_files += _insert_actions_delete_files_directory(sub_dir);1638}16391640return nb_files;1641}16421643void EditorFileSystem::_thread_func_sources(void *_userdata) {1644EditorFileSystem *efs = (EditorFileSystem *)_userdata;1645if (efs->filesystem) {1646EditorProgressBG pr("sources", TTR("ScanSources"), 1000);1647ScanProgress sp;1648sp.progress = ≺1649sp.hi = efs->nb_files_total;1650efs->_scan_fs_changes(efs->filesystem, sp);1651}1652efs->scanning_changes_done.set();1653}16541655bool EditorFileSystem::_remove_invalid_global_class_names(const HashSet<String> &p_existing_class_names) {1656LocalVector<StringName> global_classes;1657bool must_save = false;1658ScriptServer::get_global_class_list(global_classes);1659for (const StringName &class_name : global_classes) {1660if (!p_existing_class_names.has(class_name)) {1661ScriptServer::remove_global_class(class_name);1662must_save = true;1663}1664}1665return must_save;1666}16671668String EditorFileSystem::_get_file_by_class_name(EditorFileSystemDirectory *p_dir, const String &p_class_name, EditorFileSystemDirectory::FileInfo *&r_file_info) {1669for (EditorFileSystemDirectory::FileInfo *fi : p_dir->files) {1670if (fi->class_info.name == p_class_name) {1671r_file_info = fi;1672return p_dir->get_path().path_join(fi->file);1673}1674}16751676for (EditorFileSystemDirectory *sub_dir : p_dir->subdirs) {1677String file = _get_file_by_class_name(sub_dir, p_class_name, r_file_info);1678if (!file.is_empty()) {1679return file;1680}1681}1682r_file_info = nullptr;1683return "";1684}16851686void EditorFileSystem::scan_changes() {1687if (first_scan || // Prevent a premature changes scan from inhibiting the first full scan1688scanning || scanning_changes || thread.is_started()) {1689scan_changes_pending = true;1690set_process(true);1691return;1692}16931694_update_extensions();1695sources_changed.clear();1696scanning_changes = true;1697scanning_changes_done.clear();16981699if (!use_threads) {1700if (filesystem) {1701EditorProgressBG pr("sources", TTR("ScanSources"), 1000);1702ScanProgress sp;1703sp.progress = ≺1704sp.hi = nb_files_total;1705scan_total = 0;1706_scan_fs_changes(filesystem, sp);1707if (_update_scan_actions()) {1708emit_signal(SNAME("filesystem_changed"));1709}1710}1711scanning_changes = false;1712scanning_changes_done.set();1713emit_signal(SNAME("sources_changed"), sources_changed.size() > 0);1714} else {1715ERR_FAIL_COND(thread_sources.is_started());1716set_process(true);1717scan_total = 0;1718Thread::Settings s;1719s.priority = Thread::PRIORITY_LOW;1720thread_sources.start(_thread_func_sources, this, s);1721}1722}17231724void EditorFileSystem::_notification(int p_what) {1725switch (p_what) {1726case NOTIFICATION_EXIT_TREE: {1727Thread &active_thread = thread.is_started() ? thread : thread_sources;1728if (use_threads && active_thread.is_started()) {1729while (scanning) {1730OS::get_singleton()->delay_usec(1000);1731}1732active_thread.wait_to_finish();1733WARN_PRINT("Scan thread aborted...");1734set_process(false);1735}17361737if (filesystem) {1738memdelete(filesystem);1739}1740if (new_filesystem) {1741memdelete(new_filesystem);1742}1743filesystem = nullptr;1744new_filesystem = nullptr;1745} break;17461747case NOTIFICATION_PROCESS: {1748if (use_threads) {1749/** This hack exists because of the EditorProgress nature1750* of processing events recursively. This needs to be rewritten1751* at some point entirely, but in the meantime the following1752* hack prevents deadlock on import.1753*/17541755static bool prevent_recursive_process_hack = false;1756if (prevent_recursive_process_hack) {1757break;1758}17591760prevent_recursive_process_hack = true;17611762bool done_importing = false;17631764if (scanning_changes) {1765if (scanning_changes_done.is_set()) {1766set_process(false);17671768if (thread_sources.is_started()) {1769thread_sources.wait_to_finish();1770}1771bool changed = _update_scan_actions();1772// Set first_scan to false before the signals so the function doing_first_scan can return false1773// in editor_node to start the export if needed.1774first_scan = false;1775scanning_changes = false;1776done_importing = true;1777ResourceImporter::load_on_startup = nullptr;1778if (changed) {1779emit_signal(SNAME("filesystem_changed"));1780}1781emit_signal(SNAME("sources_changed"), sources_changed.size() > 0);1782}1783} else if (!scanning && thread.is_started()) {1784set_process(false);17851786if (filesystem) {1787memdelete(filesystem);1788}1789filesystem = new_filesystem;1790new_filesystem = nullptr;1791thread.wait_to_finish();1792_update_scan_actions();1793// Update all icons so they are loaded for the FileSystemDock.1794_update_files_icon_path();1795// Set first_scan to false before the signals so the function doing_first_scan can return false1796// in editor_node to start the export if needed.1797first_scan = false;1798ResourceImporter::load_on_startup = nullptr;1799emit_signal(SNAME("filesystem_changed"));1800emit_signal(SNAME("sources_changed"), sources_changed.size() > 0);1801}18021803if (done_importing && scan_changes_pending) {1804scan_changes_pending = false;1805scan_changes();1806}18071808prevent_recursive_process_hack = false;1809}1810} break;1811}1812}18131814bool EditorFileSystem::is_scanning() const {1815return scanning || scanning_changes || first_scan;1816}18171818float EditorFileSystem::get_scanning_progress() const {1819return scan_total;1820}18211822EditorFileSystemDirectory *EditorFileSystem::get_filesystem() {1823return filesystem;1824}18251826void EditorFileSystem::_save_filesystem_cache(EditorFileSystemDirectory *p_dir, Ref<FileAccess> p_file) {1827if (!p_dir) {1828return; //none1829}1830p_file->store_line("::" + p_dir->get_path() + "::" + String::num_int64(p_dir->modified_time));18311832for (int i = 0; i < p_dir->files.size(); i++) {1833const EditorFileSystemDirectory::FileInfo *file_info = p_dir->files[i];1834if (!file_info->import_group_file.is_empty()) {1835group_file_cache.insert(file_info->import_group_file);1836}18371838String type = file_info->type;1839if (file_info->resource_script_class) {1840type += "/" + String(file_info->resource_script_class);1841}18421843PackedStringArray cache_string;1844cache_string.append(file_info->file);1845cache_string.append(type);1846cache_string.append(itos(file_info->uid));1847cache_string.append(itos(file_info->modified_time));1848cache_string.append(itos(file_info->import_modified_time));1849cache_string.append(itos(file_info->import_valid));1850cache_string.append(file_info->import_group_file);1851cache_string.append(String("<>").join({ file_info->class_info.name, file_info->class_info.extends, file_info->class_info.icon_path, itos(file_info->class_info.is_abstract), itos(file_info->class_info.is_tool), file_info->import_md5, String("<*>").join(file_info->import_dest_paths) }));1852cache_string.append(String("<>").join(file_info->deps));18531854p_file->store_line(String("::").join(cache_string));1855}18561857for (int i = 0; i < p_dir->subdirs.size(); i++) {1858_save_filesystem_cache(p_dir->subdirs[i], p_file);1859}1860}18611862bool EditorFileSystem::_find_file(const String &p_file, EditorFileSystemDirectory **r_d, int &r_file_pos) const {1863//todo make faster18641865if (!filesystem || scanning) {1866return false;1867}18681869String f = ProjectSettings::get_singleton()->localize_path(p_file);18701871// Note: Only checks if base directory is case sensitive.1872Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);1873bool fs_case_sensitive = dir->is_case_sensitive("res://");18741875if (!f.begins_with("res://")) {1876return false;1877}1878f = f.substr(6);1879f = f.replace_char('\\', '/');18801881Vector<String> path = f.split("/");18821883if (path.is_empty()) {1884return false;1885}1886String file = path[path.size() - 1];1887path.resize(path.size() - 1);18881889EditorFileSystemDirectory *fs = filesystem;18901891for (int i = 0; i < path.size(); i++) {1892if (path[i].begins_with(".")) {1893return false;1894}18951896int idx = -1;1897for (int j = 0; j < fs->get_subdir_count(); j++) {1898if (fs_case_sensitive) {1899if (fs->get_subdir(j)->get_name() == path[i]) {1900idx = j;1901break;1902}1903} else {1904if (fs->get_subdir(j)->get_name().to_lower() == path[i].to_lower()) {1905idx = j;1906break;1907}1908}1909}19101911if (idx == -1) {1912// Only create a missing directory in memory when it exists on disk.1913if (!dir->dir_exists(fs->get_path().path_join(path[i]))) {1914return false;1915}1916EditorFileSystemDirectory *efsd = memnew(EditorFileSystemDirectory);19171918efsd->name = path[i];1919efsd->parent = fs;19201921int idx2 = 0;1922for (int j = 0; j < fs->get_subdir_count(); j++) {1923if (efsd->name.filenocasecmp_to(fs->get_subdir(j)->get_name()) < 0) {1924break;1925}1926idx2++;1927}19281929if (idx2 == fs->get_subdir_count()) {1930fs->subdirs.push_back(efsd);1931} else {1932fs->subdirs.insert(idx2, efsd);1933}1934fs = efsd;1935} else {1936fs = fs->get_subdir(idx);1937}1938}19391940int cpos = -1;1941for (int i = 0; i < fs->files.size(); i++) {1942if (fs_case_sensitive) {1943if (fs->files[i]->file == file) {1944cpos = i;1945break;1946}1947} else {1948if (fs->files[i]->file.to_lower() == file.to_lower()) {1949cpos = i;1950break;1951}1952}1953}19541955r_file_pos = cpos;1956*r_d = fs;19571958return cpos != -1;1959}19601961String EditorFileSystem::get_file_type(const String &p_file) const {1962EditorFileSystemDirectory *fs = nullptr;1963int cpos = -1;19641965if (!_find_file(p_file, &fs, cpos)) {1966return "";1967}19681969return fs->files[cpos]->type;1970}19711972EditorFileSystemDirectory *EditorFileSystem::find_file(const String &p_file, int *r_index) const {1973if (!filesystem || scanning) {1974return nullptr;1975}19761977EditorFileSystemDirectory *fs = nullptr;1978int cpos = -1;1979if (!_find_file(p_file, &fs, cpos)) {1980return nullptr;1981}19821983if (r_index) {1984*r_index = cpos;1985}19861987return fs;1988}19891990ResourceUID::ID EditorFileSystem::get_file_uid(const String &p_path) const {1991int file_idx;1992EditorFileSystemDirectory *directory = find_file(p_path, &file_idx);19931994if (!directory) {1995return ResourceUID::INVALID_ID;1996}1997return directory->files[file_idx]->uid;1998}19992000EditorFileSystemDirectory *EditorFileSystem::get_filesystem_path(const String &p_path) {2001if (!filesystem || scanning) {2002return nullptr;2003}20042005String f = ProjectSettings::get_singleton()->localize_path(p_path);20062007if (!f.begins_with("res://")) {2008return nullptr;2009}20102011f = f.substr(6);2012f = f.replace_char('\\', '/');2013if (f.is_empty()) {2014return filesystem;2015}20162017if (f.ends_with("/")) {2018f = f.substr(0, f.length() - 1);2019}20202021Vector<String> path = f.split("/");20222023if (path.is_empty()) {2024return nullptr;2025}20262027EditorFileSystemDirectory *fs = filesystem;20282029for (int i = 0; i < path.size(); i++) {2030int idx = -1;2031for (int j = 0; j < fs->get_subdir_count(); j++) {2032if (fs->get_subdir(j)->get_name() == path[i]) {2033idx = j;2034break;2035}2036}20372038if (idx == -1) {2039return nullptr;2040} else {2041fs = fs->get_subdir(idx);2042}2043}20442045return fs;2046}20472048void EditorFileSystem::_save_late_updated_files() {2049//files that already existed, and were modified, need re-scanning for dependencies upon project restart. This is done via saving this special file2050String fscache = EditorPaths::get_singleton()->get_project_settings_dir().path_join("filesystem_update4");2051Ref<FileAccess> f = FileAccess::open(fscache, FileAccess::WRITE);2052ERR_FAIL_COND_MSG(f.is_null(), "Cannot create file '" + fscache + "'. Check user write permissions.");2053for (const String &E : late_update_files) {2054f->store_line(E);2055}2056}20572058Vector<String> EditorFileSystem::_get_dependencies(const String &p_path) {2059// Avoid error spam on first opening of a not yet imported project by treating the following situation2060// as a benign one, not letting the file open error happen: the resource is of an importable type but2061// it has not been imported yet.2062if (ResourceFormatImporter::get_singleton()->recognize_path(p_path)) {2063const String &internal_path = ResourceFormatImporter::get_singleton()->get_internal_resource_path(p_path);2064if (!internal_path.is_empty() && !FileAccess::exists(internal_path)) { // If path is empty (error), keep the code flow to the error.2065return Vector<String>();2066}2067}20682069List<String> deps;2070ResourceLoader::get_dependencies(p_path, &deps);20712072Vector<String> ret;2073for (const String &E : deps) {2074ret.push_back(E);2075}20762077return ret;2078}20792080EditorFileSystem::ScriptClassInfo EditorFileSystem::_get_global_script_class(const String &p_type, const String &p_path) const {2081ScriptClassInfo info;2082for (int i = 0; i < ScriptServer::get_language_count(); i++) {2083if (ScriptServer::get_language(i)->handles_global_class_type(p_type)) {2084info.name = ScriptServer::get_language(i)->get_global_class_name(p_path, &info.extends, &info.icon_path, &info.is_abstract, &info.is_tool);2085break;2086}2087}2088return info;2089}20902091void EditorFileSystem::_update_file_icon_path(EditorFileSystemDirectory::FileInfo *file_info) {2092String icon_path;2093if (file_info->resource_script_class != StringName()) {2094icon_path = EditorNode::get_editor_data().script_class_get_icon_path(file_info->resource_script_class);2095} else if (file_info->class_info.icon_path.is_empty() && !file_info->deps.is_empty()) {2096const String &script_dep = file_info->deps[0]; // Assuming the first dependency is a script.2097const String &script_path = script_dep.contains("::") ? script_dep.get_slice("::", 2) : script_dep;2098if (!script_path.is_empty()) {2099String *cached = file_icon_cache.getptr(script_path);2100if (cached) {2101icon_path = *cached;2102} else {2103if (ClassDB::is_parent_class(ResourceLoader::get_resource_type(script_path), SNAME("Script"))) {2104int script_file;2105EditorFileSystemDirectory *efsd = find_file(script_path, &script_file);2106if (efsd) {2107icon_path = efsd->files[script_file]->class_info.icon_path;2108}2109}2110file_icon_cache.insert(script_path, icon_path);2111}2112}2113}21142115if (icon_path.is_empty() && !file_info->type.is_empty()) {2116Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(file_info->type);2117if (icon.is_valid()) {2118icon_path = icon->get_path();2119}2120}21212122file_info->class_info.icon_path = icon_path;2123}21242125void EditorFileSystem::_update_files_icon_path(EditorFileSystemDirectory *edp) {2126if (!edp) {2127edp = filesystem;2128file_icon_cache.clear();2129}2130for (EditorFileSystemDirectory *sub_dir : edp->subdirs) {2131_update_files_icon_path(sub_dir);2132}2133for (EditorFileSystemDirectory::FileInfo *fi : edp->files) {2134_update_file_icon_path(fi);2135}2136}21372138void EditorFileSystem::_update_script_classes() {2139if (update_script_paths.is_empty()) {2140// Ensure the global class file is always present; it's essential for exports to work.2141if (!FileAccess::exists(ProjectSettings::get_singleton()->get_global_class_list_path())) {2142ScriptServer::save_global_classes();2143}2144return;2145}21462147{2148MutexLock update_script_lock(update_script_mutex);21492150EditorProgress *ep = nullptr;2151if (update_script_paths.size() > 1) {2152if (MessageQueue::get_singleton()->is_flushing()) {2153// Use background progress when message queue is flushing.2154ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size(), false, true));2155} else {2156ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size()));2157}2158}21592160int step_count = 0;2161for (const KeyValue<String, ScriptClassInfoUpdate> &E : update_script_paths) {2162_register_global_class_script(E.key, E.key, E.value);2163if (ep) {2164ep->step(E.value.name, step_count++, false);2165}2166}21672168memdelete_notnull(ep);21692170update_script_paths.clear();2171}21722173EditorNode::get_editor_data().script_class_save_global_classes();21742175emit_signal("script_classes_updated");21762177// Rescan custom loaders and savers.2178// Doing the following here because the `filesystem_changed` signal fires multiple times and isn't always followed by script classes update.2179// So I thought it's better to do this when script classes really get updated2180ResourceLoader::remove_custom_loaders();2181ResourceLoader::add_custom_loaders();2182ResourceSaver::remove_custom_savers();2183ResourceSaver::add_custom_savers();2184}21852186void EditorFileSystem::_update_script_documentation() {2187if (update_script_paths_documentation.is_empty()) {2188return;2189}21902191MutexLock update_script_lock(update_script_mutex);21922193EditorProgress *ep = nullptr;2194if (update_script_paths_documentation.size() > 1) {2195if (MessageQueue::get_singleton()->is_flushing()) {2196// Use background progress when message queue is flushing.2197ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size(), false, true));2198} else {2199ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size()));2200}2201}22022203int step_count = 0;2204for (const String &path : update_script_paths_documentation) {2205int index = -1;2206EditorFileSystemDirectory *efd = find_file(path, &index);22072208if (!efd || index < 0) {2209// The file was removed2210EditorHelp::remove_script_doc_by_path(path);2211continue;2212}22132214if (path.ends_with(".tscn")) {2215Ref<PackedScene> packed_scene = ResourceLoader::load(path);2216if (packed_scene.is_valid()) {2217Ref<SceneState> state = packed_scene->get_state();2218if (state.is_valid()) {2219Vector<Ref<Resource>> sub_resources = state->get_sub_resources();2220for (Ref<Resource> sub_resource : sub_resources) {2221Ref<Script> scr = sub_resource;2222if (scr.is_valid()) {2223for (const DocData::ClassDoc &cd : scr->get_documentation()) {2224EditorHelp::add_doc(cd);2225if (!first_scan) {2226// Update the documentation in the Script Editor if it is open.2227ScriptEditor::get_singleton()->update_doc(cd.name);2228}2229}2230}2231}2232}2233}2234continue;2235}22362237for (int i = 0; i < ScriptServer::get_language_count(); i++) {2238ScriptLanguage *lang = ScriptServer::get_language(i);2239if (lang->supports_documentation() && efd->files[index]->type == lang->get_type()) {2240bool should_reload_script = _should_reload_script(path);2241Ref<Script> scr = ResourceLoader::load(path);2242if (scr.is_null()) {2243continue;2244}2245if (should_reload_script) {2246// Reloading the script from disk. Otherwise, the ResourceLoader::load will2247// return the last loaded version of the script (without the modifications).2248scr->reload_from_file();2249}2250for (const DocData::ClassDoc &cd : scr->get_documentation()) {2251EditorHelp::add_doc(cd);2252if (!first_scan) {2253// Update the documentation in the Script Editor if it is open.2254ScriptEditor::get_singleton()->update_doc(cd.name);2255}2256}2257}2258}22592260if (ep) {2261ep->step(efd->files[index]->file, step_count++, false);2262}2263}22642265memdelete_notnull(ep);22662267update_script_paths_documentation.clear();2268}22692270bool EditorFileSystem::_should_reload_script(const String &p_path) {2271if (first_scan) {2272return false;2273}22742275Ref<Script> scr = ResourceCache::get_ref(p_path);2276if (scr.is_null()) {2277// Not a script or not already loaded.2278return false;2279}22802281// Scripts are reloaded via the script editor if they are currently opened.2282if (ScriptEditor::get_singleton()->get_open_scripts().has(scr)) {2283return false;2284}22852286return true;2287}22882289void EditorFileSystem::_process_update_pending() {2290_update_script_classes();2291// Parse documentation second, as it requires the class names to be loaded2292// because _update_script_documentation loads the scripts completely.2293if (!EditorNode::is_cmdline_mode()) {2294_update_script_documentation();2295_update_pending_scene_groups();2296}2297}22982299void EditorFileSystem::_queue_update_script_class(const String &p_path, const ScriptClassInfoUpdate &p_script_update) {2300MutexLock update_script_lock(update_script_mutex);23012302update_script_paths.insert(p_path, p_script_update);2303update_script_paths_documentation.insert(p_path);2304}23052306void EditorFileSystem::_update_scene_groups() {2307if (update_scene_paths.is_empty()) {2308return;2309}23102311EditorProgress *ep = nullptr;2312if (update_scene_paths.size() > 20) {2313ep = memnew(EditorProgress("update_scene_groups", TTR("Updating Scene Groups"), update_scene_paths.size()));2314}2315int step_count = 0;23162317{2318MutexLock update_scene_lock(update_scene_mutex);2319for (const String &path : update_scene_paths) {2320ProjectSettings::get_singleton()->remove_scene_groups_cache(path);23212322int index = -1;2323EditorFileSystemDirectory *efd = find_file(path, &index);23242325if (!efd || index < 0) {2326// The file was removed.2327continue;2328}23292330const HashSet<StringName> scene_groups = PackedScene::get_scene_groups(path);2331if (!scene_groups.is_empty()) {2332ProjectSettings::get_singleton()->add_scene_groups_cache(path, scene_groups);2333}23342335if (ep) {2336ep->step(efd->files[index]->file, step_count++, false);2337}2338}23392340memdelete_notnull(ep);2341update_scene_paths.clear();2342}23432344ProjectSettings::get_singleton()->save_scene_groups_cache();2345}23462347void EditorFileSystem::_update_pending_scene_groups() {2348if (!FileAccess::exists(ProjectSettings::get_singleton()->get_scene_groups_cache_path())) {2349_get_all_scenes(get_filesystem(), update_scene_paths);2350_update_scene_groups();2351} else if (!update_scene_paths.is_empty()) {2352_update_scene_groups();2353}2354}23552356void EditorFileSystem::_queue_update_scene_groups(const String &p_path) {2357MutexLock update_scene_lock(update_scene_mutex);2358update_scene_paths.insert(p_path);2359}23602361void EditorFileSystem::_get_all_scenes(EditorFileSystemDirectory *p_dir, HashSet<String> &r_list) {2362for (int i = 0; i < p_dir->get_file_count(); i++) {2363if (p_dir->get_file_type(i) == SNAME("PackedScene")) {2364r_list.insert(p_dir->get_file_path(i));2365}2366}23672368for (int i = 0; i < p_dir->get_subdir_count(); i++) {2369_get_all_scenes(p_dir->get_subdir(i), r_list);2370}2371}23722373void EditorFileSystem::update_file(const String &p_file) {2374ERR_FAIL_COND(p_file.is_empty());2375update_files({ p_file });2376}23772378void EditorFileSystem::update_files(const Vector<String> &p_script_paths) {2379bool updated = false;2380bool update_files_icon_cache = false;2381Vector<EditorFileSystemDirectory::FileInfo *> files_to_update_icon_path;2382for (const String &file : p_script_paths) {2383ERR_CONTINUE(file.is_empty());2384EditorFileSystemDirectory *fs = nullptr;2385int cpos = -1;23862387if (!_find_file(file, &fs, cpos)) {2388if (!fs) {2389continue;2390}2391}23922393if (!FileAccess::exists(file)) {2394//was removed2395_delete_internal_files(file);2396if (cpos != -1) { // Might've never been part of the editor file system (*.* files deleted in Open dialog).2397if (fs->files[cpos]->uid != ResourceUID::INVALID_ID) {2398if (ResourceUID::get_singleton()->has_id(fs->files[cpos]->uid)) {2399ResourceUID::get_singleton()->remove_id(fs->files[cpos]->uid);2400}2401}2402if (ClassDB::is_parent_class(fs->files[cpos]->type, SNAME("Script"))) {2403ScriptClassInfoUpdate update;2404update.type = fs->files[cpos]->type;2405_queue_update_script_class(file, update);2406if (!fs->files[cpos]->class_info.icon_path.is_empty()) {2407update_files_icon_cache = true;2408}2409}2410if (fs->files[cpos]->type == SNAME("PackedScene")) {2411_queue_update_scene_groups(file);2412}24132414memdelete(fs->files[cpos]);2415fs->files.remove_at(cpos);2416updated = true;2417}2418} else {2419String type = ResourceLoader::get_resource_type(file);2420if (type.is_empty() && textfile_extensions.has(file.get_extension())) {2421type = "TextFile";2422}2423if (type.is_empty() && other_file_extensions.has(file.get_extension())) {2424type = "OtherFile";2425}2426String script_class = ResourceLoader::get_resource_script_class(file);24272428ResourceUID::ID uid = ResourceLoader::get_resource_uid(file);24292430if (cpos == -1) {2431// The file did not exist, it was added.2432int idx = 0;2433String file_name = file.get_file();24342435for (int i = 0; i < fs->files.size(); i++) {2436if (file.filenocasecmp_to(fs->files[i]->file) < 0) {2437break;2438}2439idx++;2440}24412442EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo);2443fi->file = file_name;2444fi->import_modified_time = 0;2445fi->import_valid = (type == "TextFile" || type == "OtherFile") ? true : ResourceLoader::is_import_valid(file);2446fi->import_md5 = "";2447fi->import_dest_paths = Vector<String>();24482449if (idx == fs->files.size()) {2450fs->files.push_back(fi);2451} else {2452fs->files.insert(idx, fi);2453}2454cpos = idx;2455} else {2456//the file exists and it was updated, and was not added in this step.2457//this means we must force upon next restart to scan it again, to get proper type and dependencies2458late_update_files.insert(file);2459_save_late_updated_files(); //files need to be updated in the re-scan2460}24612462EditorFileSystemDirectory::FileInfo *fi = fs->files[cpos];2463const String old_script_class_icon_path = fi->class_info.icon_path;2464const String old_class_name = fi->class_info.name;2465fi->type = type;2466fi->resource_script_class = script_class;2467fi->uid = uid;2468fi->class_info = _get_global_script_class(type, file);2469fi->import_group_file = ResourceLoader::get_import_group_file(file);2470fi->modified_time = FileAccess::get_modified_time(file);2471fi->deps = _get_dependencies(file);2472fi->import_valid = (type == "TextFile" || type == "OtherFile") ? true : ResourceLoader::is_import_valid(file);24732474if (uid != ResourceUID::INVALID_ID) {2475if (ResourceUID::get_singleton()->has_id(uid)) {2476ResourceUID::get_singleton()->set_id(uid, file);2477} else {2478ResourceUID::get_singleton()->add_id(uid, file);2479}24802481ResourceUID::get_singleton()->update_cache();2482} else {2483if (ResourceLoader::should_create_uid_file(file)) {2484Ref<FileAccess> f = FileAccess::open(file + ".uid", FileAccess::WRITE);2485if (f.is_valid()) {2486const ResourceUID::ID id = ResourceUID::get_singleton()->create_id_for_path(file);2487ResourceUID::get_singleton()->add_id(id, file);2488f->store_line(ResourceUID::get_singleton()->id_to_text(id));2489fi->uid = id;2490}2491}2492}24932494// Update preview2495EditorResourcePreview::get_singleton()->check_for_invalidation(file);24962497if (ClassDB::is_parent_class(fi->type, SNAME("Script"))) {2498_queue_update_script_class(file, ScriptClassInfoUpdate::from_file_info(fi));2499}2500if (fi->type == SNAME("PackedScene")) {2501_queue_update_scene_groups(file);2502}25032504if (ClassDB::is_parent_class(fi->type, SNAME("Resource"))) {2505files_to_update_icon_path.push_back(fi);2506} else if (old_script_class_icon_path != fi->class_info.icon_path) {2507update_files_icon_cache = true;2508}25092510// Restore another script as the global class name if multiple scripts had the same old class name.2511if (!old_class_name.is_empty() && fi->class_info.name != old_class_name && ClassDB::is_parent_class(type, SNAME("Script"))) {2512EditorFileSystemDirectory::FileInfo *old_fi = nullptr;2513String old_file = _get_file_by_class_name(filesystem, old_class_name, old_fi);2514if (!old_file.is_empty() && old_fi) {2515_queue_update_script_class(old_file, ScriptClassInfoUpdate::from_file_info(old_fi));2516}2517}2518updated = true;2519}2520}25212522if (updated) {2523if (update_files_icon_cache) {2524_update_files_icon_path();2525} else {2526for (EditorFileSystemDirectory::FileInfo *fi : files_to_update_icon_path) {2527_update_file_icon_path(fi);2528}2529}2530if (!is_scanning()) {2531_process_update_pending();2532}2533if (!filesystem_changed_queued) {2534filesystem_changed_queued = true;2535callable_mp(this, &EditorFileSystem::_notify_filesystem_changed).call_deferred();2536}2537}2538}25392540void EditorFileSystem::_notify_filesystem_changed() {2541emit_signal("filesystem_changed");2542filesystem_changed_queued = false;2543}25442545HashSet<String> EditorFileSystem::get_valid_extensions() const {2546return valid_extensions;2547}25482549void EditorFileSystem::_register_global_class_script(const String &p_search_path, const String &p_target_path, const ScriptClassInfoUpdate &p_script_update) {2550ScriptServer::remove_global_class_by_path(p_search_path); // First remove, just in case it changed25512552if (p_script_update.name.is_empty()) {2553return;2554}25552556String lang;2557for (int j = 0; j < ScriptServer::get_language_count(); j++) {2558if (ScriptServer::get_language(j)->handles_global_class_type(p_script_update.type)) {2559lang = ScriptServer::get_language(j)->get_name();2560break;2561}2562}2563if (lang.is_empty()) {2564return; // No lang found that can handle this global class2565}25662567ScriptServer::add_global_class(p_script_update.name, p_script_update.extends, lang, p_target_path, p_script_update.is_abstract, p_script_update.is_tool);2568EditorNode::get_editor_data().script_class_set_icon_path(p_script_update.name, p_script_update.icon_path);2569EditorNode::get_editor_data().script_class_set_name(p_target_path, p_script_update.name);2570}25712572void EditorFileSystem::register_global_class_script(const String &p_search_path, const String &p_target_path) {2573int index_file;2574EditorFileSystemDirectory *efsd = find_file(p_search_path, &index_file);2575if (efsd) {2576const EditorFileSystemDirectory::FileInfo *fi = efsd->files[index_file];2577EditorFileSystem::get_singleton()->_register_global_class_script(p_search_path, p_target_path, ScriptClassInfoUpdate::from_file_info(fi));2578} else {2579ScriptServer::remove_global_class_by_path(p_search_path);2580}2581}25822583Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector<String> &p_files) {2584String importer_name;25852586HashMap<String, HashMap<StringName, Variant>> source_file_options;2587HashMap<String, ResourceUID::ID> uids;2588HashMap<String, String> base_paths;2589for (int i = 0; i < p_files.size(); i++) {2590Ref<ConfigFile> config;2591config.instantiate();2592Error err = config->load(p_files[i] + ".import");2593ERR_CONTINUE(err != OK);2594ERR_CONTINUE(!config->has_section_key("remap", "importer"));2595String file_importer_name = config->get_value("remap", "importer");2596ERR_CONTINUE(file_importer_name.is_empty());25972598if (!importer_name.is_empty() && importer_name != file_importer_name) {2599EditorNode::get_singleton()->show_warning(vformat(TTR("There are multiple importers for different types pointing to file %s, import aborted"), p_group_file));2600ERR_FAIL_V(ERR_FILE_CORRUPT);2601}26022603ResourceUID::ID uid = ResourceUID::INVALID_ID;26042605if (config->has_section_key("remap", "uid")) {2606String uidt = config->get_value("remap", "uid");2607uid = ResourceUID::get_singleton()->text_to_id(uidt);2608}26092610uids[p_files[i]] = uid;26112612source_file_options[p_files[i]] = HashMap<StringName, Variant>();2613importer_name = file_importer_name;26142615if (importer_name == "keep" || importer_name == "skip") {2616continue; //do nothing2617}26182619Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(importer_name);2620ERR_FAIL_COND_V(importer.is_null(), ERR_FILE_CORRUPT);2621List<ResourceImporter::ImportOption> options;2622importer->get_import_options(p_files[i], &options);2623//set default values2624for (const ResourceImporter::ImportOption &E : options) {2625source_file_options[p_files[i]][E.option.name] = E.default_value;2626}26272628if (config->has_section("params")) {2629Vector<String> sk = config->get_section_keys("params");2630for (const String ¶m : sk) {2631Variant value = config->get_value("params", param);2632//override with whatever is in file2633source_file_options[p_files[i]][param] = value;2634}2635}26362637base_paths[p_files[i]] = ResourceFormatImporter::get_singleton()->get_import_base_path(p_files[i]);2638}26392640if (importer_name == "keep" || importer_name == "skip") {2641return OK; // (do nothing)2642}26432644ERR_FAIL_COND_V(importer_name.is_empty(), ERR_UNCONFIGURED);26452646Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(importer_name);26472648Error err = importer->import_group_file(p_group_file, source_file_options, base_paths);26492650//all went well, overwrite config files with proper remaps and md5s2651for (const KeyValue<String, HashMap<StringName, Variant>> &E : source_file_options) {2652const String &file = E.key;2653String base_path = ResourceFormatImporter::get_singleton()->get_import_base_path(file);2654Vector<String> dest_paths;2655ResourceUID::ID uid = uids[file];2656{2657Ref<FileAccess> f = FileAccess::open(file + ".import", FileAccess::WRITE);2658ERR_FAIL_COND_V_MSG(f.is_null(), ERR_FILE_CANT_OPEN, "Cannot open import file '" + file + ".import'.");26592660//write manually, as order matters ([remap] has to go first for performance).2661f->store_line("[remap]");2662f->store_line("");2663f->store_line("importer=\"" + importer->get_importer_name() + "\"");2664int version = importer->get_format_version();2665if (version > 0) {2666f->store_line("importer_version=" + itos(version));2667}2668if (!importer->get_resource_type().is_empty()) {2669f->store_line("type=\"" + importer->get_resource_type() + "\"");2670}26712672if (uid == ResourceUID::INVALID_ID) {2673uid = ResourceUID::get_singleton()->create_id_for_path(file);2674}26752676f->store_line("uid=\"" + ResourceUID::get_singleton()->id_to_text(uid) + "\""); // Store in readable format.26772678if (err == OK) {2679String path = base_path + "." + importer->get_save_extension();2680f->store_line("path=\"" + path + "\"");2681dest_paths.push_back(path);2682}26832684f->store_line("group_file=" + Variant(p_group_file).get_construct_string());26852686if (err == OK) {2687f->store_line("valid=true");2688} else {2689f->store_line("valid=false");2690}2691f->store_line("[deps]\n");26922693f->store_line("");26942695f->store_line("source_file=" + Variant(file).get_construct_string());2696if (dest_paths.size()) {2697Array dp;2698for (int i = 0; i < dest_paths.size(); i++) {2699dp.push_back(dest_paths[i]);2700}2701f->store_line("dest_files=" + Variant(dp).get_construct_string() + "\n");2702}2703f->store_line("[params]");2704f->store_line("");27052706//store options in provided order, to avoid file changing. Order is also important because first match is accepted first.27072708List<ResourceImporter::ImportOption> options;2709importer->get_import_options(file, &options);2710//set default values2711for (const ResourceImporter::ImportOption &F : options) {2712String base = F.option.name;2713Variant v = F.default_value;2714if (source_file_options[file].has(base)) {2715v = source_file_options[file][base];2716}2717String value;2718VariantWriter::write_to_string(v, value);2719f->store_line(base + "=" + value);2720}2721}27222723// Store the md5's of the various files. These are stored separately so that the .import files can be version controlled.2724{2725Ref<FileAccess> md5s = FileAccess::open(base_path + ".md5", FileAccess::WRITE);2726ERR_FAIL_COND_V_MSG(md5s.is_null(), ERR_FILE_CANT_OPEN, "Cannot open MD5 file '" + base_path + ".md5'.");27272728md5s->store_line("source_md5=\"" + FileAccess::get_md5(file) + "\"");2729if (dest_paths.size()) {2730md5s->store_line("dest_md5=\"" + FileAccess::get_multiple_md5(dest_paths) + "\"\n");2731}2732}27332734EditorFileSystemDirectory *fs = nullptr;2735int cpos = -1;2736bool found = _find_file(file, &fs, cpos);2737ERR_FAIL_COND_V_MSG(!found, ERR_UNCONFIGURED, vformat("Can't find file '%s' during group reimport.", file));27382739//update modified times, to avoid reimport2740fs->files[cpos]->modified_time = FileAccess::get_modified_time(file);2741fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(file + ".import");2742fs->files[cpos]->import_md5 = FileAccess::get_md5(file + ".import");2743fs->files[cpos]->import_dest_paths = dest_paths;2744fs->files[cpos]->deps = _get_dependencies(file);2745fs->files[cpos]->uid = uid;2746fs->files[cpos]->type = importer->get_resource_type();2747if (fs->files[cpos]->type == "" && textfile_extensions.has(file.get_extension())) {2748fs->files[cpos]->type = "TextFile";2749}2750if (fs->files[cpos]->type == "" && other_file_extensions.has(file.get_extension())) {2751fs->files[cpos]->type = "OtherFile";2752}2753fs->files[cpos]->import_valid = err == OK;27542755if (ResourceUID::get_singleton()->has_id(uid)) {2756ResourceUID::get_singleton()->set_id(uid, file);2757} else {2758ResourceUID::get_singleton()->add_id(uid, file);2759}27602761//if file is currently up, maybe the source it was loaded from changed, so import math must be updated for it2762//to reload properly2763Ref<Resource> r = ResourceCache::get_ref(file);27642765if (r.is_valid()) {2766if (!r->get_import_path().is_empty()) {2767String dst_path = ResourceFormatImporter::get_singleton()->get_internal_resource_path(file);2768r->set_import_path(dst_path);2769r->set_import_last_modified_time(0);2770}2771}27722773EditorResourcePreview::get_singleton()->check_for_invalidation(file);2774}27752776return err;2777}27782779Error EditorFileSystem::_reimport_file(const String &p_file, const HashMap<StringName, Variant> &p_custom_options, const String &p_custom_importer, Variant *p_generator_parameters, bool p_update_file_system) {2780print_verbose(vformat("EditorFileSystem: Importing file: %s", p_file));2781uint64_t start_time = OS::get_singleton()->get_ticks_msec();27822783EditorFileSystemDirectory *fs = nullptr;2784int cpos = -1;2785if (p_update_file_system) {2786bool found = _find_file(p_file, &fs, cpos);2787ERR_FAIL_COND_V_MSG(!found, ERR_FILE_NOT_FOUND, vformat("Can't find file '%s' during file reimport.", p_file));2788}27892790//try to obtain existing params27912792HashMap<StringName, Variant> params(p_custom_options);2793String importer_name; //empty by default though27942795if (!p_custom_importer.is_empty()) {2796importer_name = p_custom_importer;2797}27982799ResourceUID::ID uid = ResourceUID::INVALID_ID;2800Variant generator_parameters;2801String group_file;2802if (p_generator_parameters) {2803generator_parameters = *p_generator_parameters;2804}28052806if (FileAccess::exists(p_file + ".import")) {2807//use existing2808Ref<ConfigFile> cf;2809cf.instantiate();2810Error err = cf->load(p_file + ".import");2811if (err == OK) {2812if (cf->has_section("params")) {2813Vector<String> sk = cf->get_section_keys("params");2814for (const String &E : sk) {2815if (!params.has(E)) {2816params[E] = cf->get_value("params", E);2817}2818}2819}28202821if (cf->has_section("remap")) {2822if (p_custom_importer.is_empty()) {2823importer_name = cf->get_value("remap", "importer");2824}28252826if (cf->has_section_key("remap", "uid")) {2827String uidt = cf->get_value("remap", "uid");2828uid = ResourceUID::get_singleton()->text_to_id(uidt);2829}28302831if (cf->has_section_key("remap", "group_file")) {2832group_file = cf->get_value("remap", "group_file");2833}28342835if (!p_generator_parameters) {2836if (cf->has_section_key("remap", "generator_parameters")) {2837generator_parameters = cf->get_value("remap", "generator_parameters");2838}2839}2840}2841}2842}28432844if (importer_name == "keep" || importer_name == "skip") {2845//keep files, do nothing.2846if (p_update_file_system) {2847fs->files[cpos]->modified_time = FileAccess::get_modified_time(p_file);2848fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(p_file + ".import");2849fs->files[cpos]->import_md5 = FileAccess::get_md5(p_file + ".import");2850fs->files[cpos]->import_dest_paths = Vector<String>();2851fs->files[cpos]->deps.clear();2852fs->files[cpos]->type = "";2853fs->files[cpos]->import_valid = false;2854EditorResourcePreview::get_singleton()->check_for_invalidation(p_file);2855}2856return OK;2857}2858Ref<ResourceImporter> importer;2859bool load_default = false;2860//find the importer2861if (!importer_name.is_empty()) {2862importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(importer_name);2863}28642865if (importer.is_null()) {2866//not found by name, find by extension2867importer = ResourceFormatImporter::get_singleton()->get_importer_by_file(p_file);2868load_default = true;2869if (importer.is_null()) {2870ERR_FAIL_V_MSG(ERR_FILE_CANT_OPEN, "BUG: File queued for import, but can't be imported, importer for type '" + importer_name + "' not found.");2871}2872}28732874if (FileAccess::exists(p_file + ".import")) {2875// We only want to handle compat for existing files, not new ones.2876importer->handle_compatibility_options(params);2877}28782879//mix with default params, in case a parameter is missing28802881List<ResourceImporter::ImportOption> opts;2882importer->get_import_options(p_file, &opts);2883for (const ResourceImporter::ImportOption &E : opts) {2884if (!params.has(E.option.name)) { //this one is not present2885params[E.option.name] = E.default_value;2886}2887}28882889if (load_default && ProjectSettings::get_singleton()->has_setting("importer_defaults/" + importer->get_importer_name())) {2890//use defaults if exist2891Dictionary d = GLOBAL_GET("importer_defaults/" + importer->get_importer_name());28922893for (const KeyValue<Variant, Variant> &kv : d) {2894params[kv.key] = kv.value;2895}2896}28972898if (uid == ResourceUID::INVALID_ID) {2899uid = ResourceUID::get_singleton()->create_id_for_path(p_file);2900}29012902//finally, perform import!!2903String base_path = ResourceFormatImporter::get_singleton()->get_import_base_path(p_file);29042905List<String> import_variants;2906List<String> gen_files;2907Variant meta;2908Error err = importer->import(uid, p_file, base_path, params, &import_variants, &gen_files, &meta);29092910// As import is complete, save the .import file.29112912Vector<String> dest_paths;2913{2914Ref<FileAccess> f = FileAccess::open(p_file + ".import", FileAccess::WRITE);2915ERR_FAIL_COND_V_MSG(f.is_null(), ERR_FILE_CANT_OPEN, "Cannot open file from path '" + p_file + ".import'.");29162917// Write manually, as order matters ([remap] has to go first for performance).2918f->store_line("[remap]");2919f->store_line("");2920f->store_line("importer=\"" + importer->get_importer_name() + "\"");2921int version = importer->get_format_version();2922if (version > 0) {2923f->store_line("importer_version=" + itos(version));2924}2925if (!importer->get_resource_type().is_empty()) {2926f->store_line("type=\"" + importer->get_resource_type() + "\"");2927}29282929f->store_line("uid=\"" + ResourceUID::get_singleton()->id_to_text(uid) + "\""); // Store in readable format.2930if (!group_file.is_empty()) {2931f->store_line("group_file=\"" + group_file + "\"");2932}29332934if (err == OK) {2935if (importer->get_save_extension().is_empty()) {2936//no path2937} else if (import_variants.size()) {2938//import with variants2939for (const String &E : import_variants) {2940String path = base_path.c_escape() + "." + E + "." + importer->get_save_extension();29412942f->store_line("path." + E + "=\"" + path + "\"");2943dest_paths.push_back(path);2944}2945} else {2946String path = base_path + "." + importer->get_save_extension();2947f->store_line("path=\"" + path + "\"");2948dest_paths.push_back(path);2949}29502951} else {2952f->store_line("valid=false");2953}29542955if (meta != Variant()) {2956f->store_line("metadata=" + meta.get_construct_string());2957}29582959if (generator_parameters != Variant()) {2960f->store_line("generator_parameters=" + generator_parameters.get_construct_string());2961}29622963f->store_line("");29642965f->store_line("[deps]\n");29662967if (gen_files.size()) {2968Array genf;2969for (const String &E : gen_files) {2970genf.push_back(E);2971dest_paths.push_back(E);2972}29732974String value;2975VariantWriter::write_to_string(genf, value);2976f->store_line("files=" + value);2977f->store_line("");2978}29792980f->store_line("source_file=" + Variant(p_file).get_construct_string());29812982if (dest_paths.size()) {2983Array dp;2984for (int i = 0; i < dest_paths.size(); i++) {2985dp.push_back(dest_paths[i]);2986}2987f->store_line("dest_files=" + Variant(dp).get_construct_string());2988}2989f->store_line("");29902991f->store_line("[params]");2992f->store_line("");29932994// Store options in provided order, to avoid file changing. Order is also important because first match is accepted first.29952996for (const ResourceImporter::ImportOption &E : opts) {2997String base = E.option.name;2998String value;2999VariantWriter::write_to_string(params[base], value);3000f->store_line(base + "=" + value);3001}3002}30033004// Store the md5's of the various files. These are stored separately so that the .import files can be version controlled.3005{3006Ref<FileAccess> md5s = FileAccess::open(base_path + ".md5", FileAccess::WRITE);3007ERR_FAIL_COND_V_MSG(md5s.is_null(), ERR_FILE_CANT_OPEN, "Cannot open MD5 file '" + base_path + ".md5'.");30083009md5s->store_line("source_md5=\"" + FileAccess::get_md5(p_file) + "\"");3010if (dest_paths.size()) {3011md5s->store_line("dest_md5=\"" + FileAccess::get_multiple_md5(dest_paths) + "\"\n");3012}3013}30143015if (p_update_file_system) {3016// Update cpos, newly created files could've changed the index of the reimported p_file.3017_find_file(p_file, &fs, cpos);30183019// Update modified times, to avoid reimport.3020fs->files[cpos]->modified_time = FileAccess::get_modified_time(p_file);3021fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(p_file + ".import");3022fs->files[cpos]->import_md5 = FileAccess::get_md5(p_file + ".import");3023fs->files[cpos]->import_dest_paths = dest_paths;3024fs->files[cpos]->deps = _get_dependencies(p_file);3025fs->files[cpos]->type = importer->get_resource_type();3026fs->files[cpos]->uid = uid;3027fs->files[cpos]->import_valid = fs->files[cpos]->type == "TextFile" ? true : ResourceLoader::is_import_valid(p_file);3028}30293030for (const String &path : gen_files) {3031Ref<Resource> cached = ResourceCache::get_ref(path);3032if (cached.is_valid()) {3033cached->reload_from_file();3034}3035}30363037if (ResourceUID::get_singleton()->has_id(uid)) {3038ResourceUID::get_singleton()->set_id(uid, p_file);3039} else {3040ResourceUID::get_singleton()->add_id(uid, p_file);3041}30423043// If file is currently up, maybe the source it was loaded from changed, so import math must be updated for it3044// to reload properly.3045Ref<Resource> r = ResourceCache::get_ref(p_file);3046if (r.is_valid()) {3047if (!r->get_import_path().is_empty()) {3048String dst_path = ResourceFormatImporter::get_singleton()->get_internal_resource_path(p_file);3049r->set_import_path(dst_path);3050r->set_import_last_modified_time(0);3051}3052}30533054EditorResourcePreview::get_singleton()->check_for_invalidation(p_file);30553056print_verbose(vformat("EditorFileSystem: \"%s\" import took %d ms.", p_file, OS::get_singleton()->get_ticks_msec() - start_time));30573058ERR_FAIL_COND_V_MSG(err != OK, ERR_FILE_UNRECOGNIZED, "Error importing '" + p_file + "'.");3059return OK;3060}30613062void EditorFileSystem::_find_group_files(EditorFileSystemDirectory *efd, HashMap<String, Vector<String>> &group_files, HashSet<String> &groups_to_reimport) {3063int fc = efd->files.size();3064const EditorFileSystemDirectory::FileInfo *const *files = efd->files.ptr();3065for (int i = 0; i < fc; i++) {3066if (groups_to_reimport.has(files[i]->import_group_file)) {3067if (!group_files.has(files[i]->import_group_file)) {3068group_files[files[i]->import_group_file] = Vector<String>();3069}3070group_files[files[i]->import_group_file].push_back(efd->get_file_path(i));3071}3072}30733074for (int i = 0; i < efd->get_subdir_count(); i++) {3075_find_group_files(efd->get_subdir(i), group_files, groups_to_reimport);3076}3077}30783079void EditorFileSystem::reimport_file_with_custom_parameters(const String &p_file, const String &p_importer, const HashMap<StringName, Variant> &p_custom_params) {3080Vector<String> reloads;3081reloads.append(p_file);30823083// Emit the resource_reimporting signal for the single file before the actual importation.3084emit_signal(SNAME("resources_reimporting"), reloads);30853086_reimport_file(p_file, p_custom_params, p_importer);30873088// Emit the resource_reimported signal for the single file we just reimported.3089emit_signal(SNAME("resources_reimported"), reloads);3090}30913092Error EditorFileSystem::_copy_file(const String &p_from, const String &p_to) {3093Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);3094if (FileAccess::exists(p_from + ".import")) {3095Error err = da->copy(p_from, p_to);3096if (err != OK) {3097return err;3098}30993100// Save the new .import file3101Ref<ConfigFile> cfg;3102cfg.instantiate();3103cfg->load(p_from + ".import");3104String importer_name = cfg->get_value("remap", "importer");31053106if (importer_name == "keep" || importer_name == "skip") {3107err = da->copy(p_from + ".import", p_to + ".import");3108return err;3109}31103111// Roll a new uid for this copied .import file to avoid conflict.3112ResourceUID::ID res_uid = ResourceUID::get_singleton()->create_id_for_path(p_to);3113cfg->set_value("remap", "uid", ResourceUID::get_singleton()->id_to_text(res_uid));3114err = cfg->save(p_to + ".import");3115if (err != OK) {3116return err;3117}31183119// Make sure it's immediately added to the map so we can remap dependencies if we want to after this.3120ResourceUID::get_singleton()->add_id(res_uid, p_to);3121} else if (ResourceLoader::get_resource_uid(p_from) == ResourceUID::INVALID_ID) {3122// Files which do not use an uid can just be copied.3123Error err = da->copy(p_from, p_to);3124if (err != OK) {3125return err;3126}3127} else {3128// Load the resource and save it again in the new location (this generates a new UID).3129Error err = OK;3130Ref<Resource> res = ResourceCache::get_ref(p_from);3131if (res.is_null()) {3132res = ResourceLoader::load(p_from, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err);3133} else {3134bool edited = false;3135List<Ref<Resource>> cached;3136ResourceCache::get_cached_resources(&cached);3137for (Ref<Resource> &resource : cached) {3138if (!resource->is_edited()) {3139continue;3140}3141if (!resource->get_path().begins_with(p_from)) {3142continue;3143}3144// The resource or one of its built-in resources is edited.3145edited = true;3146resource->set_edited(false);3147}31483149if (edited) {3150// Save cached resources to prevent changes from being lost and to prevent discrepancies.3151EditorNode::get_singleton()->save_resource(res);3152}3153}3154if (err == OK && res.is_valid()) {3155err = ResourceSaver::save(res, p_to, ResourceSaver::FLAG_COMPRESS);3156if (err != OK) {3157return err;3158}3159} else if (err != OK) {3160// When loading files like text files the error is OK but the resource is still null.3161// We can ignore such files.3162return err;3163}3164}3165return OK;3166}31673168bool EditorFileSystem::_copy_directory(const String &p_from, const String &p_to, HashMap<String, String> *p_files) {3169Ref<DirAccess> old_dir = DirAccess::open(p_from);3170ERR_FAIL_COND_V(old_dir.is_null(), false);31713172Error err = make_dir_recursive(p_to);3173if (err != OK && err != ERR_ALREADY_EXISTS) {3174return false;3175}31763177bool success = true;3178old_dir->set_include_navigational(false);3179old_dir->list_dir_begin();31803181for (String F = old_dir->_get_next(); !F.is_empty(); F = old_dir->_get_next()) {3182if (old_dir->current_is_dir()) {3183success = _copy_directory(p_from.path_join(F), p_to.path_join(F), p_files) && success;3184} else if (F.get_extension() != "import" && F.get_extension() != "uid") {3185(*p_files)[p_from.path_join(F)] = p_to.path_join(F);3186}3187}3188return success;3189}31903191void EditorFileSystem::_queue_refresh_filesystem() {3192if (refresh_queued) {3193return;3194}3195refresh_queued = true;3196get_tree()->connect(SNAME("process_frame"), callable_mp(this, &EditorFileSystem::_refresh_filesystem), CONNECT_ONE_SHOT);3197}31983199void EditorFileSystem::_refresh_filesystem() {3200for (const ObjectID &id : folders_to_sort) {3201EditorFileSystemDirectory *dir = ObjectDB::get_instance<EditorFileSystemDirectory>(id);3202if (dir) {3203dir->subdirs.sort_custom<DirectoryComparator>();3204}3205}3206folders_to_sort.clear();32073208_update_scan_actions();32093210emit_signal(SNAME("filesystem_changed"));3211refresh_queued = false;3212}32133214void EditorFileSystem::_reimport_thread(uint32_t p_index, ImportThreadData *p_import_data) {3215ResourceLoader::set_is_import_thread(true);3216int file_idx = p_import_data->reimport_from + int(p_index);3217_reimport_file(p_import_data->reimport_files[file_idx].path);3218ResourceLoader::set_is_import_thread(false);32193220p_import_data->imported_sem->post();3221}32223223void EditorFileSystem::reimport_files(const Vector<String> &p_files) {3224ERR_FAIL_COND_MSG(importing, "Attempted to call reimport_files() recursively, this is not allowed.");3225importing = true;32263227Vector<String> reloads;32283229EditorProgress *ep = memnew(EditorProgress("reimport", TTR("(Re)Importing Assets"), p_files.size()));32303231// The method reimport_files runs on the main thread, and if VSync is enabled3232// or Update Continuously is disabled, Main::Iteration takes longer each frame.3233// Each EditorProgress::step can trigger a redraw, and when there are many files to import,3234// this could lead to a slow import process, especially when the editor is unfocused.3235// Temporarily disabling VSync and low_processor_usage_mode while reimporting fixes this.3236const bool old_low_processor_usage_mode = OS::get_singleton()->is_in_low_processor_usage_mode();3237const DisplayServer::VSyncMode old_vsync_mode = DisplayServer::get_singleton()->window_get_vsync_mode(DisplayServer::MAIN_WINDOW_ID);3238OS::get_singleton()->set_low_processor_usage_mode(false);3239DisplayServer::get_singleton()->window_set_vsync_mode(DisplayServer::VSyncMode::VSYNC_DISABLED);32403241Vector<ImportFile> reimport_files;32423243HashSet<String> groups_to_reimport;32443245for (int i = 0; i < p_files.size(); i++) {3246ep->step(TTR("Preparing files to reimport..."), i, false);32473248String file = p_files[i];32493250ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(file);3251if (uid != ResourceUID::INVALID_ID && ResourceUID::get_singleton()->has_id(uid)) {3252file = ResourceUID::get_singleton()->get_id_path(uid);3253}32543255String group_file = ResourceFormatImporter::get_singleton()->get_import_group_file(file);32563257if (group_file_cache.has(file)) {3258// Maybe the file itself is a group!3259groups_to_reimport.insert(file);3260// Groups do not belong to groups.3261group_file = String();3262} else if (groups_to_reimport.has(file)) {3263// Groups do not belong to groups.3264group_file = String();3265} else if (!group_file.is_empty()) {3266// It's a group file, add group to import and skip this file.3267groups_to_reimport.insert(group_file);3268} else {3269// It's a regular file.3270ImportFile ifile;3271ifile.path = file;3272ResourceFormatImporter::get_singleton()->get_import_order_threads_and_importer(file, ifile.order, ifile.threaded, ifile.importer);3273reloads.push_back(file);3274reimport_files.push_back(ifile);3275}32763277// Group may have changed, so also update group reference.3278EditorFileSystemDirectory *fs = nullptr;3279int cpos = -1;3280if (_find_file(file, &fs, cpos)) {3281fs->files.write[cpos]->import_group_file = group_file;3282}3283}32843285reimport_files.sort();32863287ep->step(TTR("Executing pre-reimport operations..."), 0, true);32883289// Emit the resource_reimporting signal for the single file before the actual importation.3290emit_signal(SNAME("resources_reimporting"), reloads);32913292#ifdef WEB_ENABLED3293// On web, busy-wait loops on the main thread block the JavaScript event loop,3294// causing the browser tab to appear frozen. Disable threaded imports entirely.3295// See GH-112072 for details.3296bool use_multiple_threads = false;3297#elif defined(THREADS_ENABLED)3298bool use_multiple_threads = GLOBAL_GET("editor/import/use_multiple_threads");3299#else3300bool use_multiple_threads = false;3301#endif33023303int from = 0;3304Semaphore imported_sem;3305for (int i = 0; i < reimport_files.size(); i++) {3306if (groups_to_reimport.has(reimport_files[i].path)) {3307from = i + 1;3308continue;3309}33103311if (use_multiple_threads && reimport_files[i].threaded) {3312if (i + 1 == reimport_files.size() || reimport_files[i + 1].importer != reimport_files[from].importer || groups_to_reimport.has(reimport_files[i + 1].path)) {3313if (from - i == 0) {3314// Single file, do not use threads.3315ep->step(reimport_files[i].path.get_file(), i, false);3316_reimport_file(reimport_files[i].path);3317} else {3318Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(reimport_files[from].importer);3319if (importer.is_null()) {3320ERR_PRINT(vformat("Invalid importer for \"%s\".", reimport_files[from].importer));3321from = i + 1;3322continue;3323}33243325importer->import_threaded_begin();33263327ImportThreadData tdata;3328tdata.reimport_from = from;3329tdata.reimport_files = reimport_files.ptr();3330tdata.imported_sem = &imported_sem;33313332int item_count = i - from + 1;3333WorkerThreadPool::GroupID group_task = WorkerThreadPool::get_singleton()->add_template_group_task(this, &EditorFileSystem::_reimport_thread, &tdata, item_count, -1, false, vformat(TTR("Import resources of type: %s"), reimport_files[from].importer));33343335int imported_count = 0;3336while (true) {3337while (true) {3338ep->step(reimport_files[imported_count].path.get_file(), from + imported_count, false);3339if (imported_sem.try_wait()) {3340imported_count++;3341break;3342}3343}3344if (imported_count == item_count) {3345break;3346}3347}33483349WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group_task);3350DEV_ASSERT(!imported_sem.try_wait());33513352importer->import_threaded_end();3353}33543355from = i + 1;3356}33573358} else {3359ep->step(reimport_files[i].path.get_file(), i, false);3360_reimport_file(reimport_files[i].path);33613362// We need to increment the counter, maybe the next file is multithreaded3363// and doesn't have the same importer.3364from = i + 1;3365}3366}33673368// Reimport groups.33693370from = reimport_files.size();33713372if (groups_to_reimport.size()) {3373HashMap<String, Vector<String>> group_files;3374_find_group_files(filesystem, group_files, groups_to_reimport);3375for (const KeyValue<String, Vector<String>> &E : group_files) {3376ep->step(E.key.get_file(), from++, false);3377Error err = _reimport_group(E.key, E.value);3378reloads.push_back(E.key);3379reloads.append_array(E.value);3380if (err == OK) {3381_reimport_file(E.key);3382}3383}3384}3385ep->step(TTR("Finalizing Asset Import..."), p_files.size());33863387ResourceUID::get_singleton()->update_cache(); // After reimporting, update the cache.3388_save_filesystem_cache();33893390memdelete_notnull(ep);33913392_process_update_pending();33933394// Revert to previous values to restore editor settings for VSync and Update Continuously.3395OS::get_singleton()->set_low_processor_usage_mode(old_low_processor_usage_mode);3396DisplayServer::get_singleton()->window_set_vsync_mode(old_vsync_mode);33973398importing = false;33993400ep = memnew(EditorProgress("reimport", TTR("(Re)Importing Assets"), p_files.size()));3401ep->step(TTR("Executing post-reimport operations..."), 0, true);3402if (!is_scanning()) {3403emit_signal(SNAME("filesystem_changed"));3404}3405emit_signal(SNAME("resources_reimported"), reloads);3406memdelete_notnull(ep);3407}34083409Error EditorFileSystem::reimport_append(const String &p_file, const HashMap<StringName, Variant> &p_custom_options, const String &p_custom_importer, Variant p_generator_parameters) {3410Vector<String> reloads;3411reloads.append(p_file);34123413// Emit the resource_reimporting signal for the single file before the actual importation.3414emit_signal(SNAME("resources_reimporting"), reloads);34153416Error ret = _reimport_file(p_file, p_custom_options, p_custom_importer, &p_generator_parameters);34173418// Emit the resource_reimported signal for the single file we just reimported.3419emit_signal(SNAME("resources_reimported"), reloads);3420return ret;3421}34223423Error EditorFileSystem::_resource_import(const String &p_path) {3424Vector<String> files;3425files.push_back(p_path);34263427singleton->update_file(p_path);3428singleton->reimport_files(files);34293430return OK;3431}34323433Ref<Resource> EditorFileSystem::_load_resource_on_startup(ResourceFormatImporter *p_importer, const String &p_path, Error *r_error, bool p_use_sub_threads, float *r_progress, ResourceFormatLoader::CacheMode p_cache_mode) {3434ERR_FAIL_NULL_V(p_importer, Ref<Resource>());34353436if (!FileAccess::exists(p_path)) {3437ERR_FAIL_V_MSG(Ref<Resource>(), vformat("Failed loading resource: %s. The file doesn't seem to exist.", p_path));3438}34393440// Fail silently. Hopefully the resource is not yet imported.3441Ref<Resource> res = p_importer->load_internal(p_path, r_error, p_use_sub_threads, r_progress, p_cache_mode, true);3442if (res.is_valid()) {3443return res;3444}34453446// Retry after importing the resource.3447if (singleton->_reimport_file(p_path, HashMap<StringName, Variant>(), "", nullptr, false) != OK) {3448return Ref<Resource>();3449}3450return p_importer->load_internal(p_path, r_error, p_use_sub_threads, r_progress, p_cache_mode, false);3451}34523453bool EditorFileSystem::_should_skip_directory(const String &p_path) {3454String project_data_path = ProjectSettings::get_singleton()->get_project_data_path();3455if (p_path == project_data_path || p_path.begins_with(project_data_path + "/")) {3456return true;3457}34583459if (FileAccess::exists(p_path.path_join("project.godot"))) {3460// Skip if another project inside this.3461if (EditorFileSystem::get_singleton() == nullptr || EditorFileSystem::get_singleton()->first_scan) {3462WARN_PRINT_ONCE(vformat("Detected another project.godot at %s. The folder will be ignored.", p_path));3463}3464return true;3465}34663467if (FileAccess::exists(p_path.path_join(".gdignore"))) {3468// Skip if a `.gdignore` file is inside this.3469return true;3470}34713472return false;3473}34743475bool EditorFileSystem::is_group_file(const String &p_path) const {3476return group_file_cache.has(p_path);3477}34783479void EditorFileSystem::_move_group_files(EditorFileSystemDirectory *efd, const String &p_group_file, const String &p_new_location) {3480int fc = efd->files.size();3481EditorFileSystemDirectory::FileInfo *const *files = efd->files.ptrw();3482for (int i = 0; i < fc; i++) {3483if (files[i]->import_group_file == p_group_file) {3484files[i]->import_group_file = p_new_location;34853486Ref<ConfigFile> config;3487config.instantiate();3488String path = efd->get_file_path(i) + ".import";3489Error err = config->load(path);3490if (err != OK) {3491continue;3492}3493if (config->has_section_key("remap", "group_file")) {3494config->set_value("remap", "group_file", p_new_location);3495}34963497Vector<String> sk = config->get_section_keys("params");3498for (const String ¶m : sk) {3499//not very clean, but should work3500String value = config->get_value("params", param);3501if (value == p_group_file) {3502config->set_value("params", param, p_new_location);3503}3504}35053506config->save(path);3507}3508}35093510for (int i = 0; i < efd->get_subdir_count(); i++) {3511_move_group_files(efd->get_subdir(i), p_group_file, p_new_location);3512}3513}35143515void EditorFileSystem::move_group_file(const String &p_path, const String &p_new_path) {3516if (get_filesystem()) {3517_move_group_files(get_filesystem(), p_path, p_new_path);3518if (group_file_cache.has(p_path)) {3519group_file_cache.erase(p_path);3520group_file_cache.insert(p_new_path);3521}3522}3523}35243525Error EditorFileSystem::make_dir_recursive(const String &p_path, const String &p_base_path) {3526Error err;3527Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);3528if (!p_base_path.is_empty()) {3529err = da->change_dir(p_base_path);3530ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot open base directory '" + p_base_path + "'.");3531}35323533if (da->dir_exists(p_path)) {3534return ERR_ALREADY_EXISTS;3535}35363537err = da->make_dir_recursive(p_path);3538if (err != OK) {3539return err;3540}35413542const String path = da->get_current_dir();3543EditorFileSystemDirectory *parent = get_filesystem_path(path);3544ERR_FAIL_NULL_V(parent, ERR_FILE_NOT_FOUND);3545folders_to_sort.insert(parent->get_instance_id());35463547const PackedStringArray folders = p_path.trim_prefix(path).split("/", false);3548for (const String &folder : folders) {3549const int current = parent->find_dir_index(folder);3550if (current > -1) {3551parent = parent->get_subdir(current);3552continue;3553}35543555EditorFileSystemDirectory *efd = memnew(EditorFileSystemDirectory);3556efd->parent = parent;3557efd->name = folder;3558parent->subdirs.push_back(efd);3559parent = efd;3560}35613562_queue_refresh_filesystem();3563return OK;3564}35653566Error EditorFileSystem::copy_file(const String &p_from, const String &p_to) {3567Error err = _copy_file(p_from, p_to);3568if (err != OK) {3569return err;3570}35713572EditorFileSystemDirectory *parent = get_filesystem_path(p_to.get_base_dir());3573ERR_FAIL_NULL_V(parent, ERR_FILE_NOT_FOUND);35743575ScanProgress sp;3576_scan_fs_changes(parent, sp, false);35773578_queue_refresh_filesystem();3579return OK;3580}35813582Error EditorFileSystem::copy_directory(const String &p_from, const String &p_to) {3583// Recursively copy directories and build a map of files to copy.3584HashMap<String, String> files;3585bool success = _copy_directory(p_from, p_to, &files);35863587// Copy the files themselves3588if (success) {3589EditorProgress *ep = nullptr;3590if (files.size() > 10) {3591ep = memnew(EditorProgress("copy_directory", TTR("Copying files..."), files.size()));3592}3593int i = 0;3594for (const KeyValue<String, String> &tuple : files) {3595if (_copy_file(tuple.key, tuple.value) != OK) {3596success = false;3597}3598if (ep) {3599ep->step(tuple.key.get_file(), i++, false);3600}3601}3602memdelete_notnull(ep);3603}36043605// Now remap any internal dependencies (within the folder) to use the new files.3606if (success) {3607EditorProgress *ep = nullptr;3608if (files.size() > 10) {3609ep = memnew(EditorProgress("copy_directory", TTR("Remapping dependencies..."), files.size()));3610}3611int i = 0;3612for (const KeyValue<String, String> &tuple : files) {3613if (ResourceLoader::rename_dependencies(tuple.value, files) != OK) {3614success = false;3615}3616update_file(tuple.value);3617if (ep) {3618ep->step(tuple.key.get_file(), i++, false);3619}3620}3621memdelete_notnull(ep);3622}36233624EditorFileSystemDirectory *efd = get_filesystem_path(p_to);3625ERR_FAIL_NULL_V(efd, FAILED);3626ERR_FAIL_NULL_V(efd->get_parent(), FAILED);36273628folders_to_sort.insert(efd->get_parent()->get_instance_id());36293630ScanProgress sp;3631_scan_fs_changes(efd, sp);36323633_queue_refresh_filesystem();3634return success ? OK : FAILED;3635}36363637ResourceUID::ID EditorFileSystem::_resource_saver_get_resource_id_for_path(const String &p_path, bool p_generate) {3638if (!p_path.is_resource_file() || p_path.begins_with(ProjectSettings::get_singleton()->get_project_data_path())) {3639// Saved externally (configuration file) or internal file, do not assign an ID.3640return ResourceUID::INVALID_ID;3641}36423643EditorFileSystemDirectory *fs = nullptr;3644int cpos = -1;36453646if (!singleton->_find_file(p_path, &fs, cpos)) {3647// Fallback to ResourceLoader if filesystem cache fails (can happen during scanning etc.).3648ResourceUID::ID fallback = ResourceLoader::get_resource_uid(p_path);3649if (fallback != ResourceUID::INVALID_ID) {3650return fallback;3651}36523653if (p_generate) {3654return ResourceUID::get_singleton()->create_id_for_path(p_path); // Just create a new one, we will be notified of save anyway and fetch the right UID at that time, to keep things simple.3655} else {3656return ResourceUID::INVALID_ID;3657}3658} else if (fs->files[cpos]->uid != ResourceUID::INVALID_ID) {3659return fs->files[cpos]->uid;3660} else if (p_generate) {3661return ResourceUID::get_singleton()->create_id_for_path(p_path); // Just create a new one, we will be notified of save anyway and fetch the right UID at that time, to keep things simple.3662} else {3663return ResourceUID::INVALID_ID;3664}3665}36663667static void _scan_extensions_dir(EditorFileSystemDirectory *d, HashSet<String> &extensions) {3668int fc = d->get_file_count();3669for (int i = 0; i < fc; i++) {3670if (d->get_file_type(i) == SNAME("GDExtension")) {3671extensions.insert(d->get_file_path(i));3672}3673}3674int dc = d->get_subdir_count();3675for (int i = 0; i < dc; i++) {3676_scan_extensions_dir(d->get_subdir(i), extensions);3677}3678}3679bool EditorFileSystem::_scan_extensions() {3680EditorFileSystemDirectory *d = get_filesystem();3681HashSet<String> extensions;36823683_scan_extensions_dir(d, extensions);36843685return GDExtensionManager::get_singleton()->ensure_extensions_loaded(extensions);3686}36873688void EditorFileSystem::_bind_methods() {3689ClassDB::bind_method(D_METHOD("get_filesystem"), &EditorFileSystem::get_filesystem);3690ClassDB::bind_method(D_METHOD("is_scanning"), &EditorFileSystem::is_scanning);3691ClassDB::bind_method(D_METHOD("get_scanning_progress"), &EditorFileSystem::get_scanning_progress);3692ClassDB::bind_method(D_METHOD("scan"), &EditorFileSystem::scan);3693ClassDB::bind_method(D_METHOD("scan_sources"), &EditorFileSystem::scan_changes);3694ClassDB::bind_method(D_METHOD("update_file", "path"), &EditorFileSystem::update_file);3695ClassDB::bind_method(D_METHOD("get_filesystem_path", "path"), &EditorFileSystem::get_filesystem_path);3696ClassDB::bind_method(D_METHOD("get_file_type", "path"), &EditorFileSystem::get_file_type);3697ClassDB::bind_method(D_METHOD("reimport_files", "files"), &EditorFileSystem::reimport_files);36983699ADD_SIGNAL(MethodInfo("filesystem_changed"));3700ADD_SIGNAL(MethodInfo("script_classes_updated"));3701ADD_SIGNAL(MethodInfo("sources_changed", PropertyInfo(Variant::BOOL, "exist")));3702ADD_SIGNAL(MethodInfo("resources_reimporting", PropertyInfo(Variant::PACKED_STRING_ARRAY, "resources")));3703ADD_SIGNAL(MethodInfo("resources_reimported", PropertyInfo(Variant::PACKED_STRING_ARRAY, "resources")));3704ADD_SIGNAL(MethodInfo("resources_reload", PropertyInfo(Variant::PACKED_STRING_ARRAY, "resources")));3705}37063707void EditorFileSystem::_update_extensions() {3708valid_extensions.clear();3709import_extensions.clear();3710textfile_extensions.clear();3711other_file_extensions.clear();37123713List<String> extensionsl;3714ResourceLoader::get_recognized_extensions_for_type("", &extensionsl);3715for (const String &E : extensionsl) {3716valid_extensions.insert(E);3717}37183719const Vector<String> textfile_ext = ((String)(EDITOR_GET("docks/filesystem/textfile_extensions"))).split(",", false);3720for (const String &E : textfile_ext) {3721if (valid_extensions.has(E)) {3722continue;3723}3724valid_extensions.insert(E);3725textfile_extensions.insert(E);3726}3727const Vector<String> other_file_ext = ((String)(EDITOR_GET("docks/filesystem/other_file_extensions"))).split(",", false);3728for (const String &E : other_file_ext) {3729if (valid_extensions.has(E)) {3730continue;3731}3732valid_extensions.insert(E);3733other_file_extensions.insert(E);3734}37353736extensionsl.clear();3737ResourceFormatImporter::get_singleton()->get_recognized_extensions(&extensionsl);3738for (const String &E : extensionsl) {3739import_extensions.insert(!E.begins_with(".") ? "." + E : E);3740}3741}37423743bool EditorFileSystem::_can_import_file(const String &p_file) {3744for (const String &F : import_extensions) {3745if (p_file.right(F.length()).nocasecmp_to(F) == 0) {3746return true;3747}3748}37493750return false;3751}37523753void EditorFileSystem::add_import_format_support_query(Ref<EditorFileSystemImportFormatSupportQuery> p_query) {3754ERR_FAIL_COND(import_support_queries.has(p_query));3755import_support_queries.push_back(p_query);3756}3757void EditorFileSystem::remove_import_format_support_query(Ref<EditorFileSystemImportFormatSupportQuery> p_query) {3758import_support_queries.erase(p_query);3759}37603761EditorFileSystem::EditorFileSystem() {3762#if defined(THREADS_ENABLED) && !defined(WEB_ENABLED)3763// On web, threaded scanning blocks the browser's event loop, causing freezes.3764// See GH-112072 for details.3765use_threads = true;3766#endif37673768ResourceLoader::import = _resource_import;3769reimport_on_missing_imported_files = GLOBAL_GET("editor/import/reimport_missing_imported_files");3770singleton = this;3771filesystem = memnew(EditorFileSystemDirectory); //like, empty3772filesystem->parent = nullptr;37733774new_filesystem = nullptr;37753776// This should probably also work on Unix and use the string it returns for FAT32 or exFAT3777Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);3778using_fat32_or_exfat = (da->get_filesystem_type() == "FAT32" || da->get_filesystem_type() == "EXFAT");37793780scan_total = 0;3781ResourceSaver::set_get_resource_id_for_path(_resource_saver_get_resource_id_for_path);37823783// Set the callback method that the ResourceFormatImporter will use3784// if resources are loaded during the first scan.3785ResourceImporter::load_on_startup = _load_resource_on_startup;3786}37873788EditorFileSystem::~EditorFileSystem() {3789if (filesystem) {3790memdelete(filesystem);3791}3792filesystem = nullptr;3793ResourceSaver::set_get_resource_id_for_path(nullptr);3794}379537963797