Path: blob/master/editor/file_system/editor_file_system.cpp
9902 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}130131Vector<String> EditorFileSystemDirectory::get_file_deps(int p_idx) const {132ERR_FAIL_INDEX_V(p_idx, files.size(), Vector<String>());133Vector<String> deps;134135for (int i = 0; i < files[p_idx]->deps.size(); i++) {136String dep = files[p_idx]->deps[i];137int sep_idx = dep.find("::"); //may contain type information, unwanted138if (sep_idx != -1) {139dep = dep.substr(0, sep_idx);140}141ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(dep);142if (uid != ResourceUID::INVALID_ID) {143//return proper dependency resource from uid144if (ResourceUID::get_singleton()->has_id(uid)) {145dep = ResourceUID::get_singleton()->get_id_path(uid);146} else {147continue;148}149}150deps.push_back(dep);151}152return deps;153}154155bool EditorFileSystemDirectory::get_file_import_is_valid(int p_idx) const {156ERR_FAIL_INDEX_V(p_idx, files.size(), false);157return files[p_idx]->import_valid;158}159160uint64_t EditorFileSystemDirectory::get_file_modified_time(int p_idx) const {161ERR_FAIL_INDEX_V(p_idx, files.size(), 0);162return files[p_idx]->modified_time;163}164165uint64_t EditorFileSystemDirectory::get_file_import_modified_time(int p_idx) const {166ERR_FAIL_INDEX_V(p_idx, files.size(), 0);167return files[p_idx]->import_modified_time;168}169170String EditorFileSystemDirectory::get_file_script_class_name(int p_idx) const {171return files[p_idx]->class_info.name;172}173174String EditorFileSystemDirectory::get_file_script_class_extends(int p_idx) const {175return files[p_idx]->class_info.extends;176}177178String EditorFileSystemDirectory::get_file_script_class_icon_path(int p_idx) const {179return files[p_idx]->class_info.icon_path;180}181182String EditorFileSystemDirectory::get_file_icon_path(int p_idx) const {183return files[p_idx]->class_info.icon_path;184}185186StringName EditorFileSystemDirectory::get_file_type(int p_idx) const {187ERR_FAIL_INDEX_V(p_idx, files.size(), "");188return files[p_idx]->type;189}190191StringName EditorFileSystemDirectory::get_file_resource_script_class(int p_idx) const {192ERR_FAIL_INDEX_V(p_idx, files.size(), "");193return files[p_idx]->resource_script_class;194}195196String EditorFileSystemDirectory::get_name() {197return name;198}199200EditorFileSystemDirectory *EditorFileSystemDirectory::get_parent() {201return parent;202}203204void EditorFileSystemDirectory::_bind_methods() {205ClassDB::bind_method(D_METHOD("get_subdir_count"), &EditorFileSystemDirectory::get_subdir_count);206ClassDB::bind_method(D_METHOD("get_subdir", "idx"), &EditorFileSystemDirectory::get_subdir);207ClassDB::bind_method(D_METHOD("get_file_count"), &EditorFileSystemDirectory::get_file_count);208ClassDB::bind_method(D_METHOD("get_file", "idx"), &EditorFileSystemDirectory::get_file);209ClassDB::bind_method(D_METHOD("get_file_path", "idx"), &EditorFileSystemDirectory::get_file_path);210ClassDB::bind_method(D_METHOD("get_file_type", "idx"), &EditorFileSystemDirectory::get_file_type);211ClassDB::bind_method(D_METHOD("get_file_script_class_name", "idx"), &EditorFileSystemDirectory::get_file_script_class_name);212ClassDB::bind_method(D_METHOD("get_file_script_class_extends", "idx"), &EditorFileSystemDirectory::get_file_script_class_extends);213ClassDB::bind_method(D_METHOD("get_file_import_is_valid", "idx"), &EditorFileSystemDirectory::get_file_import_is_valid);214ClassDB::bind_method(D_METHOD("get_name"), &EditorFileSystemDirectory::get_name);215ClassDB::bind_method(D_METHOD("get_path"), &EditorFileSystemDirectory::get_path);216ClassDB::bind_method(D_METHOD("get_parent"), &EditorFileSystemDirectory::get_parent);217ClassDB::bind_method(D_METHOD("find_file_index", "name"), &EditorFileSystemDirectory::find_file_index);218ClassDB::bind_method(D_METHOD("find_dir_index", "name"), &EditorFileSystemDirectory::find_dir_index);219}220221EditorFileSystemDirectory::EditorFileSystemDirectory() {222modified_time = 0;223parent = nullptr;224}225226EditorFileSystemDirectory::~EditorFileSystemDirectory() {227for (EditorFileSystemDirectory::FileInfo *fi : files) {228memdelete(fi);229}230231for (EditorFileSystemDirectory *dir : subdirs) {232memdelete(dir);233}234}235236EditorFileSystem::ScannedDirectory::~ScannedDirectory() {237for (ScannedDirectory *dir : subdirs) {238memdelete(dir);239}240}241242void EditorFileSystem::_load_first_scan_root_dir() {243Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);244first_scan_root_dir = memnew(ScannedDirectory);245first_scan_root_dir->full_path = "res://";246247nb_files_total = _scan_new_dir(first_scan_root_dir, d);248}249250void EditorFileSystem::scan_for_uid() {251// Load file structure into memory.252_load_first_scan_root_dir();253254// Load extensions for which an .import should exists.255List<String> extensionsl;256HashSet<String> import_extensions;257ResourceFormatImporter::get_singleton()->get_recognized_extensions(&extensionsl);258for (const String &E : extensionsl) {259import_extensions.insert(E);260}261262// Scan the file system to load uid.263_scan_for_uid_directory(first_scan_root_dir, import_extensions);264265// It's done, resetting the callback method to prevent a second scan.266ResourceUID::scan_for_uid_on_startup = nullptr;267}268269void EditorFileSystem::_scan_for_uid_directory(const ScannedDirectory *p_scan_dir, const HashSet<String> &p_import_extensions) {270for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) {271_scan_for_uid_directory(scan_sub_dir, p_import_extensions);272}273274for (const String &scan_file : p_scan_dir->files) {275const String ext = scan_file.get_extension().to_lower();276277if (ext == "uid" || ext == "import") {278continue;279}280281const String path = p_scan_dir->full_path.path_join(scan_file);282ResourceUID::ID uid = ResourceUID::INVALID_ID;283if (p_import_extensions.has(ext)) {284if (FileAccess::exists(path + ".import")) {285uid = ResourceFormatImporter::get_singleton()->get_resource_uid(path);286}287} else {288uid = ResourceLoader::get_resource_uid(path);289}290291if (uid != ResourceUID::INVALID_ID) {292if (!ResourceUID::get_singleton()->has_id(uid)) {293ResourceUID::get_singleton()->add_id(uid, path);294}295}296}297}298299void EditorFileSystem::_first_scan_filesystem() {300EditorProgress ep = EditorProgress("first_scan_filesystem", TTR("Project initialization"), 5);301HashSet<String> existing_class_names;302HashSet<String> extensions;303304if (!first_scan_root_dir) {305ep.step(TTR("Scanning file structure..."), 0, true);306_load_first_scan_root_dir();307}308309// Preloading GDExtensions file extensions to prevent looping on all the resource loaders310// for each files in _first_scan_process_scripts.311List<String> gdextension_extensions;312ResourceLoader::get_recognized_extensions_for_type("GDExtension", &gdextension_extensions);313314// This loads the global class names from the scripts and ensures that even if the315// global_script_class_cache.cfg was missing or invalid, the global class names are valid in ScriptServer.316// At the same time, to prevent looping multiple times in all files, it looks for extensions.317ep.step(TTR("Loading global class names..."), 1, true);318_first_scan_process_scripts(first_scan_root_dir, gdextension_extensions, existing_class_names, extensions);319320// Removing invalid global class to prevent having invalid paths in ScriptServer.321bool save_scripts = _remove_invalid_global_class_names(existing_class_names);322323// If a global class is found or removed, we sync global_script_class_cache.cfg with the ScriptServer324if (!existing_class_names.is_empty() || save_scripts) {325EditorNode::get_editor_data().script_class_save_global_classes();326}327328// Processing extensions to add new extensions or remove invalid ones.329// Important to do it in the first scan so custom types, new class names, custom importers, etc...330// from extensions are ready to go before plugins, autoloads and resources validation/importation.331// At this point, a restart of the editor should not be needed so we don't use the return value.332ep.step(TTR("Verifying GDExtensions..."), 2, true);333GDExtensionManager::get_singleton()->ensure_extensions_loaded(extensions);334335// Now that all the global class names should be loaded, create autoloads and plugins.336// This is done after loading the global class names because autoloads and plugins can use337// global class names.338ep.step(TTR("Creating autoload scripts..."), 3, true);339ProjectSettingsEditor::get_singleton()->init_autoloads();340341ep.step(TTR("Initializing plugins..."), 4, true);342EditorNode::get_singleton()->init_plugins();343344ep.step(TTR("Starting file scan..."), 5, true);345}346347void 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) {348for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) {349_first_scan_process_scripts(scan_sub_dir, p_gdextension_extensions, p_existing_class_names, p_extensions);350}351352for (const String &scan_file : p_scan_dir->files) {353// Optimization to skip the ResourceLoader::get_resource_type for files354// that are not scripts. Some loader get_resource_type methods read the file355// which can be very slow on large projects.356const String ext = scan_file.get_extension().to_lower();357bool is_script = false;358for (int i = 0; i < ScriptServer::get_language_count(); i++) {359if (ScriptServer::get_language(i)->get_extension() == ext) {360is_script = true;361break;362}363}364if (is_script) {365const String path = p_scan_dir->full_path.path_join(scan_file);366const String type = ResourceLoader::get_resource_type(path);367368if (ClassDB::is_parent_class(type, SNAME("Script"))) {369const ScriptClassInfo &info = _get_global_script_class(type, path);370ScriptClassInfoUpdate update(info);371update.type = type;372_register_global_class_script(path, path, update);373374if (!info.name.is_empty()) {375p_existing_class_names.insert(info.name);376}377}378}379380// Check for GDExtensions.381if (p_gdextension_extensions.find(ext)) {382const String path = p_scan_dir->full_path.path_join(scan_file);383const String type = ResourceLoader::get_resource_type(path);384if (type == SNAME("GDExtension")) {385p_extensions.insert(path);386}387}388}389}390391void EditorFileSystem::_scan_filesystem() {392// On the first scan, the first_scan_root_dir is created in _first_scan_filesystem.393ERR_FAIL_COND(!scanning || new_filesystem || (first_scan && !first_scan_root_dir));394395//read .fscache396String cpath;397398sources_changed.clear();399file_cache.clear();400401String project = ProjectSettings::get_singleton()->get_resource_path();402403String fscache = EditorPaths::get_singleton()->get_project_settings_dir().path_join(CACHE_FILE_NAME);404{405Ref<FileAccess> f = FileAccess::open(fscache, FileAccess::READ);406407bool first = true;408if (f.is_valid()) {409//read the disk cache410while (!f->eof_reached()) {411String l = f->get_line().strip_edges();412if (first) {413if (first_scan) {414// only use this on first scan, afterwards it gets ignored415// this is so on first reimport we synchronize versions, then416// we don't care until editor restart. This is for usability mainly so417// your workflow is not killed after changing a setting by forceful reimporting418// everything there is.419filesystem_settings_version_for_import = l.strip_edges();420if (filesystem_settings_version_for_import != ResourceFormatImporter::get_singleton()->get_import_settings_hash()) {421revalidate_import_files = true;422}423}424first = false;425continue;426}427if (l.is_empty()) {428continue;429}430431if (l.begins_with("::")) {432Vector<String> split = l.split("::");433ERR_CONTINUE(split.size() != 3);434const String &name = split[1];435436cpath = name;437438} else {439// The last section (deps) may contain the same splitter, so limit the maxsplit to 8 to get the complete deps.440Vector<String> split = l.split("::", true, 8);441ERR_CONTINUE(split.size() < 9);442String name = split[0];443String file;444445file = name;446name = cpath.path_join(name);447448FileCache fc;449fc.type = split[1].get_slicec('/', 0);450fc.resource_script_class = split[1].get_slicec('/', 1);451fc.uid = split[2].to_int();452fc.modification_time = split[3].to_int();453fc.import_modification_time = split[4].to_int();454fc.import_valid = split[5].to_int() != 0;455fc.import_group_file = split[6].strip_edges();456{457const Vector<String> &slices = split[7].split("<>");458ERR_CONTINUE(slices.size() < 7);459fc.class_info.name = slices[0];460fc.class_info.extends = slices[1];461fc.class_info.icon_path = slices[2];462fc.class_info.is_abstract = slices[3].to_int();463fc.class_info.is_tool = slices[4].to_int();464fc.import_md5 = slices[5];465fc.import_dest_paths = slices[6].split("<*>");466}467fc.deps = split[8].strip_edges().split("<>", false);468469file_cache[name] = fc;470}471}472}473}474475const String update_cache = EditorPaths::get_singleton()->get_project_settings_dir().path_join("filesystem_update4");476if (first_scan && FileAccess::exists(update_cache)) {477{478Ref<FileAccess> f2 = FileAccess::open(update_cache, FileAccess::READ);479String l = f2->get_line().strip_edges();480while (!l.is_empty()) {481dep_update_list.insert(l);482file_cache.erase(l); // Erase cache for this, so it gets updated.483l = f2->get_line().strip_edges();484}485}486487Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);488d->remove(update_cache); // Bye bye update cache.489}490491EditorProgressBG scan_progress("efs", "ScanFS", 1000);492ScanProgress sp;493sp.hi = nb_files_total;494sp.progress = &scan_progress;495496new_filesystem = memnew(EditorFileSystemDirectory);497new_filesystem->parent = nullptr;498499ScannedDirectory *sd;500HashSet<String> *processed_files = nullptr;501// On the first scan, the first_scan_root_dir is created in _first_scan_filesystem.502if (first_scan) {503sd = first_scan_root_dir;504// Will be updated on scan.505ResourceUID::get_singleton()->clear();506ResourceUID::scan_for_uid_on_startup = nullptr;507processed_files = memnew(HashSet<String>());508} else {509Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);510sd = memnew(ScannedDirectory);511sd->full_path = "res://";512nb_files_total = _scan_new_dir(sd, d);513}514515_process_file_system(sd, new_filesystem, sp, processed_files);516517if (first_scan) {518_process_removed_files(*processed_files);519}520dep_update_list.clear();521file_cache.clear(); //clear caches, no longer needed522523if (first_scan) {524memdelete(first_scan_root_dir);525first_scan_root_dir = nullptr;526memdelete(processed_files);527} else {528//on the first scan this is done from the main thread after re-importing529_save_filesystem_cache();530}531532scanning = false;533}534535void EditorFileSystem::_save_filesystem_cache() {536group_file_cache.clear();537538String fscache = EditorPaths::get_singleton()->get_project_settings_dir().path_join(CACHE_FILE_NAME);539540Ref<FileAccess> f = FileAccess::open(fscache, FileAccess::WRITE);541ERR_FAIL_COND_MSG(f.is_null(), "Cannot create file '" + fscache + "'. Check user write permissions.");542543f->store_line(filesystem_settings_version_for_import);544_save_filesystem_cache(filesystem, f);545}546547void EditorFileSystem::_thread_func(void *_userdata) {548EditorFileSystem *sd = (EditorFileSystem *)_userdata;549sd->_scan_filesystem();550}551552bool 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) {553// The idea here is to trust the cache. If the last modification times in the cache correspond554// to the last modification times of the files on disk, it means the files have not changed since555// the last import, and the files in .godot/imported (p_import_dest_paths) should all be valid.556if (p_last_modification_time != p_modification_time) {557return true;558}559if (p_last_import_modification_time != p_import_modification_time) {560return true;561}562if (reimport_on_missing_imported_files) {563for (const String &path : p_import_dest_paths) {564if (!FileAccess::exists(path)) {565return true;566}567}568}569return false;570}571572bool EditorFileSystem::_test_for_reimport(const String &p_path, const String &p_expected_import_md5) {573if (p_expected_import_md5.is_empty()) {574// Marked as reimportation needed.575return true;576}577String new_md5 = FileAccess::get_md5(p_path + ".import");578if (p_expected_import_md5 != new_md5) {579return true;580}581582Error err;583Ref<FileAccess> f = FileAccess::open(p_path + ".import", FileAccess::READ, &err);584585if (f.is_null()) { // No import file, reimport.586return true;587}588589VariantParser::StreamFile stream;590stream.f = f;591592String assign;593Variant value;594VariantParser::Tag next_tag;595596int lines = 0;597String error_text;598599Vector<String> to_check;600601String importer_name;602String source_file = "";603String source_md5 = "";604Vector<String> dest_files;605String dest_md5 = "";606int version = 0;607bool found_uid = false;608Variant meta;609610while (true) {611assign = Variant();612next_tag.fields.clear();613next_tag.name = String();614615err = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true);616if (err == ERR_FILE_EOF) {617break;618} else if (err != OK) {619ERR_PRINT("ResourceFormatImporter::load - '" + p_path + ".import:" + itos(lines) + "' error '" + error_text + "'.");620// Parse error, skip and let user attempt manual reimport to avoid reimport loop.621return false;622}623624if (!assign.is_empty()) {625if (assign == "valid" && value.operator bool() == false) {626// Invalid import (failed previous import), skip and let user attempt manual reimport to avoid reimport loop.627return false;628}629if (assign.begins_with("path")) {630to_check.push_back(value);631} else if (assign == "files") {632Array fa = value;633for (const Variant &check_path : fa) {634to_check.push_back(check_path);635}636} else if (assign == "importer_version") {637version = value;638} else if (assign == "importer") {639importer_name = value;640} else if (assign == "uid") {641found_uid = true;642} else if (assign == "source_file") {643source_file = value;644} else if (assign == "dest_files") {645dest_files = value;646} else if (assign == "metadata") {647meta = value;648}649650} else if (next_tag.name != "remap" && next_tag.name != "deps") {651break;652}653}654655if (importer_name == "keep" || importer_name == "skip") {656return false; // Keep mode, do not reimport.657}658659if (!found_uid) {660return true; // UID not found, old format, reimport.661}662663// Imported files are gone, reimport.664for (const String &E : to_check) {665if (!FileAccess::exists(E)) {666return true;667}668}669670Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(importer_name);671672if (importer.is_null()) {673return true; // The importer has possibly changed, try to reimport.674}675676if (importer->get_format_version() > version) {677return true; // Version changed, reimport.678}679680if (!importer->are_import_settings_valid(p_path, meta)) {681// Reimport settings are out of sync with project settings, reimport.682return true;683}684685// Read the md5's from a separate file (so the import parameters aren't dependent on the file version).686String base_path = ResourceFormatImporter::get_singleton()->get_import_base_path(p_path);687Ref<FileAccess> md5s = FileAccess::open(base_path + ".md5", FileAccess::READ, &err);688if (md5s.is_null()) { // No md5's stored for this resource.689return true;690}691692VariantParser::StreamFile md5_stream;693md5_stream.f = md5s;694695while (true) {696assign = Variant();697next_tag.fields.clear();698next_tag.name = String();699700err = VariantParser::parse_tag_assign_eof(&md5_stream, lines, error_text, next_tag, assign, value, nullptr, true);701702if (err == ERR_FILE_EOF) {703break;704} else if (err != OK) {705ERR_PRINT("ResourceFormatImporter::load - '" + p_path + ".import.md5:" + itos(lines) + "' error '" + error_text + "'.");706return false; // Parse error.707}708if (!assign.is_empty()) {709if (assign == "source_md5") {710source_md5 = value;711} else if (assign == "dest_md5") {712dest_md5 = value;713}714}715}716717// Check source md5 matching.718if (!source_file.is_empty() && source_file != p_path) {719return true; // File was moved, reimport.720}721722if (source_md5.is_empty()) {723return true; // Lacks md5, so just reimport.724}725726String md5 = FileAccess::get_md5(p_path);727if (md5 != source_md5) {728return true;729}730731if (!dest_files.is_empty() && !dest_md5.is_empty()) {732md5 = FileAccess::get_multiple_md5(dest_files);733if (md5 != dest_md5) {734return true;735}736}737738return false; // Nothing changed.739}740741Vector<String> EditorFileSystem::_get_import_dest_paths(const String &p_path) {742Error err;743Ref<FileAccess> f = FileAccess::open(p_path + ".import", FileAccess::READ, &err);744745if (f.is_null()) { // No import file, reimport.746return Vector<String>();747}748749VariantParser::StreamFile stream;750stream.f = f;751752String assign;753Variant value;754VariantParser::Tag next_tag;755756int lines = 0;757String error_text;758759Vector<String> dest_paths;760String importer_name;761762while (true) {763assign = Variant();764next_tag.fields.clear();765next_tag.name = String();766767err = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true);768if (err == ERR_FILE_EOF) {769break;770} else if (err != OK) {771ERR_PRINT("ResourceFormatImporter::load - '" + p_path + ".import:" + itos(lines) + "' error '" + error_text + "'.");772// Parse error, skip and let user attempt manual reimport to avoid reimport loop.773return Vector<String>();774}775776if (!assign.is_empty()) {777if (assign == "valid" && value.operator bool() == false) {778// Invalid import (failed previous import), skip and let user attempt manual reimport to avoid reimport loop.779return Vector<String>();780}781if (assign.begins_with("path")) {782dest_paths.push_back(value);783} else if (assign == "files") {784Array fa = value;785for (const Variant &dest_path : fa) {786dest_paths.push_back(dest_path);787}788} else if (assign == "importer") {789importer_name = value;790}791} else if (next_tag.name != "remap" && next_tag.name != "deps") {792break;793}794}795796if (importer_name == "keep" || importer_name == "skip") {797return Vector<String>();798}799800return dest_paths;801}802803bool EditorFileSystem::_scan_import_support(const Vector<String> &reimports) {804if (import_support_queries.is_empty()) {805return false;806}807HashMap<String, int> import_support_test;808Vector<bool> import_support_tested;809import_support_tested.resize(import_support_queries.size());810for (int i = 0; i < import_support_queries.size(); i++) {811import_support_tested.write[i] = false;812if (import_support_queries[i]->is_active()) {813Vector<String> extensions = import_support_queries[i]->get_file_extensions();814for (int j = 0; j < extensions.size(); j++) {815import_support_test.insert(extensions[j], i);816}817}818}819820if (import_support_test.is_empty()) {821return false; //well nothing to do822}823824for (int i = 0; i < reimports.size(); i++) {825HashMap<String, int>::Iterator E = import_support_test.find(reimports[i].get_extension().to_lower());826if (E) {827import_support_tested.write[E->value] = true;828}829}830831for (int i = 0; i < import_support_tested.size(); i++) {832if (import_support_tested[i]) {833if (import_support_queries.write[i]->query()) {834return true;835}836}837}838839return false;840}841842bool EditorFileSystem::_update_scan_actions() {843sources_changed.clear();844845// We need to update the script global class names before the reimports to be sure that846// all the importer classes that depends on class names will work.847_update_script_classes();848849bool fs_changed = false;850851Vector<String> reimports;852Vector<String> reloads;853854EditorProgress *ep = nullptr;855if (scan_actions.size() > 1) {856ep = memnew(EditorProgress("_update_scan_actions", TTR("Scanning actions..."), scan_actions.size()));857}858859int step_count = 0;860for (const ItemAction &ia : scan_actions) {861switch (ia.action) {862case ItemAction::ACTION_NONE: {863} break;864case ItemAction::ACTION_DIR_ADD: {865int idx = 0;866for (int i = 0; i < ia.dir->subdirs.size(); i++) {867if (ia.new_dir->name.filenocasecmp_to(ia.dir->subdirs[i]->name) < 0) {868break;869}870idx++;871}872if (idx == ia.dir->subdirs.size()) {873ia.dir->subdirs.push_back(ia.new_dir);874} else {875ia.dir->subdirs.insert(idx, ia.new_dir);876}877878fs_changed = true;879} break;880case ItemAction::ACTION_DIR_REMOVE: {881ERR_CONTINUE(!ia.dir->parent);882ia.dir->parent->subdirs.erase(ia.dir);883memdelete(ia.dir);884fs_changed = true;885} break;886case ItemAction::ACTION_FILE_ADD: {887int idx = 0;888for (int i = 0; i < ia.dir->files.size(); i++) {889if (ia.new_file->file.filenocasecmp_to(ia.dir->files[i]->file) < 0) {890break;891}892idx++;893}894if (idx == ia.dir->files.size()) {895ia.dir->files.push_back(ia.new_file);896} else {897ia.dir->files.insert(idx, ia.new_file);898}899900fs_changed = true;901902const String new_file_path = ia.dir->get_file_path(idx);903const ResourceUID::ID existing_id = ResourceLoader::get_resource_uid(new_file_path);904if (existing_id != ResourceUID::INVALID_ID) {905const String old_path = ResourceUID::get_singleton()->get_id_path(existing_id);906if (old_path != new_file_path && FileAccess::exists(old_path)) {907const ResourceUID::ID new_id = ResourceUID::get_singleton()->create_id_for_path(new_file_path);908ResourceUID::get_singleton()->add_id(new_id, new_file_path);909ResourceSaver::set_uid(new_file_path, new_id);910WARN_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));911} else {912// Re-assign the UID to file, just in case it was pulled from cache.913ResourceSaver::set_uid(new_file_path, existing_id);914}915} else if (ResourceLoader::should_create_uid_file(new_file_path)) {916Ref<FileAccess> f = FileAccess::open(new_file_path + ".uid", FileAccess::WRITE);917if (f.is_valid()) {918ia.new_file->uid = ResourceUID::get_singleton()->create_id_for_path(new_file_path);919f->store_line(ResourceUID::get_singleton()->id_to_text(ia.new_file->uid));920}921}922923if (ClassDB::is_parent_class(ia.new_file->type, SNAME("Script"))) {924_queue_update_script_class(new_file_path, ScriptClassInfoUpdate::from_file_info(ia.new_file));925}926if (ia.new_file->type == SNAME("PackedScene")) {927_queue_update_scene_groups(new_file_path);928}929930} break;931case ItemAction::ACTION_FILE_REMOVE: {932int idx = ia.dir->find_file_index(ia.file);933ERR_CONTINUE(idx == -1);934935const String file_path = ia.dir->get_file_path(idx);936const String class_name = ia.dir->files[idx]->class_info.name;937if (ClassDB::is_parent_class(ia.dir->files[idx]->type, SNAME("Script"))) {938_queue_update_script_class(file_path, ScriptClassInfoUpdate());939}940if (ia.dir->files[idx]->type == SNAME("PackedScene")) {941_queue_update_scene_groups(file_path);942}943944_delete_internal_files(file_path);945memdelete(ia.dir->files[idx]);946ia.dir->files.remove_at(idx);947948// Restore another script with the same global class name if it exists.949if (!class_name.is_empty()) {950EditorFileSystemDirectory::FileInfo *old_fi = nullptr;951String old_file = _get_file_by_class_name(filesystem, class_name, old_fi);952if (!old_file.is_empty() && old_fi) {953_queue_update_script_class(old_file, ScriptClassInfoUpdate::from_file_info(old_fi));954}955}956957fs_changed = true;958959} break;960case ItemAction::ACTION_FILE_TEST_REIMPORT: {961int idx = ia.dir->find_file_index(ia.file);962ERR_CONTINUE(idx == -1);963String full_path = ia.dir->get_file_path(idx);964965bool need_reimport = _test_for_reimport(full_path, ia.dir->files[idx]->import_md5);966if (need_reimport) {967// Must reimport.968reimports.push_back(full_path);969Vector<String> dependencies = _get_dependencies(full_path);970for (const String &dep : dependencies) {971const String &dependency_path = dep.contains("::") ? dep.get_slice("::", 0) : dep;972if (_can_import_file(dep)) {973reimports.push_back(dependency_path);974}975}976} else {977// Must not reimport, all was good.978// Update modified times, md5 and destination paths, to avoid reimport.979ia.dir->files[idx]->modified_time = FileAccess::get_modified_time(full_path);980ia.dir->files[idx]->import_modified_time = FileAccess::get_modified_time(full_path + ".import");981if (ia.dir->files[idx]->import_md5.is_empty()) {982ia.dir->files[idx]->import_md5 = FileAccess::get_md5(full_path + ".import");983}984ia.dir->files[idx]->import_dest_paths = _get_import_dest_paths(full_path);985}986987fs_changed = true;988} break;989case ItemAction::ACTION_FILE_RELOAD: {990int idx = ia.dir->find_file_index(ia.file);991ERR_CONTINUE(idx == -1);992993// Only reloads the resources that are already loaded.994if (ResourceCache::has(ia.dir->get_file_path(idx))) {995reloads.push_back(ia.dir->get_file_path(idx));996}997} break;998}9991000if (ep) {1001ep->step(ia.file, step_count++, false);1002}1003}10041005memdelete_notnull(ep);10061007if (_scan_extensions()) {1008//needs editor restart1009//extensions also may provide filetypes to be imported, so they must run before importing1010if (EditorNode::immediate_confirmation_dialog(TTR("Some extensions need the editor to restart to take effect."), first_scan ? TTR("Restart") : TTR("Save & Restart"), TTR("Continue"))) {1011if (!first_scan) {1012EditorNode::get_singleton()->save_all_scenes();1013}1014EditorNode::get_singleton()->restart_editor();1015//do not import1016return true;1017}1018}10191020if (!reimports.is_empty()) {1021if (_scan_import_support(reimports)) {1022return true;1023}10241025reimport_files(reimports);1026} else {1027//reimport files will update the uid cache file so if nothing was reimported, update it manually1028ResourceUID::get_singleton()->update_cache();1029}10301031if (!reloads.is_empty()) {1032// Update global class names, dependencies, etc...1033update_files(reloads);1034}10351036if (first_scan) {1037//only on first scan this is valid and updated, then settings changed.1038revalidate_import_files = false;1039filesystem_settings_version_for_import = ResourceFormatImporter::get_singleton()->get_import_settings_hash();1040_save_filesystem_cache();1041}10421043// Moving the processing of pending updates before the resources_reload event to be sure all global class names1044// are updated. Script.cpp listens on resources_reload and reloads updated scripts.1045_process_update_pending();10461047if (reloads.size()) {1048emit_signal(SNAME("resources_reload"), reloads);1049}1050scan_actions.clear();10511052return fs_changed;1053}10541055void EditorFileSystem::scan() {1056if (false /*&& bool(Globals::get_singleton()->get("debug/disable_scan"))*/) {1057return;1058}10591060if (scanning || scanning_changes || thread.is_started()) {1061return;1062}10631064// The first scan must be on the main thread because, after the first scan and update1065// of global class names, we load the plugins and autoloads. These need to1066// be added on the main thread because they are nodes, and we need to wait for them1067// to be loaded to continue the scan and reimportations.1068if (first_scan) {1069_first_scan_filesystem();1070#ifdef ANDROID_ENABLED1071// Create a .nomedia file to hide assets from media apps on Android.1072// Android 11 has some issues with nomedia files, so it's disabled there. See GH-106479 and GH-105399 for details.1073// NOTE: Nomedia file is also handled in project manager. See project_dialog.cpp -> ProjectDialog::ok_pressed().1074String sdk_version = OS::get_singleton()->get_version().get_slicec('.', 0);1075if (sdk_version != "30") {1076const String nomedia_file_path = ProjectSettings::get_singleton()->get_resource_path().path_join(".nomedia");1077if (!FileAccess::exists(nomedia_file_path)) {1078Ref<FileAccess> f = FileAccess::open(nomedia_file_path, FileAccess::WRITE);1079if (f.is_null()) {1080// .nomedia isn't so critical.1081ERR_PRINT("Couldn't create .nomedia in project path.");1082} else {1083f->close();1084}1085}1086}1087#endif1088}10891090_update_extensions();10911092if (!use_threads) {1093scanning = true;1094scan_total = 0;1095_scan_filesystem();1096if (filesystem) {1097memdelete(filesystem);1098}1099//file_type_cache.clear();1100filesystem = new_filesystem;1101new_filesystem = nullptr;1102_update_scan_actions();1103// Update all icons so they are loaded for the FileSystemDock.1104_update_files_icon_path();1105scanning = false;1106// Set first_scan to false before the signals so the function doing_first_scan can return false1107// in editor_node to start the export if needed.1108first_scan = false;1109ResourceImporter::load_on_startup = nullptr;1110emit_signal(SNAME("filesystem_changed"));1111emit_signal(SNAME("sources_changed"), sources_changed.size() > 0);1112} else {1113ERR_FAIL_COND(thread.is_started());1114set_process(true);1115Thread::Settings s;1116scanning = true;1117scan_total = 0;1118s.priority = Thread::PRIORITY_LOW;1119thread.start(_thread_func, this, s);1120}1121}11221123void EditorFileSystem::ScanProgress::increment() {1124current++;1125float ratio = current / MAX(hi, 1.0f);1126if (progress) {1127progress->step(ratio * 1000.0f);1128}1129EditorFileSystem::singleton->scan_total = ratio;1130}11311132int EditorFileSystem::_scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da) {1133List<String> dirs;1134List<String> files;11351136String cd = da->get_current_dir();11371138da->list_dir_begin();1139while (true) {1140String f = da->get_next();1141if (f.is_empty()) {1142break;1143}11441145if (da->current_is_hidden()) {1146continue;1147}11481149if (da->current_is_dir()) {1150if (f.begins_with(".")) { // Ignore special and . / ..1151continue;1152}11531154if (_should_skip_directory(cd.path_join(f))) {1155continue;1156}11571158dirs.push_back(f);11591160} else {1161files.push_back(f);1162}1163}11641165da->list_dir_end();11661167dirs.sort_custom<FileNoCaseComparator>();1168files.sort_custom<FileNoCaseComparator>();11691170int nb_files_total_scan = 0;11711172for (const String &dir : dirs) {1173if (da->change_dir(dir) == OK) {1174String d = da->get_current_dir();11751176if (d == cd || !d.begins_with(cd)) {1177da->change_dir(cd); //avoid recursion1178} else {1179ScannedDirectory *sd = memnew(ScannedDirectory);1180sd->name = dir;1181sd->full_path = p_dir->full_path.path_join(sd->name);11821183nb_files_total_scan += _scan_new_dir(sd, da);11841185p_dir->subdirs.push_back(sd);11861187da->change_dir("..");1188}1189} else {1190ERR_PRINT("Cannot go into subdir '" + dir + "'.");1191}1192}11931194p_dir->files = files;1195nb_files_total_scan += files.size();11961197return nb_files_total_scan;1198}11991200void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress, HashSet<String> *r_processed_files) {1201p_dir->modified_time = FileAccess::get_modified_time(p_scan_dir->full_path);12021203for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) {1204EditorFileSystemDirectory *sub_dir = memnew(EditorFileSystemDirectory);1205sub_dir->parent = p_dir;1206sub_dir->name = scan_sub_dir->name;1207p_dir->subdirs.push_back(sub_dir);1208_process_file_system(scan_sub_dir, sub_dir, p_progress, r_processed_files);1209}12101211for (const String &scan_file : p_scan_dir->files) {1212String ext = scan_file.get_extension().to_lower();1213if (!valid_extensions.has(ext)) {1214p_progress.increment();1215continue; //invalid1216}12171218String path = p_scan_dir->full_path.path_join(scan_file);12191220EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo);1221fi->file = scan_file;1222p_dir->files.push_back(fi);12231224if (r_processed_files) {1225r_processed_files->insert(path);1226}12271228FileCache *fc = file_cache.getptr(path);1229uint64_t mt = FileAccess::get_modified_time(path);12301231if (_can_import_file(scan_file)) {1232//is imported1233uint64_t import_mt = FileAccess::get_modified_time(path + ".import");12341235if (fc) {1236fi->type = fc->type;1237fi->resource_script_class = fc->resource_script_class;1238fi->uid = fc->uid;1239fi->deps = fc->deps;1240fi->modified_time = mt;1241fi->import_modified_time = import_mt;1242fi->import_md5 = fc->import_md5;1243fi->import_dest_paths = fc->import_dest_paths;1244fi->import_valid = fc->import_valid;1245fi->import_group_file = fc->import_group_file;1246fi->class_info = fc->class_info;12471248// Ensures backward compatibility when the project is loaded for the first time with the added import_md51249// and import_dest_paths properties in the file cache.1250if (fc->import_md5.is_empty()) {1251fi->import_md5 = FileAccess::get_md5(path + ".import");1252fi->import_dest_paths = _get_import_dest_paths(path);1253}12541255// The method _is_test_for_reimport_needed checks if the files were modified and ensures that1256// all the destination files still exist without reading the .import file.1257// If something is different, we will queue a test for reimportation that will check1258// the md5 of all files and import settings and, if necessary, execute a reimportation.1259if (_is_test_for_reimport_needed(path, fc->modification_time, mt, fc->import_modification_time, import_mt, fi->import_dest_paths) ||1260(revalidate_import_files && !ResourceFormatImporter::get_singleton()->are_import_settings_valid(path))) {1261ItemAction ia;1262ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;1263ia.dir = p_dir;1264ia.file = fi->file;1265scan_actions.push_back(ia);1266}12671268if (fc->type.is_empty()) {1269fi->type = ResourceLoader::get_resource_type(path);1270fi->resource_script_class = ResourceLoader::get_resource_script_class(path);1271fi->import_group_file = ResourceLoader::get_import_group_file(path);1272//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?)1273//note: I think this should not happen any longer..1274}12751276if (fc->uid == ResourceUID::INVALID_ID) {1277// imported files should always have a UID, so attempt to fetch it.1278fi->uid = ResourceLoader::get_resource_uid(path);1279}12801281} else {1282// Using get_resource_import_info() to prevent calling 3 times ResourceFormatImporter::_get_path_and_type.1283ResourceFormatImporter::get_singleton()->get_resource_import_info(path, fi->type, fi->uid, fi->import_group_file);1284fi->class_info = _get_global_script_class(fi->type, path);1285fi->modified_time = 0;1286fi->import_modified_time = 0;1287fi->import_md5 = FileAccess::get_md5(path + ".import");1288fi->import_dest_paths = Vector<String>();1289fi->import_valid = (fi->type == "TextFile" || fi->type == "OtherFile") ? true : ResourceLoader::is_import_valid(path);12901291ItemAction ia;1292ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;1293ia.dir = p_dir;1294ia.file = fi->file;1295scan_actions.push_back(ia);1296}1297} else {1298if (fc && fc->modification_time == mt) {1299//not imported, so just update type if changed1300fi->type = fc->type;1301fi->resource_script_class = fc->resource_script_class;1302fi->uid = fc->uid;1303fi->modified_time = mt;1304fi->deps = fc->deps;1305fi->import_modified_time = 0;1306fi->import_md5 = "";1307fi->import_dest_paths = Vector<String>();1308fi->import_valid = true;1309fi->class_info = fc->class_info;13101311if (first_scan && ClassDB::is_parent_class(fi->type, SNAME("Script"))) {1312bool update_script = false;1313String old_class_name = fi->class_info.name;1314fi->class_info = _get_global_script_class(fi->type, path);1315if (old_class_name != fi->class_info.name) {1316update_script = true;1317} 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)) {1318// This script has a class name but is not in the global class names or the path of the class has changed.1319update_script = true;1320}1321if (update_script) {1322_queue_update_script_class(path, ScriptClassInfoUpdate::from_file_info(fi));1323}1324}1325} else {1326//new or modified time1327fi->type = ResourceLoader::get_resource_type(path);1328fi->resource_script_class = ResourceLoader::get_resource_script_class(path);1329if (fi->type == "" && textfile_extensions.has(ext)) {1330fi->type = "TextFile";1331}1332if (fi->type == "" && other_file_extensions.has(ext)) {1333fi->type = "OtherFile";1334}1335fi->uid = ResourceLoader::get_resource_uid(path);1336fi->class_info = _get_global_script_class(fi->type, path);1337fi->deps = _get_dependencies(path);1338fi->modified_time = mt;1339fi->import_modified_time = 0;1340fi->import_md5 = "";1341fi->import_dest_paths = Vector<String>();1342fi->import_valid = true;13431344// Files in dep_update_list are forced for rescan to update dependencies. They don't need other updates.1345if (!dep_update_list.has(path)) {1346if (ClassDB::is_parent_class(fi->type, SNAME("Script"))) {1347_queue_update_script_class(path, ScriptClassInfoUpdate::from_file_info(fi));1348}1349if (fi->type == SNAME("PackedScene")) {1350_queue_update_scene_groups(path);1351}1352}1353}13541355if (ResourceLoader::should_create_uid_file(path)) {1356// Create a UID file and new UID, if it's invalid.1357Ref<FileAccess> f = FileAccess::open(path + ".uid", FileAccess::WRITE);1358if (f.is_valid()) {1359if (fi->uid == ResourceUID::INVALID_ID) {1360fi->uid = ResourceUID::get_singleton()->create_id_for_path(path);1361} else {1362WARN_PRINT(vformat("Missing .uid file for path \"%s\". The file was re-created from cache.", path));1363}1364f->store_line(ResourceUID::get_singleton()->id_to_text(fi->uid));1365}1366}1367}13681369if (fi->uid != ResourceUID::INVALID_ID) {1370if (ResourceUID::get_singleton()->has_id(fi->uid)) {1371// Restrict UID dupe warning to first-scan since we know there are no file moves going on yet.1372if (first_scan) {1373// Warn if we detect files with duplicate UIDs.1374const String other_path = ResourceUID::get_singleton()->get_id_path(fi->uid);1375if (other_path != path) {1376WARN_PRINT(vformat("UID duplicate detected between %s and %s.", path, other_path));1377}1378}1379ResourceUID::get_singleton()->set_id(fi->uid, path);1380} else {1381ResourceUID::get_singleton()->add_id(fi->uid, path);1382}1383}13841385p_progress.increment();1386}1387}13881389void EditorFileSystem::_process_removed_files(const HashSet<String> &p_processed_files) {1390for (const KeyValue<String, EditorFileSystem::FileCache> &kv : file_cache) {1391if (!p_processed_files.has(kv.key)) {1392if (ClassDB::is_parent_class(kv.value.type, SNAME("Script")) || ClassDB::is_parent_class(kv.value.type, SNAME("PackedScene"))) {1393// A script has been removed from disk since the last startup. The documentation needs to be updated.1394// There's no need to add the path in update_script_paths since that is exclusively for updating global class names,1395// which is handled in _first_scan_filesystem before the full scan to ensure plugins and autoloads can be created.1396MutexLock update_script_lock(update_script_mutex);1397update_script_paths_documentation.insert(kv.key);1398}1399}1400}1401}14021403void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanProgress &p_progress, bool p_recursive) {1404uint64_t current_mtime = FileAccess::get_modified_time(p_dir->get_path());14051406bool updated_dir = false;1407String cd = p_dir->get_path();1408int diff_nb_files = 0;14091410if (current_mtime != p_dir->modified_time || using_fat32_or_exfat) {1411updated_dir = true;1412p_dir->modified_time = current_mtime;1413//ooooops, dir changed, see what's going on14141415//first mark everything as verified14161417for (int i = 0; i < p_dir->files.size(); i++) {1418p_dir->files[i]->verified = false;1419}14201421for (int i = 0; i < p_dir->subdirs.size(); i++) {1422p_dir->get_subdir(i)->verified = false;1423}14241425diff_nb_files -= p_dir->files.size();14261427//then scan files and directories and check what's different14281429Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);14301431Error ret = da->change_dir(cd);1432ERR_FAIL_COND_MSG(ret != OK, "Cannot change to '" + cd + "' folder.");14331434da->list_dir_begin();1435while (true) {1436String f = da->get_next();1437if (f.is_empty()) {1438break;1439}14401441if (da->current_is_hidden()) {1442continue;1443}14441445if (da->current_is_dir()) {1446if (f.begins_with(".")) { // Ignore special and . / ..1447continue;1448}14491450int idx = p_dir->find_dir_index(f);1451if (idx == -1) {1452String dir_path = cd.path_join(f);1453if (_should_skip_directory(dir_path)) {1454continue;1455}14561457ScannedDirectory sd;1458sd.name = f;1459sd.full_path = dir_path;14601461EditorFileSystemDirectory *efd = memnew(EditorFileSystemDirectory);1462efd->parent = p_dir;1463efd->name = f;14641465Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);1466d->change_dir(dir_path);1467int nb_files_dir = _scan_new_dir(&sd, d);1468p_progress.hi += nb_files_dir;1469diff_nb_files += nb_files_dir;1470_process_file_system(&sd, efd, p_progress, nullptr);14711472ItemAction ia;1473ia.action = ItemAction::ACTION_DIR_ADD;1474ia.dir = p_dir;1475ia.file = f;1476ia.new_dir = efd;1477scan_actions.push_back(ia);1478} else {1479p_dir->subdirs[idx]->verified = true;1480}14811482} else {1483String ext = f.get_extension().to_lower();1484if (!valid_extensions.has(ext)) {1485continue; //invalid1486}14871488int idx = p_dir->find_file_index(f);14891490if (idx == -1) {1491//never seen this file, add actition to add it1492EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo);1493fi->file = f;14941495String path = cd.path_join(fi->file);1496fi->modified_time = FileAccess::get_modified_time(path);1497fi->import_modified_time = 0;1498fi->import_md5 = "";1499fi->import_dest_paths = Vector<String>();1500fi->type = ResourceLoader::get_resource_type(path);1501fi->resource_script_class = ResourceLoader::get_resource_script_class(path);1502if (fi->type == "" && textfile_extensions.has(ext)) {1503fi->type = "TextFile";1504}1505if (fi->type == "" && other_file_extensions.has(ext)) {1506fi->type = "OtherFile";1507}1508fi->class_info = _get_global_script_class(fi->type, path);1509fi->import_valid = (fi->type == "TextFile" || fi->type == "OtherFile") ? true : ResourceLoader::is_import_valid(path);1510fi->import_group_file = ResourceLoader::get_import_group_file(path);15111512{1513ItemAction ia;1514ia.action = ItemAction::ACTION_FILE_ADD;1515ia.dir = p_dir;1516ia.file = f;1517ia.new_file = fi;1518scan_actions.push_back(ia);1519}15201521if (_can_import_file(f)) {1522//if it can be imported, and it was added, it needs to be reimported1523ItemAction ia;1524ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;1525ia.dir = p_dir;1526ia.file = f;1527scan_actions.push_back(ia);1528}1529diff_nb_files++;1530} else {1531p_dir->files[idx]->verified = true;1532}1533}1534}15351536da->list_dir_end();1537}15381539for (int i = 0; i < p_dir->files.size(); i++) {1540if (updated_dir && !p_dir->files[i]->verified) {1541//this file was removed, add action to remove it1542ItemAction ia;1543ia.action = ItemAction::ACTION_FILE_REMOVE;1544ia.dir = p_dir;1545ia.file = p_dir->files[i]->file;1546scan_actions.push_back(ia);1547diff_nb_files--;1548continue;1549}15501551String path = cd.path_join(p_dir->files[i]->file);15521553if (_can_import_file(p_dir->files[i]->file)) {1554// Check here if file must be imported or not.1555// Same logic as in _process_file_system, the last modifications dates1556// needs to be trusted to prevent reading all the .import files and the md51557// each time the user switch back to Godot.1558uint64_t mt = FileAccess::get_modified_time(path);1559uint64_t import_mt = FileAccess::get_modified_time(path + ".import");1560if (_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)) {1561ItemAction ia;1562ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;1563ia.dir = p_dir;1564ia.file = p_dir->files[i]->file;1565scan_actions.push_back(ia);1566}1567} else {1568uint64_t mt = FileAccess::get_modified_time(path);15691570if (mt != p_dir->files[i]->modified_time) {1571p_dir->files[i]->modified_time = mt; //save new time, but test for reload15721573ItemAction ia;1574ia.action = ItemAction::ACTION_FILE_RELOAD;1575ia.dir = p_dir;1576ia.file = p_dir->files[i]->file;1577scan_actions.push_back(ia);1578}1579}15801581p_progress.increment();1582}15831584for (int i = 0; i < p_dir->subdirs.size(); i++) {1585if ((updated_dir && !p_dir->subdirs[i]->verified) || _should_skip_directory(p_dir->subdirs[i]->get_path())) {1586// Add all the files of the folder to be sure _update_scan_actions process the removed files1587// for global class names.1588diff_nb_files += _insert_actions_delete_files_directory(p_dir->subdirs[i]);15891590//this directory was removed or ignored, add action to remove it1591ItemAction ia;1592ia.action = ItemAction::ACTION_DIR_REMOVE;1593ia.dir = p_dir->subdirs[i];1594scan_actions.push_back(ia);1595continue;1596}1597if (p_recursive) {1598_scan_fs_changes(p_dir->get_subdir(i), p_progress);1599}1600}16011602nb_files_total = MAX(nb_files_total + diff_nb_files, 0);1603}16041605void EditorFileSystem::_delete_internal_files(const String &p_file) {1606if (FileAccess::exists(p_file + ".import")) {1607List<String> paths;1608ResourceFormatImporter::get_singleton()->get_internal_resource_path_list(p_file, &paths);1609Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);1610for (const String &E : paths) {1611da->remove(E);1612}1613da->remove(p_file + ".import");1614}1615if (FileAccess::exists(p_file + ".uid")) {1616DirAccess::remove_absolute(p_file + ".uid");1617}1618}16191620int EditorFileSystem::_insert_actions_delete_files_directory(EditorFileSystemDirectory *p_dir) {1621int nb_files = 0;1622for (EditorFileSystemDirectory::FileInfo *fi : p_dir->files) {1623ItemAction ia;1624ia.action = ItemAction::ACTION_FILE_REMOVE;1625ia.dir = p_dir;1626ia.file = fi->file;1627scan_actions.push_back(ia);1628nb_files++;1629}16301631for (EditorFileSystemDirectory *sub_dir : p_dir->subdirs) {1632nb_files += _insert_actions_delete_files_directory(sub_dir);1633}16341635return nb_files;1636}16371638void EditorFileSystem::_thread_func_sources(void *_userdata) {1639EditorFileSystem *efs = (EditorFileSystem *)_userdata;1640if (efs->filesystem) {1641EditorProgressBG pr("sources", TTR("ScanSources"), 1000);1642ScanProgress sp;1643sp.progress = ≺1644sp.hi = efs->nb_files_total;1645efs->_scan_fs_changes(efs->filesystem, sp);1646}1647efs->scanning_changes_done.set();1648}16491650bool EditorFileSystem::_remove_invalid_global_class_names(const HashSet<String> &p_existing_class_names) {1651List<StringName> global_classes;1652bool must_save = false;1653ScriptServer::get_global_class_list(&global_classes);1654for (const StringName &class_name : global_classes) {1655if (!p_existing_class_names.has(class_name)) {1656ScriptServer::remove_global_class(class_name);1657must_save = true;1658}1659}1660return must_save;1661}16621663String EditorFileSystem::_get_file_by_class_name(EditorFileSystemDirectory *p_dir, const String &p_class_name, EditorFileSystemDirectory::FileInfo *&r_file_info) {1664for (EditorFileSystemDirectory::FileInfo *fi : p_dir->files) {1665if (fi->class_info.name == p_class_name) {1666r_file_info = fi;1667return p_dir->get_path().path_join(fi->file);1668}1669}16701671for (EditorFileSystemDirectory *sub_dir : p_dir->subdirs) {1672String file = _get_file_by_class_name(sub_dir, p_class_name, r_file_info);1673if (!file.is_empty()) {1674return file;1675}1676}1677r_file_info = nullptr;1678return "";1679}16801681void EditorFileSystem::scan_changes() {1682if (first_scan || // Prevent a premature changes scan from inhibiting the first full scan1683scanning || scanning_changes || thread.is_started()) {1684scan_changes_pending = true;1685set_process(true);1686return;1687}16881689_update_extensions();1690sources_changed.clear();1691scanning_changes = true;1692scanning_changes_done.clear();16931694if (!use_threads) {1695if (filesystem) {1696EditorProgressBG pr("sources", TTR("ScanSources"), 1000);1697ScanProgress sp;1698sp.progress = ≺1699sp.hi = nb_files_total;1700scan_total = 0;1701_scan_fs_changes(filesystem, sp);1702if (_update_scan_actions()) {1703emit_signal(SNAME("filesystem_changed"));1704}1705}1706scanning_changes = false;1707scanning_changes_done.set();1708emit_signal(SNAME("sources_changed"), sources_changed.size() > 0);1709} else {1710ERR_FAIL_COND(thread_sources.is_started());1711set_process(true);1712scan_total = 0;1713Thread::Settings s;1714s.priority = Thread::PRIORITY_LOW;1715thread_sources.start(_thread_func_sources, this, s);1716}1717}17181719void EditorFileSystem::_notification(int p_what) {1720switch (p_what) {1721case NOTIFICATION_EXIT_TREE: {1722Thread &active_thread = thread.is_started() ? thread : thread_sources;1723if (use_threads && active_thread.is_started()) {1724while (scanning) {1725OS::get_singleton()->delay_usec(1000);1726}1727active_thread.wait_to_finish();1728WARN_PRINT("Scan thread aborted...");1729set_process(false);1730}17311732if (filesystem) {1733memdelete(filesystem);1734}1735if (new_filesystem) {1736memdelete(new_filesystem);1737}1738filesystem = nullptr;1739new_filesystem = nullptr;1740} break;17411742case NOTIFICATION_PROCESS: {1743if (use_threads) {1744/** This hack exists because of the EditorProgress nature1745* of processing events recursively. This needs to be rewritten1746* at some point entirely, but in the meantime the following1747* hack prevents deadlock on import.1748*/17491750static bool prevent_recursive_process_hack = false;1751if (prevent_recursive_process_hack) {1752break;1753}17541755prevent_recursive_process_hack = true;17561757bool done_importing = false;17581759if (scanning_changes) {1760if (scanning_changes_done.is_set()) {1761set_process(false);17621763if (thread_sources.is_started()) {1764thread_sources.wait_to_finish();1765}1766bool changed = _update_scan_actions();1767// Set first_scan to false before the signals so the function doing_first_scan can return false1768// in editor_node to start the export if needed.1769first_scan = false;1770scanning_changes = false;1771done_importing = true;1772ResourceImporter::load_on_startup = nullptr;1773if (changed) {1774emit_signal(SNAME("filesystem_changed"));1775}1776emit_signal(SNAME("sources_changed"), sources_changed.size() > 0);1777}1778} else if (!scanning && thread.is_started()) {1779set_process(false);17801781if (filesystem) {1782memdelete(filesystem);1783}1784filesystem = new_filesystem;1785new_filesystem = nullptr;1786thread.wait_to_finish();1787_update_scan_actions();1788// Update all icons so they are loaded for the FileSystemDock.1789_update_files_icon_path();1790// Set first_scan to false before the signals so the function doing_first_scan can return false1791// in editor_node to start the export if needed.1792first_scan = false;1793ResourceImporter::load_on_startup = nullptr;1794emit_signal(SNAME("filesystem_changed"));1795emit_signal(SNAME("sources_changed"), sources_changed.size() > 0);1796}17971798if (done_importing && scan_changes_pending) {1799scan_changes_pending = false;1800scan_changes();1801}18021803prevent_recursive_process_hack = false;1804}1805} break;1806}1807}18081809bool EditorFileSystem::is_scanning() const {1810return scanning || scanning_changes || first_scan;1811}18121813float EditorFileSystem::get_scanning_progress() const {1814return scan_total;1815}18161817EditorFileSystemDirectory *EditorFileSystem::get_filesystem() {1818return filesystem;1819}18201821void EditorFileSystem::_save_filesystem_cache(EditorFileSystemDirectory *p_dir, Ref<FileAccess> p_file) {1822if (!p_dir) {1823return; //none1824}1825p_file->store_line("::" + p_dir->get_path() + "::" + String::num_int64(p_dir->modified_time));18261827for (int i = 0; i < p_dir->files.size(); i++) {1828const EditorFileSystemDirectory::FileInfo *file_info = p_dir->files[i];1829if (!file_info->import_group_file.is_empty()) {1830group_file_cache.insert(file_info->import_group_file);1831}18321833String type = file_info->type;1834if (file_info->resource_script_class) {1835type += "/" + String(file_info->resource_script_class);1836}18371838PackedStringArray cache_string;1839cache_string.append(file_info->file);1840cache_string.append(type);1841cache_string.append(itos(file_info->uid));1842cache_string.append(itos(file_info->modified_time));1843cache_string.append(itos(file_info->import_modified_time));1844cache_string.append(itos(file_info->import_valid));1845cache_string.append(file_info->import_group_file);1846cache_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) }));1847cache_string.append(String("<>").join(file_info->deps));18481849p_file->store_line(String("::").join(cache_string));1850}18511852for (int i = 0; i < p_dir->subdirs.size(); i++) {1853_save_filesystem_cache(p_dir->subdirs[i], p_file);1854}1855}18561857bool EditorFileSystem::_find_file(const String &p_file, EditorFileSystemDirectory **r_d, int &r_file_pos) const {1858//todo make faster18591860if (!filesystem || scanning) {1861return false;1862}18631864String f = ProjectSettings::get_singleton()->localize_path(p_file);18651866// Note: Only checks if base directory is case sensitive.1867Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);1868bool fs_case_sensitive = dir->is_case_sensitive("res://");18691870if (!f.begins_with("res://")) {1871return false;1872}1873f = f.substr(6);1874f = f.replace_char('\\', '/');18751876Vector<String> path = f.split("/");18771878if (path.is_empty()) {1879return false;1880}1881String file = path[path.size() - 1];1882path.resize(path.size() - 1);18831884EditorFileSystemDirectory *fs = filesystem;18851886for (int i = 0; i < path.size(); i++) {1887if (path[i].begins_with(".")) {1888return false;1889}18901891int idx = -1;1892for (int j = 0; j < fs->get_subdir_count(); j++) {1893if (fs_case_sensitive) {1894if (fs->get_subdir(j)->get_name() == path[i]) {1895idx = j;1896break;1897}1898} else {1899if (fs->get_subdir(j)->get_name().to_lower() == path[i].to_lower()) {1900idx = j;1901break;1902}1903}1904}19051906if (idx == -1) {1907// Only create a missing directory in memory when it exists on disk.1908if (!dir->dir_exists(fs->get_path().path_join(path[i]))) {1909return false;1910}1911EditorFileSystemDirectory *efsd = memnew(EditorFileSystemDirectory);19121913efsd->name = path[i];1914efsd->parent = fs;19151916int idx2 = 0;1917for (int j = 0; j < fs->get_subdir_count(); j++) {1918if (efsd->name.filenocasecmp_to(fs->get_subdir(j)->get_name()) < 0) {1919break;1920}1921idx2++;1922}19231924if (idx2 == fs->get_subdir_count()) {1925fs->subdirs.push_back(efsd);1926} else {1927fs->subdirs.insert(idx2, efsd);1928}1929fs = efsd;1930} else {1931fs = fs->get_subdir(idx);1932}1933}19341935int cpos = -1;1936for (int i = 0; i < fs->files.size(); i++) {1937if (fs_case_sensitive) {1938if (fs->files[i]->file == file) {1939cpos = i;1940break;1941}1942} else {1943if (fs->files[i]->file.to_lower() == file.to_lower()) {1944cpos = i;1945break;1946}1947}1948}19491950r_file_pos = cpos;1951*r_d = fs;19521953return cpos != -1;1954}19551956String EditorFileSystem::get_file_type(const String &p_file) const {1957EditorFileSystemDirectory *fs = nullptr;1958int cpos = -1;19591960if (!_find_file(p_file, &fs, cpos)) {1961return "";1962}19631964return fs->files[cpos]->type;1965}19661967EditorFileSystemDirectory *EditorFileSystem::find_file(const String &p_file, int *r_index) const {1968if (!filesystem || scanning) {1969return nullptr;1970}19711972EditorFileSystemDirectory *fs = nullptr;1973int cpos = -1;1974if (!_find_file(p_file, &fs, cpos)) {1975return nullptr;1976}19771978if (r_index) {1979*r_index = cpos;1980}19811982return fs;1983}19841985ResourceUID::ID EditorFileSystem::get_file_uid(const String &p_path) const {1986int file_idx;1987EditorFileSystemDirectory *directory = find_file(p_path, &file_idx);19881989if (!directory) {1990return ResourceUID::INVALID_ID;1991}1992return directory->files[file_idx]->uid;1993}19941995EditorFileSystemDirectory *EditorFileSystem::get_filesystem_path(const String &p_path) {1996if (!filesystem || scanning) {1997return nullptr;1998}19992000String f = ProjectSettings::get_singleton()->localize_path(p_path);20012002if (!f.begins_with("res://")) {2003return nullptr;2004}20052006f = f.substr(6);2007f = f.replace_char('\\', '/');2008if (f.is_empty()) {2009return filesystem;2010}20112012if (f.ends_with("/")) {2013f = f.substr(0, f.length() - 1);2014}20152016Vector<String> path = f.split("/");20172018if (path.is_empty()) {2019return nullptr;2020}20212022EditorFileSystemDirectory *fs = filesystem;20232024for (int i = 0; i < path.size(); i++) {2025int idx = -1;2026for (int j = 0; j < fs->get_subdir_count(); j++) {2027if (fs->get_subdir(j)->get_name() == path[i]) {2028idx = j;2029break;2030}2031}20322033if (idx == -1) {2034return nullptr;2035} else {2036fs = fs->get_subdir(idx);2037}2038}20392040return fs;2041}20422043void EditorFileSystem::_save_late_updated_files() {2044//files that already existed, and were modified, need re-scanning for dependencies upon project restart. This is done via saving this special file2045String fscache = EditorPaths::get_singleton()->get_project_settings_dir().path_join("filesystem_update4");2046Ref<FileAccess> f = FileAccess::open(fscache, FileAccess::WRITE);2047ERR_FAIL_COND_MSG(f.is_null(), "Cannot create file '" + fscache + "'. Check user write permissions.");2048for (const String &E : late_update_files) {2049f->store_line(E);2050}2051}20522053Vector<String> EditorFileSystem::_get_dependencies(const String &p_path) {2054// Avoid error spam on first opening of a not yet imported project by treating the following situation2055// as a benign one, not letting the file open error happen: the resource is of an importable type but2056// it has not been imported yet.2057if (ResourceFormatImporter::get_singleton()->recognize_path(p_path)) {2058const String &internal_path = ResourceFormatImporter::get_singleton()->get_internal_resource_path(p_path);2059if (!internal_path.is_empty() && !FileAccess::exists(internal_path)) { // If path is empty (error), keep the code flow to the error.2060return Vector<String>();2061}2062}20632064List<String> deps;2065ResourceLoader::get_dependencies(p_path, &deps);20662067Vector<String> ret;2068for (const String &E : deps) {2069ret.push_back(E);2070}20712072return ret;2073}20742075EditorFileSystem::ScriptClassInfo EditorFileSystem::_get_global_script_class(const String &p_type, const String &p_path) const {2076ScriptClassInfo info;2077for (int i = 0; i < ScriptServer::get_language_count(); i++) {2078if (ScriptServer::get_language(i)->handles_global_class_type(p_type)) {2079info.name = ScriptServer::get_language(i)->get_global_class_name(p_path, &info.extends, &info.icon_path, &info.is_abstract, &info.is_tool);2080break;2081}2082}2083return info;2084}20852086void EditorFileSystem::_update_file_icon_path(EditorFileSystemDirectory::FileInfo *file_info) {2087String icon_path;2088if (file_info->resource_script_class != StringName()) {2089icon_path = EditorNode::get_editor_data().script_class_get_icon_path(file_info->resource_script_class);2090} else if (file_info->class_info.icon_path.is_empty() && !file_info->deps.is_empty()) {2091const String &script_dep = file_info->deps[0]; // Assuming the first dependency is a script.2092const String &script_path = script_dep.contains("::") ? script_dep.get_slice("::", 2) : script_dep;2093if (!script_path.is_empty()) {2094String *cached = file_icon_cache.getptr(script_path);2095if (cached) {2096icon_path = *cached;2097} else {2098if (ClassDB::is_parent_class(ResourceLoader::get_resource_type(script_path), SNAME("Script"))) {2099int script_file;2100EditorFileSystemDirectory *efsd = find_file(script_path, &script_file);2101if (efsd) {2102icon_path = efsd->files[script_file]->class_info.icon_path;2103}2104}2105file_icon_cache.insert(script_path, icon_path);2106}2107}2108}21092110if (icon_path.is_empty() && !file_info->type.is_empty()) {2111Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(file_info->type);2112if (icon.is_valid()) {2113icon_path = icon->get_path();2114}2115}21162117file_info->class_info.icon_path = icon_path;2118}21192120void EditorFileSystem::_update_files_icon_path(EditorFileSystemDirectory *edp) {2121if (!edp) {2122edp = filesystem;2123file_icon_cache.clear();2124}2125for (EditorFileSystemDirectory *sub_dir : edp->subdirs) {2126_update_files_icon_path(sub_dir);2127}2128for (EditorFileSystemDirectory::FileInfo *fi : edp->files) {2129_update_file_icon_path(fi);2130}2131}21322133void EditorFileSystem::_update_script_classes() {2134if (update_script_paths.is_empty()) {2135// Ensure the global class file is always present; it's essential for exports to work.2136if (!FileAccess::exists(ProjectSettings::get_singleton()->get_global_class_list_path())) {2137ScriptServer::save_global_classes();2138}2139return;2140}21412142{2143MutexLock update_script_lock(update_script_mutex);21442145EditorProgress *ep = nullptr;2146if (update_script_paths.size() > 1) {2147if (MessageQueue::get_singleton()->is_flushing()) {2148// Use background progress when message queue is flushing.2149ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size(), false, true));2150} else {2151ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size()));2152}2153}21542155int step_count = 0;2156for (const KeyValue<String, ScriptClassInfoUpdate> &E : update_script_paths) {2157_register_global_class_script(E.key, E.key, E.value);2158if (ep) {2159ep->step(E.value.name, step_count++, false);2160}2161}21622163memdelete_notnull(ep);21642165update_script_paths.clear();2166}21672168EditorNode::get_editor_data().script_class_save_global_classes();21692170emit_signal("script_classes_updated");21712172// Rescan custom loaders and savers.2173// Doing the following here because the `filesystem_changed` signal fires multiple times and isn't always followed by script classes update.2174// So I thought it's better to do this when script classes really get updated2175ResourceLoader::remove_custom_loaders();2176ResourceLoader::add_custom_loaders();2177ResourceSaver::remove_custom_savers();2178ResourceSaver::add_custom_savers();2179}21802181void EditorFileSystem::_update_script_documentation() {2182if (update_script_paths_documentation.is_empty()) {2183return;2184}21852186MutexLock update_script_lock(update_script_mutex);21872188EditorProgress *ep = nullptr;2189if (update_script_paths_documentation.size() > 1) {2190if (MessageQueue::get_singleton()->is_flushing()) {2191// Use background progress when message queue is flushing.2192ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size(), false, true));2193} else {2194ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size()));2195}2196}21972198int step_count = 0;2199for (const String &path : update_script_paths_documentation) {2200int index = -1;2201EditorFileSystemDirectory *efd = find_file(path, &index);22022203if (!efd || index < 0) {2204// The file was removed2205EditorHelp::remove_script_doc_by_path(path);2206continue;2207}22082209if (path.ends_with(".tscn")) {2210Ref<PackedScene> packed_scene = ResourceLoader::load(path);2211if (packed_scene.is_valid()) {2212Ref<SceneState> state = packed_scene->get_state();2213if (state.is_valid()) {2214Vector<Ref<Resource>> sub_resources = state->get_sub_resources();2215for (Ref<Resource> sub_resource : sub_resources) {2216Ref<Script> scr = sub_resource;2217if (scr.is_valid()) {2218for (const DocData::ClassDoc &cd : scr->get_documentation()) {2219EditorHelp::add_doc(cd);2220if (!first_scan) {2221// Update the documentation in the Script Editor if it is open.2222ScriptEditor::get_singleton()->update_doc(cd.name);2223}2224}2225}2226}2227}2228}2229continue;2230}22312232for (int i = 0; i < ScriptServer::get_language_count(); i++) {2233ScriptLanguage *lang = ScriptServer::get_language(i);2234if (lang->supports_documentation() && efd->files[index]->type == lang->get_type()) {2235bool should_reload_script = _should_reload_script(path);2236Ref<Script> scr = ResourceLoader::load(path);2237if (scr.is_null()) {2238continue;2239}2240if (should_reload_script) {2241// Reloading the script from disk. Otherwise, the ResourceLoader::load will2242// return the last loaded version of the script (without the modifications).2243scr->reload_from_file();2244}2245for (const DocData::ClassDoc &cd : scr->get_documentation()) {2246EditorHelp::add_doc(cd);2247if (!first_scan) {2248// Update the documentation in the Script Editor if it is open.2249ScriptEditor::get_singleton()->update_doc(cd.name);2250}2251}2252}2253}22542255if (ep) {2256ep->step(efd->files[index]->file, step_count++, false);2257}2258}22592260memdelete_notnull(ep);22612262update_script_paths_documentation.clear();2263}22642265bool EditorFileSystem::_should_reload_script(const String &p_path) {2266if (first_scan) {2267return false;2268}22692270Ref<Script> scr = ResourceCache::get_ref(p_path);2271if (scr.is_null()) {2272// Not a script or not already loaded.2273return false;2274}22752276// Scripts are reloaded via the script editor if they are currently opened.2277if (ScriptEditor::get_singleton()->get_open_scripts().has(scr)) {2278return false;2279}22802281return true;2282}22832284void EditorFileSystem::_process_update_pending() {2285_update_script_classes();2286// Parse documentation second, as it requires the class names to be loaded2287// because _update_script_documentation loads the scripts completely.2288if (!EditorNode::is_cmdline_mode()) {2289_update_script_documentation();2290_update_pending_scene_groups();2291}2292}22932294void EditorFileSystem::_queue_update_script_class(const String &p_path, const ScriptClassInfoUpdate &p_script_update) {2295MutexLock update_script_lock(update_script_mutex);22962297update_script_paths.insert(p_path, p_script_update);2298update_script_paths_documentation.insert(p_path);2299}23002301void EditorFileSystem::_update_scene_groups() {2302if (update_scene_paths.is_empty()) {2303return;2304}23052306EditorProgress *ep = nullptr;2307if (update_scene_paths.size() > 20) {2308ep = memnew(EditorProgress("update_scene_groups", TTR("Updating Scene Groups"), update_scene_paths.size()));2309}2310int step_count = 0;23112312{2313MutexLock update_scene_lock(update_scene_mutex);2314for (const String &path : update_scene_paths) {2315ProjectSettings::get_singleton()->remove_scene_groups_cache(path);23162317int index = -1;2318EditorFileSystemDirectory *efd = find_file(path, &index);23192320if (!efd || index < 0) {2321// The file was removed.2322continue;2323}23242325const HashSet<StringName> scene_groups = PackedScene::get_scene_groups(path);2326if (!scene_groups.is_empty()) {2327ProjectSettings::get_singleton()->add_scene_groups_cache(path, scene_groups);2328}23292330if (ep) {2331ep->step(efd->files[index]->file, step_count++, false);2332}2333}23342335memdelete_notnull(ep);2336update_scene_paths.clear();2337}23382339ProjectSettings::get_singleton()->save_scene_groups_cache();2340}23412342void EditorFileSystem::_update_pending_scene_groups() {2343if (!FileAccess::exists(ProjectSettings::get_singleton()->get_scene_groups_cache_path())) {2344_get_all_scenes(get_filesystem(), update_scene_paths);2345_update_scene_groups();2346} else if (!update_scene_paths.is_empty()) {2347_update_scene_groups();2348}2349}23502351void EditorFileSystem::_queue_update_scene_groups(const String &p_path) {2352MutexLock update_scene_lock(update_scene_mutex);2353update_scene_paths.insert(p_path);2354}23552356void EditorFileSystem::_get_all_scenes(EditorFileSystemDirectory *p_dir, HashSet<String> &r_list) {2357for (int i = 0; i < p_dir->get_file_count(); i++) {2358if (p_dir->get_file_type(i) == SNAME("PackedScene")) {2359r_list.insert(p_dir->get_file_path(i));2360}2361}23622363for (int i = 0; i < p_dir->get_subdir_count(); i++) {2364_get_all_scenes(p_dir->get_subdir(i), r_list);2365}2366}23672368void EditorFileSystem::update_file(const String &p_file) {2369ERR_FAIL_COND(p_file.is_empty());2370update_files({ p_file });2371}23722373void EditorFileSystem::update_files(const Vector<String> &p_script_paths) {2374bool updated = false;2375bool update_files_icon_cache = false;2376Vector<EditorFileSystemDirectory::FileInfo *> files_to_update_icon_path;2377for (const String &file : p_script_paths) {2378ERR_CONTINUE(file.is_empty());2379EditorFileSystemDirectory *fs = nullptr;2380int cpos = -1;23812382if (!_find_file(file, &fs, cpos)) {2383if (!fs) {2384continue;2385}2386}23872388if (!FileAccess::exists(file)) {2389//was removed2390_delete_internal_files(file);2391if (cpos != -1) { // Might've never been part of the editor file system (*.* files deleted in Open dialog).2392if (fs->files[cpos]->uid != ResourceUID::INVALID_ID) {2393if (ResourceUID::get_singleton()->has_id(fs->files[cpos]->uid)) {2394ResourceUID::get_singleton()->remove_id(fs->files[cpos]->uid);2395}2396}2397if (ClassDB::is_parent_class(fs->files[cpos]->type, SNAME("Script"))) {2398ScriptClassInfoUpdate update;2399update.type = fs->files[cpos]->type;2400_queue_update_script_class(file, update);2401if (!fs->files[cpos]->class_info.icon_path.is_empty()) {2402update_files_icon_cache = true;2403}2404}2405if (fs->files[cpos]->type == SNAME("PackedScene")) {2406_queue_update_scene_groups(file);2407}24082409memdelete(fs->files[cpos]);2410fs->files.remove_at(cpos);2411updated = true;2412}2413} else {2414String type = ResourceLoader::get_resource_type(file);2415if (type.is_empty() && textfile_extensions.has(file.get_extension())) {2416type = "TextFile";2417}2418if (type.is_empty() && other_file_extensions.has(file.get_extension())) {2419type = "OtherFile";2420}2421String script_class = ResourceLoader::get_resource_script_class(file);24222423ResourceUID::ID uid = ResourceLoader::get_resource_uid(file);24242425if (cpos == -1) {2426// The file did not exist, it was added.2427int idx = 0;2428String file_name = file.get_file();24292430for (int i = 0; i < fs->files.size(); i++) {2431if (file.filenocasecmp_to(fs->files[i]->file) < 0) {2432break;2433}2434idx++;2435}24362437EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo);2438fi->file = file_name;2439fi->import_modified_time = 0;2440fi->import_valid = (type == "TextFile" || type == "OtherFile") ? true : ResourceLoader::is_import_valid(file);2441fi->import_md5 = "";2442fi->import_dest_paths = Vector<String>();24432444if (idx == fs->files.size()) {2445fs->files.push_back(fi);2446} else {2447fs->files.insert(idx, fi);2448}2449cpos = idx;2450} else {2451//the file exists and it was updated, and was not added in this step.2452//this means we must force upon next restart to scan it again, to get proper type and dependencies2453late_update_files.insert(file);2454_save_late_updated_files(); //files need to be updated in the re-scan2455}24562457EditorFileSystemDirectory::FileInfo *fi = fs->files[cpos];2458const String old_script_class_icon_path = fi->class_info.icon_path;2459const String old_class_name = fi->class_info.name;2460fi->type = type;2461fi->resource_script_class = script_class;2462fi->uid = uid;2463fi->class_info = _get_global_script_class(type, file);2464fi->import_group_file = ResourceLoader::get_import_group_file(file);2465fi->modified_time = FileAccess::get_modified_time(file);2466fi->deps = _get_dependencies(file);2467fi->import_valid = (type == "TextFile" || type == "OtherFile") ? true : ResourceLoader::is_import_valid(file);24682469if (uid != ResourceUID::INVALID_ID) {2470if (ResourceUID::get_singleton()->has_id(uid)) {2471ResourceUID::get_singleton()->set_id(uid, file);2472} else {2473ResourceUID::get_singleton()->add_id(uid, file);2474}24752476ResourceUID::get_singleton()->update_cache();2477} else {2478if (ResourceLoader::should_create_uid_file(file)) {2479Ref<FileAccess> f = FileAccess::open(file + ".uid", FileAccess::WRITE);2480if (f.is_valid()) {2481const ResourceUID::ID id = ResourceUID::get_singleton()->create_id_for_path(file);2482ResourceUID::get_singleton()->add_id(id, file);2483f->store_line(ResourceUID::get_singleton()->id_to_text(id));2484fi->uid = id;2485}2486}2487}24882489// Update preview2490EditorResourcePreview::get_singleton()->check_for_invalidation(file);24912492if (ClassDB::is_parent_class(fi->type, SNAME("Script"))) {2493_queue_update_script_class(file, ScriptClassInfoUpdate::from_file_info(fi));2494}2495if (fi->type == SNAME("PackedScene")) {2496_queue_update_scene_groups(file);2497}24982499if (ClassDB::is_parent_class(fi->type, SNAME("Resource"))) {2500files_to_update_icon_path.push_back(fi);2501} else if (old_script_class_icon_path != fi->class_info.icon_path) {2502update_files_icon_cache = true;2503}25042505// Restore another script as the global class name if multiple scripts had the same old class name.2506if (!old_class_name.is_empty() && fi->class_info.name != old_class_name && ClassDB::is_parent_class(type, SNAME("Script"))) {2507EditorFileSystemDirectory::FileInfo *old_fi = nullptr;2508String old_file = _get_file_by_class_name(filesystem, old_class_name, old_fi);2509if (!old_file.is_empty() && old_fi) {2510_queue_update_script_class(old_file, ScriptClassInfoUpdate::from_file_info(old_fi));2511}2512}2513updated = true;2514}2515}25162517if (updated) {2518if (update_files_icon_cache) {2519_update_files_icon_path();2520} else {2521for (EditorFileSystemDirectory::FileInfo *fi : files_to_update_icon_path) {2522_update_file_icon_path(fi);2523}2524}2525if (!is_scanning()) {2526_process_update_pending();2527}2528if (!filesystem_changed_queued) {2529filesystem_changed_queued = true;2530callable_mp(this, &EditorFileSystem::_notify_filesystem_changed).call_deferred();2531}2532}2533}25342535void EditorFileSystem::_notify_filesystem_changed() {2536emit_signal("filesystem_changed");2537filesystem_changed_queued = false;2538}25392540HashSet<String> EditorFileSystem::get_valid_extensions() const {2541return valid_extensions;2542}25432544void EditorFileSystem::_register_global_class_script(const String &p_search_path, const String &p_target_path, const ScriptClassInfoUpdate &p_script_update) {2545ScriptServer::remove_global_class_by_path(p_search_path); // First remove, just in case it changed25462547if (p_script_update.name.is_empty()) {2548return;2549}25502551String lang;2552for (int j = 0; j < ScriptServer::get_language_count(); j++) {2553if (ScriptServer::get_language(j)->handles_global_class_type(p_script_update.type)) {2554lang = ScriptServer::get_language(j)->get_name();2555break;2556}2557}2558if (lang.is_empty()) {2559return; // No lang found that can handle this global class2560}25612562ScriptServer::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);2563EditorNode::get_editor_data().script_class_set_icon_path(p_script_update.name, p_script_update.icon_path);2564EditorNode::get_editor_data().script_class_set_name(p_target_path, p_script_update.name);2565}25662567void EditorFileSystem::register_global_class_script(const String &p_search_path, const String &p_target_path) {2568int index_file;2569EditorFileSystemDirectory *efsd = find_file(p_search_path, &index_file);2570if (efsd) {2571const EditorFileSystemDirectory::FileInfo *fi = efsd->files[index_file];2572EditorFileSystem::get_singleton()->_register_global_class_script(p_search_path, p_target_path, ScriptClassInfoUpdate::from_file_info(fi));2573} else {2574ScriptServer::remove_global_class_by_path(p_search_path);2575}2576}25772578Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector<String> &p_files) {2579String importer_name;25802581HashMap<String, HashMap<StringName, Variant>> source_file_options;2582HashMap<String, ResourceUID::ID> uids;2583HashMap<String, String> base_paths;2584for (int i = 0; i < p_files.size(); i++) {2585Ref<ConfigFile> config;2586config.instantiate();2587Error err = config->load(p_files[i] + ".import");2588ERR_CONTINUE(err != OK);2589ERR_CONTINUE(!config->has_section_key("remap", "importer"));2590String file_importer_name = config->get_value("remap", "importer");2591ERR_CONTINUE(file_importer_name.is_empty());25922593if (!importer_name.is_empty() && importer_name != file_importer_name) {2594EditorNode::get_singleton()->show_warning(vformat(TTR("There are multiple importers for different types pointing to file %s, import aborted"), p_group_file));2595ERR_FAIL_V(ERR_FILE_CORRUPT);2596}25972598ResourceUID::ID uid = ResourceUID::INVALID_ID;25992600if (config->has_section_key("remap", "uid")) {2601String uidt = config->get_value("remap", "uid");2602uid = ResourceUID::get_singleton()->text_to_id(uidt);2603}26042605uids[p_files[i]] = uid;26062607source_file_options[p_files[i]] = HashMap<StringName, Variant>();2608importer_name = file_importer_name;26092610if (importer_name == "keep" || importer_name == "skip") {2611continue; //do nothing2612}26132614Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(importer_name);2615ERR_FAIL_COND_V(importer.is_null(), ERR_FILE_CORRUPT);2616List<ResourceImporter::ImportOption> options;2617importer->get_import_options(p_files[i], &options);2618//set default values2619for (const ResourceImporter::ImportOption &E : options) {2620source_file_options[p_files[i]][E.option.name] = E.default_value;2621}26222623if (config->has_section("params")) {2624Vector<String> sk = config->get_section_keys("params");2625for (const String ¶m : sk) {2626Variant value = config->get_value("params", param);2627//override with whatever is in file2628source_file_options[p_files[i]][param] = value;2629}2630}26312632base_paths[p_files[i]] = ResourceFormatImporter::get_singleton()->get_import_base_path(p_files[i]);2633}26342635if (importer_name == "keep" || importer_name == "skip") {2636return OK; // (do nothing)2637}26382639ERR_FAIL_COND_V(importer_name.is_empty(), ERR_UNCONFIGURED);26402641Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(importer_name);26422643Error err = importer->import_group_file(p_group_file, source_file_options, base_paths);26442645//all went well, overwrite config files with proper remaps and md5s2646for (const KeyValue<String, HashMap<StringName, Variant>> &E : source_file_options) {2647const String &file = E.key;2648String base_path = ResourceFormatImporter::get_singleton()->get_import_base_path(file);2649Vector<String> dest_paths;2650ResourceUID::ID uid = uids[file];2651{2652Ref<FileAccess> f = FileAccess::open(file + ".import", FileAccess::WRITE);2653ERR_FAIL_COND_V_MSG(f.is_null(), ERR_FILE_CANT_OPEN, "Cannot open import file '" + file + ".import'.");26542655//write manually, as order matters ([remap] has to go first for performance).2656f->store_line("[remap]");2657f->store_line("");2658f->store_line("importer=\"" + importer->get_importer_name() + "\"");2659int version = importer->get_format_version();2660if (version > 0) {2661f->store_line("importer_version=" + itos(version));2662}2663if (!importer->get_resource_type().is_empty()) {2664f->store_line("type=\"" + importer->get_resource_type() + "\"");2665}26662667if (uid == ResourceUID::INVALID_ID) {2668uid = ResourceUID::get_singleton()->create_id_for_path(file);2669}26702671f->store_line("uid=\"" + ResourceUID::get_singleton()->id_to_text(uid) + "\""); // Store in readable format.26722673if (err == OK) {2674String path = base_path + "." + importer->get_save_extension();2675f->store_line("path=\"" + path + "\"");2676dest_paths.push_back(path);2677}26782679f->store_line("group_file=" + Variant(p_group_file).get_construct_string());26802681if (err == OK) {2682f->store_line("valid=true");2683} else {2684f->store_line("valid=false");2685}2686f->store_line("[deps]\n");26872688f->store_line("");26892690f->store_line("source_file=" + Variant(file).get_construct_string());2691if (dest_paths.size()) {2692Array dp;2693for (int i = 0; i < dest_paths.size(); i++) {2694dp.push_back(dest_paths[i]);2695}2696f->store_line("dest_files=" + Variant(dp).get_construct_string() + "\n");2697}2698f->store_line("[params]");2699f->store_line("");27002701//store options in provided order, to avoid file changing. Order is also important because first match is accepted first.27022703List<ResourceImporter::ImportOption> options;2704importer->get_import_options(file, &options);2705//set default values2706for (const ResourceImporter::ImportOption &F : options) {2707String base = F.option.name;2708Variant v = F.default_value;2709if (source_file_options[file].has(base)) {2710v = source_file_options[file][base];2711}2712String value;2713VariantWriter::write_to_string(v, value);2714f->store_line(base + "=" + value);2715}2716}27172718// Store the md5's of the various files. These are stored separately so that the .import files can be version controlled.2719{2720Ref<FileAccess> md5s = FileAccess::open(base_path + ".md5", FileAccess::WRITE);2721ERR_FAIL_COND_V_MSG(md5s.is_null(), ERR_FILE_CANT_OPEN, "Cannot open MD5 file '" + base_path + ".md5'.");27222723md5s->store_line("source_md5=\"" + FileAccess::get_md5(file) + "\"");2724if (dest_paths.size()) {2725md5s->store_line("dest_md5=\"" + FileAccess::get_multiple_md5(dest_paths) + "\"\n");2726}2727}27282729EditorFileSystemDirectory *fs = nullptr;2730int cpos = -1;2731bool found = _find_file(file, &fs, cpos);2732ERR_FAIL_COND_V_MSG(!found, ERR_UNCONFIGURED, vformat("Can't find file '%s' during group reimport.", file));27332734//update modified times, to avoid reimport2735fs->files[cpos]->modified_time = FileAccess::get_modified_time(file);2736fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(file + ".import");2737fs->files[cpos]->import_md5 = FileAccess::get_md5(file + ".import");2738fs->files[cpos]->import_dest_paths = dest_paths;2739fs->files[cpos]->deps = _get_dependencies(file);2740fs->files[cpos]->uid = uid;2741fs->files[cpos]->type = importer->get_resource_type();2742if (fs->files[cpos]->type == "" && textfile_extensions.has(file.get_extension())) {2743fs->files[cpos]->type = "TextFile";2744}2745if (fs->files[cpos]->type == "" && other_file_extensions.has(file.get_extension())) {2746fs->files[cpos]->type = "OtherFile";2747}2748fs->files[cpos]->import_valid = err == OK;27492750if (ResourceUID::get_singleton()->has_id(uid)) {2751ResourceUID::get_singleton()->set_id(uid, file);2752} else {2753ResourceUID::get_singleton()->add_id(uid, file);2754}27552756//if file is currently up, maybe the source it was loaded from changed, so import math must be updated for it2757//to reload properly2758Ref<Resource> r = ResourceCache::get_ref(file);27592760if (r.is_valid()) {2761if (!r->get_import_path().is_empty()) {2762String dst_path = ResourceFormatImporter::get_singleton()->get_internal_resource_path(file);2763r->set_import_path(dst_path);2764r->set_import_last_modified_time(0);2765}2766}27672768EditorResourcePreview::get_singleton()->check_for_invalidation(file);2769}27702771return err;2772}27732774Error 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) {2775print_verbose(vformat("EditorFileSystem: Importing file: %s", p_file));2776uint64_t start_time = OS::get_singleton()->get_ticks_msec();27772778EditorFileSystemDirectory *fs = nullptr;2779int cpos = -1;2780if (p_update_file_system) {2781bool found = _find_file(p_file, &fs, cpos);2782ERR_FAIL_COND_V_MSG(!found, ERR_FILE_NOT_FOUND, vformat("Can't find file '%s' during file reimport.", p_file));2783}27842785//try to obtain existing params27862787HashMap<StringName, Variant> params = p_custom_options;2788String importer_name; //empty by default though27892790if (!p_custom_importer.is_empty()) {2791importer_name = p_custom_importer;2792}27932794ResourceUID::ID uid = ResourceUID::INVALID_ID;2795Variant generator_parameters;2796if (p_generator_parameters) {2797generator_parameters = *p_generator_parameters;2798}27992800if (FileAccess::exists(p_file + ".import")) {2801//use existing2802Ref<ConfigFile> cf;2803cf.instantiate();2804Error err = cf->load(p_file + ".import");2805if (err == OK) {2806if (cf->has_section("params")) {2807Vector<String> sk = cf->get_section_keys("params");2808for (const String &E : sk) {2809if (!params.has(E)) {2810params[E] = cf->get_value("params", E);2811}2812}2813}28142815if (cf->has_section("remap")) {2816if (p_custom_importer.is_empty()) {2817importer_name = cf->get_value("remap", "importer");2818}28192820if (cf->has_section_key("remap", "uid")) {2821String uidt = cf->get_value("remap", "uid");2822uid = ResourceUID::get_singleton()->text_to_id(uidt);2823}28242825if (!p_generator_parameters) {2826if (cf->has_section_key("remap", "generator_parameters")) {2827generator_parameters = cf->get_value("remap", "generator_parameters");2828}2829}2830}2831}2832}28332834if (importer_name == "keep" || importer_name == "skip") {2835//keep files, do nothing.2836if (p_update_file_system) {2837fs->files[cpos]->modified_time = FileAccess::get_modified_time(p_file);2838fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(p_file + ".import");2839fs->files[cpos]->import_md5 = FileAccess::get_md5(p_file + ".import");2840fs->files[cpos]->import_dest_paths = Vector<String>();2841fs->files[cpos]->deps.clear();2842fs->files[cpos]->type = "";2843fs->files[cpos]->import_valid = false;2844EditorResourcePreview::get_singleton()->check_for_invalidation(p_file);2845}2846return OK;2847}2848Ref<ResourceImporter> importer;2849bool load_default = false;2850//find the importer2851if (!importer_name.is_empty()) {2852importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(importer_name);2853}28542855if (importer.is_null()) {2856//not found by name, find by extension2857importer = ResourceFormatImporter::get_singleton()->get_importer_by_file(p_file);2858load_default = true;2859if (importer.is_null()) {2860ERR_FAIL_V_MSG(ERR_FILE_CANT_OPEN, "BUG: File queued for import, but can't be imported, importer for type '" + importer_name + "' not found.");2861}2862}28632864if (FileAccess::exists(p_file + ".import")) {2865// We only want to handle compat for existing files, not new ones.2866importer->handle_compatibility_options(params);2867}28682869//mix with default params, in case a parameter is missing28702871List<ResourceImporter::ImportOption> opts;2872importer->get_import_options(p_file, &opts);2873for (const ResourceImporter::ImportOption &E : opts) {2874if (!params.has(E.option.name)) { //this one is not present2875params[E.option.name] = E.default_value;2876}2877}28782879if (load_default && ProjectSettings::get_singleton()->has_setting("importer_defaults/" + importer->get_importer_name())) {2880//use defaults if exist2881Dictionary d = GLOBAL_GET("importer_defaults/" + importer->get_importer_name());28822883for (const KeyValue<Variant, Variant> &kv : d) {2884params[kv.key] = kv.value;2885}2886}28872888if (uid == ResourceUID::INVALID_ID) {2889uid = ResourceUID::get_singleton()->create_id_for_path(p_file);2890}28912892//finally, perform import!!2893String base_path = ResourceFormatImporter::get_singleton()->get_import_base_path(p_file);28942895List<String> import_variants;2896List<String> gen_files;2897Variant meta;2898Error err = importer->import(uid, p_file, base_path, params, &import_variants, &gen_files, &meta);28992900// As import is complete, save the .import file.29012902Vector<String> dest_paths;2903{2904Ref<FileAccess> f = FileAccess::open(p_file + ".import", FileAccess::WRITE);2905ERR_FAIL_COND_V_MSG(f.is_null(), ERR_FILE_CANT_OPEN, "Cannot open file from path '" + p_file + ".import'.");29062907// Write manually, as order matters ([remap] has to go first for performance).2908f->store_line("[remap]");2909f->store_line("");2910f->store_line("importer=\"" + importer->get_importer_name() + "\"");2911int version = importer->get_format_version();2912if (version > 0) {2913f->store_line("importer_version=" + itos(version));2914}2915if (!importer->get_resource_type().is_empty()) {2916f->store_line("type=\"" + importer->get_resource_type() + "\"");2917}29182919f->store_line("uid=\"" + ResourceUID::get_singleton()->id_to_text(uid) + "\""); // Store in readable format.29202921if (err == OK) {2922if (importer->get_save_extension().is_empty()) {2923//no path2924} else if (import_variants.size()) {2925//import with variants2926for (const String &E : import_variants) {2927String path = base_path.c_escape() + "." + E + "." + importer->get_save_extension();29282929f->store_line("path." + E + "=\"" + path + "\"");2930dest_paths.push_back(path);2931}2932} else {2933String path = base_path + "." + importer->get_save_extension();2934f->store_line("path=\"" + path + "\"");2935dest_paths.push_back(path);2936}29372938} else {2939f->store_line("valid=false");2940}29412942if (meta != Variant()) {2943f->store_line("metadata=" + meta.get_construct_string());2944}29452946if (generator_parameters != Variant()) {2947f->store_line("generator_parameters=" + generator_parameters.get_construct_string());2948}29492950f->store_line("");29512952f->store_line("[deps]\n");29532954if (gen_files.size()) {2955Array genf;2956for (const String &E : gen_files) {2957genf.push_back(E);2958dest_paths.push_back(E);2959}29602961String value;2962VariantWriter::write_to_string(genf, value);2963f->store_line("files=" + value);2964f->store_line("");2965}29662967f->store_line("source_file=" + Variant(p_file).get_construct_string());29682969if (dest_paths.size()) {2970Array dp;2971for (int i = 0; i < dest_paths.size(); i++) {2972dp.push_back(dest_paths[i]);2973}2974f->store_line("dest_files=" + Variant(dp).get_construct_string());2975}2976f->store_line("");29772978f->store_line("[params]");2979f->store_line("");29802981// Store options in provided order, to avoid file changing. Order is also important because first match is accepted first.29822983for (const ResourceImporter::ImportOption &E : opts) {2984String base = E.option.name;2985String value;2986VariantWriter::write_to_string(params[base], value);2987f->store_line(base + "=" + value);2988}2989}29902991// Store the md5's of the various files. These are stored separately so that the .import files can be version controlled.2992{2993Ref<FileAccess> md5s = FileAccess::open(base_path + ".md5", FileAccess::WRITE);2994ERR_FAIL_COND_V_MSG(md5s.is_null(), ERR_FILE_CANT_OPEN, "Cannot open MD5 file '" + base_path + ".md5'.");29952996md5s->store_line("source_md5=\"" + FileAccess::get_md5(p_file) + "\"");2997if (dest_paths.size()) {2998md5s->store_line("dest_md5=\"" + FileAccess::get_multiple_md5(dest_paths) + "\"\n");2999}3000}30013002if (p_update_file_system) {3003// Update cpos, newly created files could've changed the index of the reimported p_file.3004_find_file(p_file, &fs, cpos);30053006// Update modified times, to avoid reimport.3007fs->files[cpos]->modified_time = FileAccess::get_modified_time(p_file);3008fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(p_file + ".import");3009fs->files[cpos]->import_md5 = FileAccess::get_md5(p_file + ".import");3010fs->files[cpos]->import_dest_paths = dest_paths;3011fs->files[cpos]->deps = _get_dependencies(p_file);3012fs->files[cpos]->type = importer->get_resource_type();3013fs->files[cpos]->uid = uid;3014fs->files[cpos]->import_valid = fs->files[cpos]->type == "TextFile" ? true : ResourceLoader::is_import_valid(p_file);3015}30163017for (const String &path : gen_files) {3018Ref<Resource> cached = ResourceCache::get_ref(path);3019if (cached.is_valid()) {3020cached->reload_from_file();3021}3022}30233024if (ResourceUID::get_singleton()->has_id(uid)) {3025ResourceUID::get_singleton()->set_id(uid, p_file);3026} else {3027ResourceUID::get_singleton()->add_id(uid, p_file);3028}30293030// If file is currently up, maybe the source it was loaded from changed, so import math must be updated for it3031// to reload properly.3032Ref<Resource> r = ResourceCache::get_ref(p_file);3033if (r.is_valid()) {3034if (!r->get_import_path().is_empty()) {3035String dst_path = ResourceFormatImporter::get_singleton()->get_internal_resource_path(p_file);3036r->set_import_path(dst_path);3037r->set_import_last_modified_time(0);3038}3039}30403041EditorResourcePreview::get_singleton()->check_for_invalidation(p_file);30423043print_verbose(vformat("EditorFileSystem: \"%s\" import took %d ms.", p_file, OS::get_singleton()->get_ticks_msec() - start_time));30443045ERR_FAIL_COND_V_MSG(err != OK, ERR_FILE_UNRECOGNIZED, "Error importing '" + p_file + "'.");3046return OK;3047}30483049void EditorFileSystem::_find_group_files(EditorFileSystemDirectory *efd, HashMap<String, Vector<String>> &group_files, HashSet<String> &groups_to_reimport) {3050int fc = efd->files.size();3051const EditorFileSystemDirectory::FileInfo *const *files = efd->files.ptr();3052for (int i = 0; i < fc; i++) {3053if (groups_to_reimport.has(files[i]->import_group_file)) {3054if (!group_files.has(files[i]->import_group_file)) {3055group_files[files[i]->import_group_file] = Vector<String>();3056}3057group_files[files[i]->import_group_file].push_back(efd->get_file_path(i));3058}3059}30603061for (int i = 0; i < efd->get_subdir_count(); i++) {3062_find_group_files(efd->get_subdir(i), group_files, groups_to_reimport);3063}3064}30653066void EditorFileSystem::reimport_file_with_custom_parameters(const String &p_file, const String &p_importer, const HashMap<StringName, Variant> &p_custom_params) {3067Vector<String> reloads;3068reloads.append(p_file);30693070// Emit the resource_reimporting signal for the single file before the actual importation.3071emit_signal(SNAME("resources_reimporting"), reloads);30723073_reimport_file(p_file, p_custom_params, p_importer);30743075// Emit the resource_reimported signal for the single file we just reimported.3076emit_signal(SNAME("resources_reimported"), reloads);3077}30783079Error EditorFileSystem::_copy_file(const String &p_from, const String &p_to) {3080Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);3081if (FileAccess::exists(p_from + ".import")) {3082Error err = da->copy(p_from, p_to);3083if (err != OK) {3084return err;3085}30863087// Roll a new uid for this copied .import file to avoid conflict.3088ResourceUID::ID res_uid = ResourceUID::get_singleton()->create_id();30893090// Save the new .import file3091Ref<ConfigFile> cfg;3092cfg.instantiate();3093cfg->load(p_from + ".import");3094cfg->set_value("remap", "uid", ResourceUID::get_singleton()->id_to_text(res_uid));3095err = cfg->save(p_to + ".import");3096if (err != OK) {3097return err;3098}30993100// Make sure it's immediately added to the map so we can remap dependencies if we want to after this.3101ResourceUID::get_singleton()->add_id(res_uid, p_to);3102} else if (ResourceLoader::get_resource_uid(p_from) == ResourceUID::INVALID_ID) {3103// Files which do not use an uid can just be copied.3104Error err = da->copy(p_from, p_to);3105if (err != OK) {3106return err;3107}3108} else {3109// Load the resource and save it again in the new location (this generates a new UID).3110Error err = OK;3111Ref<Resource> res = ResourceCache::get_ref(p_from);3112if (res.is_null()) {3113res = ResourceLoader::load(p_from, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err);3114} else {3115bool edited = false;3116List<Ref<Resource>> cached;3117ResourceCache::get_cached_resources(&cached);3118for (Ref<Resource> &resource : cached) {3119if (!resource->is_edited()) {3120continue;3121}3122if (!resource->get_path().begins_with(p_from)) {3123continue;3124}3125// The resource or one of its built-in resources is edited.3126edited = true;3127resource->set_edited(false);3128}31293130if (edited) {3131// Save cached resources to prevent changes from being lost and to prevent discrepancies.3132EditorNode::get_singleton()->save_resource(res);3133}3134}3135if (err == OK && res.is_valid()) {3136err = ResourceSaver::save(res, p_to, ResourceSaver::FLAG_COMPRESS);3137if (err != OK) {3138return err;3139}3140} else if (err != OK) {3141// When loading files like text files the error is OK but the resource is still null.3142// We can ignore such files.3143return err;3144}3145}3146return OK;3147}31483149bool EditorFileSystem::_copy_directory(const String &p_from, const String &p_to, HashMap<String, String> *p_files) {3150Ref<DirAccess> old_dir = DirAccess::open(p_from);3151ERR_FAIL_COND_V(old_dir.is_null(), false);31523153Error err = make_dir_recursive(p_to);3154if (err != OK && err != ERR_ALREADY_EXISTS) {3155return false;3156}31573158bool success = true;3159old_dir->set_include_navigational(false);3160old_dir->list_dir_begin();31613162for (String F = old_dir->_get_next(); !F.is_empty(); F = old_dir->_get_next()) {3163if (old_dir->current_is_dir()) {3164success = _copy_directory(p_from.path_join(F), p_to.path_join(F), p_files) && success;3165} else if (F.get_extension() != "import" && F.get_extension() != "uid") {3166(*p_files)[p_from.path_join(F)] = p_to.path_join(F);3167}3168}3169return success;3170}31713172void EditorFileSystem::_queue_refresh_filesystem() {3173if (refresh_queued) {3174return;3175}3176refresh_queued = true;3177get_tree()->connect(SNAME("process_frame"), callable_mp(this, &EditorFileSystem::_refresh_filesystem), CONNECT_ONE_SHOT);3178}31793180void EditorFileSystem::_refresh_filesystem() {3181for (const ObjectID &id : folders_to_sort) {3182EditorFileSystemDirectory *dir = ObjectDB::get_instance<EditorFileSystemDirectory>(id);3183if (dir) {3184dir->subdirs.sort_custom<DirectoryComparator>();3185}3186}3187folders_to_sort.clear();31883189_update_scan_actions();31903191emit_signal(SNAME("filesystem_changed"));3192refresh_queued = false;3193}31943195void EditorFileSystem::_reimport_thread(uint32_t p_index, ImportThreadData *p_import_data) {3196ResourceLoader::set_is_import_thread(true);3197int file_idx = p_import_data->reimport_from + int(p_index);3198_reimport_file(p_import_data->reimport_files[file_idx].path);3199ResourceLoader::set_is_import_thread(false);32003201p_import_data->imported_sem->post();3202}32033204void EditorFileSystem::reimport_files(const Vector<String> &p_files) {3205ERR_FAIL_COND_MSG(importing, "Attempted to call reimport_files() recursively, this is not allowed.");3206importing = true;32073208Vector<String> reloads;32093210EditorProgress *ep = memnew(EditorProgress("reimport", TTR("(Re)Importing Assets"), p_files.size()));32113212// The method reimport_files runs on the main thread, and if VSync is enabled3213// or Update Continuously is disabled, Main::Iteration takes longer each frame.3214// Each EditorProgress::step can trigger a redraw, and when there are many files to import,3215// this could lead to a slow import process, especially when the editor is unfocused.3216// Temporarily disabling VSync and low_processor_usage_mode while reimporting fixes this.3217const bool old_low_processor_usage_mode = OS::get_singleton()->is_in_low_processor_usage_mode();3218const DisplayServer::VSyncMode old_vsync_mode = DisplayServer::get_singleton()->window_get_vsync_mode(DisplayServer::MAIN_WINDOW_ID);3219OS::get_singleton()->set_low_processor_usage_mode(false);3220DisplayServer::get_singleton()->window_set_vsync_mode(DisplayServer::VSyncMode::VSYNC_DISABLED);32213222Vector<ImportFile> reimport_files;32233224HashSet<String> groups_to_reimport;32253226for (int i = 0; i < p_files.size(); i++) {3227ep->step(TTR("Preparing files to reimport..."), i, false);32283229String file = p_files[i];32303231ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(file);3232if (uid != ResourceUID::INVALID_ID && ResourceUID::get_singleton()->has_id(uid)) {3233file = ResourceUID::get_singleton()->get_id_path(uid);3234}32353236String group_file = ResourceFormatImporter::get_singleton()->get_import_group_file(file);32373238if (group_file_cache.has(file)) {3239// Maybe the file itself is a group!3240groups_to_reimport.insert(file);3241// Groups do not belong to groups.3242group_file = String();3243} else if (groups_to_reimport.has(file)) {3244// Groups do not belong to groups.3245group_file = String();3246} else if (!group_file.is_empty()) {3247// It's a group file, add group to import and skip this file.3248groups_to_reimport.insert(group_file);3249} else {3250// It's a regular file.3251ImportFile ifile;3252ifile.path = file;3253ResourceFormatImporter::get_singleton()->get_import_order_threads_and_importer(file, ifile.order, ifile.threaded, ifile.importer);3254reloads.push_back(file);3255reimport_files.push_back(ifile);3256}32573258// Group may have changed, so also update group reference.3259EditorFileSystemDirectory *fs = nullptr;3260int cpos = -1;3261if (_find_file(file, &fs, cpos)) {3262fs->files.write[cpos]->import_group_file = group_file;3263}3264}32653266reimport_files.sort();32673268ep->step(TTR("Executing pre-reimport operations..."), 0, true);32693270// Emit the resource_reimporting signal for the single file before the actual importation.3271emit_signal(SNAME("resources_reimporting"), reloads);32723273#ifdef THREADS_ENABLED3274bool use_multiple_threads = GLOBAL_GET("editor/import/use_multiple_threads");3275#else3276bool use_multiple_threads = false;3277#endif32783279int from = 0;3280Semaphore imported_sem;3281for (int i = 0; i < reimport_files.size(); i++) {3282if (groups_to_reimport.has(reimport_files[i].path)) {3283from = i + 1;3284continue;3285}32863287if (use_multiple_threads && reimport_files[i].threaded) {3288if (i + 1 == reimport_files.size() || reimport_files[i + 1].importer != reimport_files[from].importer || groups_to_reimport.has(reimport_files[i + 1].path)) {3289if (from - i == 0) {3290// Single file, do not use threads.3291ep->step(reimport_files[i].path.get_file(), i, false);3292_reimport_file(reimport_files[i].path);3293} else {3294Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(reimport_files[from].importer);3295if (importer.is_null()) {3296ERR_PRINT(vformat("Invalid importer for \"%s\".", reimport_files[from].importer));3297from = i + 1;3298continue;3299}33003301importer->import_threaded_begin();33023303ImportThreadData tdata;3304tdata.reimport_from = from;3305tdata.reimport_files = reimport_files.ptr();3306tdata.imported_sem = &imported_sem;33073308int item_count = i - from + 1;3309WorkerThreadPool::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));33103311int imported_count = 0;3312while (true) {3313while (true) {3314ep->step(reimport_files[imported_count].path.get_file(), from + imported_count, false);3315if (imported_sem.try_wait()) {3316imported_count++;3317break;3318}3319}3320if (imported_count == item_count) {3321break;3322}3323}33243325WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group_task);3326DEV_ASSERT(!imported_sem.try_wait());33273328importer->import_threaded_end();3329}33303331from = i + 1;3332}33333334} else {3335ep->step(reimport_files[i].path.get_file(), i, false);3336_reimport_file(reimport_files[i].path);33373338// We need to increment the counter, maybe the next file is multithreaded3339// and doesn't have the same importer.3340from = i + 1;3341}3342}33433344// Reimport groups.33453346from = reimport_files.size();33473348if (groups_to_reimport.size()) {3349HashMap<String, Vector<String>> group_files;3350_find_group_files(filesystem, group_files, groups_to_reimport);3351for (const KeyValue<String, Vector<String>> &E : group_files) {3352ep->step(E.key.get_file(), from++, false);3353Error err = _reimport_group(E.key, E.value);3354reloads.push_back(E.key);3355reloads.append_array(E.value);3356if (err == OK) {3357_reimport_file(E.key);3358}3359}3360}3361ep->step(TTR("Finalizing Asset Import..."), p_files.size());33623363ResourceUID::get_singleton()->update_cache(); // After reimporting, update the cache.3364_save_filesystem_cache();33653366memdelete_notnull(ep);33673368_process_update_pending();33693370// Revert to previous values to restore editor settings for VSync and Update Continuously.3371OS::get_singleton()->set_low_processor_usage_mode(old_low_processor_usage_mode);3372DisplayServer::get_singleton()->window_set_vsync_mode(old_vsync_mode);33733374importing = false;33753376ep = memnew(EditorProgress("reimport", TTR("(Re)Importing Assets"), p_files.size()));3377ep->step(TTR("Executing post-reimport operations..."), 0, true);3378if (!is_scanning()) {3379emit_signal(SNAME("filesystem_changed"));3380}3381emit_signal(SNAME("resources_reimported"), reloads);3382memdelete_notnull(ep);3383}33843385Error EditorFileSystem::reimport_append(const String &p_file, const HashMap<StringName, Variant> &p_custom_options, const String &p_custom_importer, Variant p_generator_parameters) {3386Vector<String> reloads;3387reloads.append(p_file);33883389// Emit the resource_reimporting signal for the single file before the actual importation.3390emit_signal(SNAME("resources_reimporting"), reloads);33913392Error ret = _reimport_file(p_file, p_custom_options, p_custom_importer, &p_generator_parameters);33933394// Emit the resource_reimported signal for the single file we just reimported.3395emit_signal(SNAME("resources_reimported"), reloads);3396return ret;3397}33983399Error EditorFileSystem::_resource_import(const String &p_path) {3400Vector<String> files;3401files.push_back(p_path);34023403singleton->update_file(p_path);3404singleton->reimport_files(files);34053406return OK;3407}34083409Ref<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) {3410ERR_FAIL_NULL_V(p_importer, Ref<Resource>());34113412if (!FileAccess::exists(p_path)) {3413ERR_FAIL_V_MSG(Ref<Resource>(), vformat("Failed loading resource: %s. The file doesn't seem to exist.", p_path));3414}34153416Ref<Resource> res;3417bool can_retry = true;3418bool retry = true;3419while (retry) {3420retry = false;34213422res = p_importer->load_internal(p_path, r_error, p_use_sub_threads, r_progress, p_cache_mode, can_retry);34233424if (res.is_null() && can_retry) {3425can_retry = false;3426Error err = singleton->_reimport_file(p_path, HashMap<StringName, Variant>(), "", nullptr, false);3427if (err == OK) {3428retry = true;3429}3430}3431}34323433return res;3434}34353436bool EditorFileSystem::_should_skip_directory(const String &p_path) {3437String project_data_path = ProjectSettings::get_singleton()->get_project_data_path();3438if (p_path == project_data_path || p_path.begins_with(project_data_path + "/")) {3439return true;3440}34413442if (FileAccess::exists(p_path.path_join("project.godot"))) {3443// Skip if another project inside this.3444if (EditorFileSystem::get_singleton()->first_scan) {3445WARN_PRINT_ONCE(vformat("Detected another project.godot at %s. The folder will be ignored.", p_path));3446}3447return true;3448}34493450if (FileAccess::exists(p_path.path_join(".gdignore"))) {3451// Skip if a `.gdignore` file is inside this.3452return true;3453}34543455return false;3456}34573458bool EditorFileSystem::is_group_file(const String &p_path) const {3459return group_file_cache.has(p_path);3460}34613462void EditorFileSystem::_move_group_files(EditorFileSystemDirectory *efd, const String &p_group_file, const String &p_new_location) {3463int fc = efd->files.size();3464EditorFileSystemDirectory::FileInfo *const *files = efd->files.ptrw();3465for (int i = 0; i < fc; i++) {3466if (files[i]->import_group_file == p_group_file) {3467files[i]->import_group_file = p_new_location;34683469Ref<ConfigFile> config;3470config.instantiate();3471String path = efd->get_file_path(i) + ".import";3472Error err = config->load(path);3473if (err != OK) {3474continue;3475}3476if (config->has_section_key("remap", "group_file")) {3477config->set_value("remap", "group_file", p_new_location);3478}34793480Vector<String> sk = config->get_section_keys("params");3481for (const String ¶m : sk) {3482//not very clean, but should work3483String value = config->get_value("params", param);3484if (value == p_group_file) {3485config->set_value("params", param, p_new_location);3486}3487}34883489config->save(path);3490}3491}34923493for (int i = 0; i < efd->get_subdir_count(); i++) {3494_move_group_files(efd->get_subdir(i), p_group_file, p_new_location);3495}3496}34973498void EditorFileSystem::move_group_file(const String &p_path, const String &p_new_path) {3499if (get_filesystem()) {3500_move_group_files(get_filesystem(), p_path, p_new_path);3501if (group_file_cache.has(p_path)) {3502group_file_cache.erase(p_path);3503group_file_cache.insert(p_new_path);3504}3505}3506}35073508Error EditorFileSystem::make_dir_recursive(const String &p_path, const String &p_base_path) {3509Error err;3510Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);3511if (!p_base_path.is_empty()) {3512err = da->change_dir(p_base_path);3513ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot open base directory '" + p_base_path + "'.");3514}35153516if (da->dir_exists(p_path)) {3517return ERR_ALREADY_EXISTS;3518}35193520err = da->make_dir_recursive(p_path);3521if (err != OK) {3522return err;3523}35243525const String path = da->get_current_dir();3526EditorFileSystemDirectory *parent = get_filesystem_path(path);3527ERR_FAIL_NULL_V(parent, ERR_FILE_NOT_FOUND);3528folders_to_sort.insert(parent->get_instance_id());35293530const PackedStringArray folders = p_path.trim_prefix(path).split("/", false);3531for (const String &folder : folders) {3532const int current = parent->find_dir_index(folder);3533if (current > -1) {3534parent = parent->get_subdir(current);3535continue;3536}35373538EditorFileSystemDirectory *efd = memnew(EditorFileSystemDirectory);3539efd->parent = parent;3540efd->name = folder;3541parent->subdirs.push_back(efd);3542parent = efd;3543}35443545_queue_refresh_filesystem();3546return OK;3547}35483549Error EditorFileSystem::copy_file(const String &p_from, const String &p_to) {3550_copy_file(p_from, p_to);35513552EditorFileSystemDirectory *parent = get_filesystem_path(p_to.get_base_dir());3553ERR_FAIL_NULL_V(parent, ERR_FILE_NOT_FOUND);35543555ScanProgress sp;3556_scan_fs_changes(parent, sp, false);35573558_queue_refresh_filesystem();3559return OK;3560}35613562Error EditorFileSystem::copy_directory(const String &p_from, const String &p_to) {3563// Recursively copy directories and build a map of files to copy.3564HashMap<String, String> files;3565bool success = _copy_directory(p_from, p_to, &files);35663567// Copy the files themselves3568if (success) {3569EditorProgress *ep = nullptr;3570if (files.size() > 10) {3571ep = memnew(EditorProgress("copy_directory", TTR("Copying files..."), files.size()));3572}3573int i = 0;3574for (const KeyValue<String, String> &tuple : files) {3575if (_copy_file(tuple.key, tuple.value) != OK) {3576success = false;3577}3578if (ep) {3579ep->step(tuple.key.get_file(), i++, false);3580}3581}3582memdelete_notnull(ep);3583}35843585// Now remap any internal dependencies (within the folder) to use the new files.3586if (success) {3587EditorProgress *ep = nullptr;3588if (files.size() > 10) {3589ep = memnew(EditorProgress("copy_directory", TTR("Remapping dependencies..."), files.size()));3590}3591int i = 0;3592for (const KeyValue<String, String> &tuple : files) {3593if (ResourceLoader::rename_dependencies(tuple.value, files) != OK) {3594success = false;3595}3596update_file(tuple.value);3597if (ep) {3598ep->step(tuple.key.get_file(), i++, false);3599}3600}3601memdelete_notnull(ep);3602}36033604EditorFileSystemDirectory *efd = get_filesystem_path(p_to);3605ERR_FAIL_NULL_V(efd, FAILED);3606ERR_FAIL_NULL_V(efd->get_parent(), FAILED);36073608folders_to_sort.insert(efd->get_parent()->get_instance_id());36093610ScanProgress sp;3611_scan_fs_changes(efd, sp);36123613_queue_refresh_filesystem();3614return success ? OK : FAILED;3615}36163617ResourceUID::ID EditorFileSystem::_resource_saver_get_resource_id_for_path(const String &p_path, bool p_generate) {3618if (!p_path.is_resource_file() || p_path.begins_with(ProjectSettings::get_singleton()->get_project_data_path())) {3619// Saved externally (configuration file) or internal file, do not assign an ID.3620return ResourceUID::INVALID_ID;3621}36223623EditorFileSystemDirectory *fs = nullptr;3624int cpos = -1;36253626if (!singleton->_find_file(p_path, &fs, cpos)) {3627// Fallback to ResourceLoader if filesystem cache fails (can happen during scanning etc.).3628ResourceUID::ID fallback = ResourceLoader::get_resource_uid(p_path);3629if (fallback != ResourceUID::INVALID_ID) {3630return fallback;3631}36323633if (p_generate) {3634return 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.3635} else {3636return ResourceUID::INVALID_ID;3637}3638} else if (fs->files[cpos]->uid != ResourceUID::INVALID_ID) {3639return fs->files[cpos]->uid;3640} else if (p_generate) {3641return 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.3642} else {3643return ResourceUID::INVALID_ID;3644}3645}36463647static void _scan_extensions_dir(EditorFileSystemDirectory *d, HashSet<String> &extensions) {3648int fc = d->get_file_count();3649for (int i = 0; i < fc; i++) {3650if (d->get_file_type(i) == SNAME("GDExtension")) {3651extensions.insert(d->get_file_path(i));3652}3653}3654int dc = d->get_subdir_count();3655for (int i = 0; i < dc; i++) {3656_scan_extensions_dir(d->get_subdir(i), extensions);3657}3658}3659bool EditorFileSystem::_scan_extensions() {3660EditorFileSystemDirectory *d = get_filesystem();3661HashSet<String> extensions;36623663_scan_extensions_dir(d, extensions);36643665return GDExtensionManager::get_singleton()->ensure_extensions_loaded(extensions);3666}36673668void EditorFileSystem::_bind_methods() {3669ClassDB::bind_method(D_METHOD("get_filesystem"), &EditorFileSystem::get_filesystem);3670ClassDB::bind_method(D_METHOD("is_scanning"), &EditorFileSystem::is_scanning);3671ClassDB::bind_method(D_METHOD("get_scanning_progress"), &EditorFileSystem::get_scanning_progress);3672ClassDB::bind_method(D_METHOD("scan"), &EditorFileSystem::scan);3673ClassDB::bind_method(D_METHOD("scan_sources"), &EditorFileSystem::scan_changes);3674ClassDB::bind_method(D_METHOD("update_file", "path"), &EditorFileSystem::update_file);3675ClassDB::bind_method(D_METHOD("get_filesystem_path", "path"), &EditorFileSystem::get_filesystem_path);3676ClassDB::bind_method(D_METHOD("get_file_type", "path"), &EditorFileSystem::get_file_type);3677ClassDB::bind_method(D_METHOD("reimport_files", "files"), &EditorFileSystem::reimport_files);36783679ADD_SIGNAL(MethodInfo("filesystem_changed"));3680ADD_SIGNAL(MethodInfo("script_classes_updated"));3681ADD_SIGNAL(MethodInfo("sources_changed", PropertyInfo(Variant::BOOL, "exist")));3682ADD_SIGNAL(MethodInfo("resources_reimporting", PropertyInfo(Variant::PACKED_STRING_ARRAY, "resources")));3683ADD_SIGNAL(MethodInfo("resources_reimported", PropertyInfo(Variant::PACKED_STRING_ARRAY, "resources")));3684ADD_SIGNAL(MethodInfo("resources_reload", PropertyInfo(Variant::PACKED_STRING_ARRAY, "resources")));3685}36863687void EditorFileSystem::_update_extensions() {3688valid_extensions.clear();3689import_extensions.clear();3690textfile_extensions.clear();3691other_file_extensions.clear();36923693List<String> extensionsl;3694ResourceLoader::get_recognized_extensions_for_type("", &extensionsl);3695for (const String &E : extensionsl) {3696valid_extensions.insert(E);3697}36983699const Vector<String> textfile_ext = ((String)(EDITOR_GET("docks/filesystem/textfile_extensions"))).split(",", false);3700for (const String &E : textfile_ext) {3701if (valid_extensions.has(E)) {3702continue;3703}3704valid_extensions.insert(E);3705textfile_extensions.insert(E);3706}3707const Vector<String> other_file_ext = ((String)(EDITOR_GET("docks/filesystem/other_file_extensions"))).split(",", false);3708for (const String &E : other_file_ext) {3709if (valid_extensions.has(E)) {3710continue;3711}3712valid_extensions.insert(E);3713other_file_extensions.insert(E);3714}37153716extensionsl.clear();3717ResourceFormatImporter::get_singleton()->get_recognized_extensions(&extensionsl);3718for (const String &E : extensionsl) {3719import_extensions.insert(!E.begins_with(".") ? "." + E : E);3720}3721}37223723bool EditorFileSystem::_can_import_file(const String &p_file) {3724for (const String &F : import_extensions) {3725if (p_file.right(F.length()).nocasecmp_to(F) == 0) {3726return true;3727}3728}37293730return false;3731}37323733void EditorFileSystem::add_import_format_support_query(Ref<EditorFileSystemImportFormatSupportQuery> p_query) {3734ERR_FAIL_COND(import_support_queries.has(p_query));3735import_support_queries.push_back(p_query);3736}3737void EditorFileSystem::remove_import_format_support_query(Ref<EditorFileSystemImportFormatSupportQuery> p_query) {3738import_support_queries.erase(p_query);3739}37403741EditorFileSystem::EditorFileSystem() {3742#ifdef THREADS_ENABLED3743use_threads = true;3744#endif37453746ResourceLoader::import = _resource_import;3747reimport_on_missing_imported_files = GLOBAL_GET("editor/import/reimport_missing_imported_files");3748singleton = this;3749filesystem = memnew(EditorFileSystemDirectory); //like, empty3750filesystem->parent = nullptr;37513752new_filesystem = nullptr;37533754// This should probably also work on Unix and use the string it returns for FAT32 or exFAT3755Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);3756using_fat32_or_exfat = (da->get_filesystem_type() == "FAT32" || da->get_filesystem_type() == "EXFAT");37573758scan_total = 0;3759ResourceSaver::set_get_resource_id_for_path(_resource_saver_get_resource_id_for_path);37603761// Set the callback method that the ResourceFormatImporter will use3762// if resources are loaded during the first scan.3763ResourceImporter::load_on_startup = _load_resource_on_startup;3764}37653766EditorFileSystem::~EditorFileSystem() {3767if (filesystem) {3768memdelete(filesystem);3769}3770filesystem = nullptr;3771ResourceSaver::set_get_resource_id_for_path(nullptr);3772}377337743775