Repository: godotengine/godot (language: cpp)

Issue:
Import plugin does not support extensions with multiple periods
**Godot version:**
3.1

**OS/device including version:**
Windows

**Issue description:**
I am writing an import plugin for Yarn Spinner dialogue which uses a `.yarn.txt` extension. The import plugin must be registered to just `.txt` in order to work but then causes all sorts of issues where it imports other txt files in the filesystem.

It would be great if:
1. You could specify `.yarn.txt`
2. OR if you could tell the import plugin to skip a file if it's not the right format. So I would still register a .txt but then could just return an error that tells Godot to skip importing the file. Let me know if I should create a separate issue for this. It would also be useful for other import plugins (like Tiled importer) that handle `.json` but then end up causing a lot of headache when it tries to import other json files in the project.


Discussion / hints:
### Problem

The way the resource loading system works is:

1. Get the extension for the file you want to load. This only looks at the last `.` character in the file name.
``` cpp
String String::get_extension() const {

	int pos = find_last(".");
	if (pos < 0 || pos < MAX(find_last("/"), find_last("\\")))
		return "";

	return substr(pos + 1, length());
}
```
2. Match the retrieved extension against all the extensions supported by the various resource loaders.

### Trivial solution

In your case, we'd have to change `get_extension` to return everything after the first `.`, so that extensions with `.` characters are recognised.

The problem then is that we'd stop being able to load files with `.` in their names that are not intended to be part of the extension. This is **backwards incompatible**.

### More comprehensive solution

Make the `get_importer_by_extension` work with an extension from the last `.` and an extension from the first `.`. I guess we could make it so that the more specific extension match wins?

Thoughts?
Sounds good. I'm not sure what the best way to do this on the native side is but from a "consumer" point of view right now we an extension like this in the import plugin
```
func get_recognized_extensions():
	return ["txt"]
```
I would like to be able to specify
```
func get_recognized_extensions():
	return ["yarn.txt"]
``` 
and you're right my import plugin for `yarn.txt` should take precedence over an import plugin for `txt`.

Also, should I open a separate issue for the ability to "skip" your import plugin from running? I believe it would be very useful to be able to return an error code to tell Godot that a plugin wasn't able to handle the file (even if it matches the extension since there are very common extensions like JSON) and it should proceed to use the next import plugin.
@dsaltares There is a trivial solution that doesn't even require attempting to distinguish extension from file-name. Simply take the entire path and check if the end of the path matches the desired extension, multiple periods or no.

Pseudo-code:

    String full_path = imported_file.get_file_path();
    String match_extension = get_recognized_extension();

    if (full_path.substr(full_path.length() - match_extension.length(), match_extension.length()) == match_extension)
    {
        // File has correct extension; do stuff here.
    }


Fix patch:
diff --git a/core/io/resource_importer.cpp b/core/io/resource_importer.cpp
index b7a14f2b8807..3c53b1c17d54 100644
--- a/core/io/resource_importer.cpp
+++ b/core/io/resource_importer.cpp
@@ -250,7 +250,7 @@ Error ResourceFormatImporter::get_import_order_threads_and_importer(const String
 			importer = get_importer_by_name(pat.importer);
 		}
 	} else {
-		importer = get_importer_by_extension(p_path.get_extension().to_lower());
+		importer = get_importer_by_file(p_path);
 	}
 
 	if (importer.is_valid()) {
@@ -274,7 +274,7 @@ int ResourceFormatImporter::get_import_order(const String &p_path) const {
 			importer = get_importer_by_name(pat.importer);
 		}
 	} else {
-		importer = get_importer_by_extension(p_path.get_extension().to_lower());
+		importer = get_importer_by_file(p_path);
 	}
 
 	if (importer.is_valid()) {
@@ -459,12 +459,12 @@ void ResourceFormatImporter::add_importer(const Ref<ResourceImporter> &p_importe
 	}
 }
 
-void ResourceFormatImporter::get_importers_for_extension(const String &p_extension, List<Ref<ResourceImporter>> *r_importers) {
+void ResourceFormatImporter::get_importers_for_file(const String &p_file, List<Ref<ResourceImporter>> *r_importers) {
 	for (int i = 0; i < importers.size(); i++) {
 		List<String> local_exts;
 		importers[i]->get_recognized_extensions(&local_exts);
 		for (const String &F : local_exts) {
-			if (p_extension.to_lower() == F) {
+			if (p_file.right(F.length()).nocasecmp_to(F) == 0) {
 				r_importers->push_back(importers[i]);
 				break;
 			}
@@ -478,7 +478,7 @@ void ResourceFormatImporter::get_importers(List<Ref<ResourceImporter>> *r_import
 	}
 }
 
-Ref<ResourceImporter> ResourceFormatImporter::get_importer_by_extension(const String &p_extension) const {
+Ref<ResourceImporter> ResourceFormatImporter::get_importer_by_file(const String &p_file) const {
 	Ref<ResourceImporter> importer;
 	float priority = 0;
 
@@ -486,9 +486,10 @@ Ref<ResourceImporter> ResourceFormatImporter::get_importer_by_extension(const St
 		List<String> local_exts;
 		importers[i]->get_recognized_extensions(&local_exts);
 		for (const String &F : local_exts) {
-			if (p_extension.to_lower() == F && importers[i]->get_priority() > priority) {
+			if (p_file.right(F.length()).nocasecmp_to(F) == 0 && importers[i]->get_priority() > priority) {
 				importer = importers[i];
 				priority = importers[i]->get_priority();
+				break;
 			}
 		}
 	}
diff --git a/core/io/resource_importer.h b/core/io/resource_importer.h
index 1d00ef3b8246..d0bb511d9dcd 100644
--- a/core/io/resource_importer.h
+++ b/core/io/resource_importer.h
@@ -90,8 +90,8 @@ class ResourceFormatImporter : public ResourceFormatLoader {
 
 	void remove_importer(const Ref<ResourceImporter> &p_importer) { importers.erase(p_importer); }
 	Ref<ResourceImporter> get_importer_by_name(const String &p_name) const;
-	Ref<ResourceImporter> get_importer_by_extension(const String &p_extension) const;
-	void get_importers_for_extension(const String &p_extension, List<Ref<ResourceImporter>> *r_importers);
+	Ref<ResourceImporter> get_importer_by_file(const String &p_file) const;
+	void get_importers_for_file(const String &p_file, List<Ref<ResourceImporter>> *r_importers);
 	void get_importers(List<Ref<ResourceImporter>> *r_importers);
 
 	bool are_import_settings_valid(const String &p_path) const;
diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp
index 04fd641f2446..5999d5013d37 100644
--- a/core/io/resource_loader.cpp
+++ b/core/io/resource_loader.cpp
@@ -61,8 +61,6 @@ bool ResourceFormatLoader::recognize_path(const String &p_path, const String &p_
 		return ret;
 	}
 
-	String extension = p_path.get_extension();
-
 	List<String> extensions;
 	if (p_for_type.is_empty()) {
 		get_recognized_extensions(&extensions);
@@ -71,7 +69,8 @@ bool ResourceFormatLoader::recognize_path(const String &p_path, const String &p_
 	}
 
 	for (const String &E : extensions) {
-		if (E.nocasecmp_to(extension) == 0) {
+		const String ext = !E.begins_with(".") ? "." + E : E;
+		if (p_path.right(ext.length()).nocasecmp_to(ext) == 0) {
 			return true;
 		}
 	}
@@ -322,7 +321,7 @@ Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_origin
 
 #ifdef TOOLS_ENABLED
 	if (Engine::get_singleton()->is_editor_hint()) {
-		if (ResourceFormatImporter::get_singleton()->get_importer_by_extension(p_path.get_extension()).is_valid()) {
+		if (ResourceFormatImporter::get_singleton()->get_importer_by_file(p_path).is_valid()) {
 			// The format is known to the editor, but the file hasn't been imported
 			// (otherwise, ResourceFormatImporter would have been found as a suitable loader).
 			found = true;
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index e78f77b5bf19..e86e58428271 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -963,7 +963,7 @@ bool EditorFileSystem::_update_scan_actions() {
 					Vector<String> dependencies = _get_dependencies(full_path);
 					for (const String &dep : dependencies) {
 						const String &dependency_path = dep.contains("::") ? dep.get_slice("::", 0) : dep;
-						if (import_extensions.has(dep.get_extension())) {
+						if (_can_import_file(dep)) {
 							reimports.push_back(dependency_path);
 						}
 					}
@@ -1204,7 +1204,7 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir,
 		FileCache *fc = file_cache.getptr(path);
 		uint64_t mt = FileAccess::get_modified_time(path);
 
-		if (import_extensions.has(ext)) {
+		if (_can_import_file(scan_file)) {
 			//is imported
 			uint64_t import_mt = FileAccess::get_modified_time(path + ".import");
 
@@ -1494,7 +1494,7 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanPr
 						scan_actions.push_back(ia);
 					}
 
-					if (import_extensions.has(ext)) {
+					if (_can_import_file(f)) {
 						//if it can be imported, and it was added, it needs to be reimported
 						ItemAction ia;
 						ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;
@@ -1526,7 +1526,7 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanPr
 
 		String path = cd.path_join(p_dir->files[i]->file);
 
-		if (import_extensions.has(p_dir->files[i]->file.get_extension().to_lower())) {
+		if (_can_import_file(p_dir->files[i]->file)) {
 			// Check here if file must be imported or not.
 			// Same logic as in _process_file_system, the last modifications dates
 			// needs to be trusted to prevent reading all the .import files and the md5
@@ -2799,7 +2799,7 @@ Error EditorFileSystem::_reimport_file(const String &p_file, const HashMap<Strin
 
 	if (importer.is_null()) {
 		//not found by name, find by extension
-		importer = ResourceFormatImporter::get_singleton()->get_importer_by_extension(p_file.get_extension());
+		importer = ResourceFormatImporter::get_singleton()->get_importer_by_file(p_file);
 		load_default = true;
 		if (importer.is_null()) {
 			ERR_FAIL_V_MSG(ERR_FILE_CANT_OPEN, "BUG: File queued for import, but can't be imported, importer for type '" + importer_name + "' not found.");
@@ -3604,8 +3604,18 @@ void EditorFileSystem::_update_extensions() {
 	extensionsl.clear();
 	ResourceFormatImporter::get_singleton()->get_recognized_extensions(&extensionsl);
 	for (const String &E : extensionsl) {
-		import_extensions.insert(E);
+		import_extensions.insert(!E.begins_with(".") ? "." + E : E);
+	}
+}
+
+bool EditorFileSystem::_can_import_file(const String &p_file) {
+	for (const String &F : import_extensions) {
+		if (p_file.right(F.length()).nocasecmp_to(F) == 0) {
+			return true;
+		}
 	}
+
+	return false;
 }
 
 void EditorFileSystem::add_import_format_support_query(Ref<EditorFileSystemImportFormatSupportQuery> p_query) {
diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h
index 2e483a542575..4d616ee7df92 100644
--- a/editor/editor_file_system.h
+++ b/editor/editor_file_system.h
@@ -280,6 +280,7 @@ class EditorFileSystem : public Node {
 
 	bool _test_for_reimport(const String &p_path, const String &p_expected_import_md5);
 	bool _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);
+	bool _can_import_file(const String &p_path);
 	Vector<String> _get_import_dest_paths(const String &p_path);
 
 	bool reimport_on_missing_imported_files;
diff --git a/editor/import_dock.cpp b/editor/import_dock.cpp
index 1c0249bd4be0..e4effa94996f 100644
--- a/editor/import_dock.cpp
+++ b/editor/import_dock.cpp
@@ -124,7 +124,7 @@ void ImportDock::set_edit_path(const String &p_path) {
 	_update_options(p_path, config);
 
 	List<Ref<ResourceImporter>> importers;
-	ResourceFormatImporter::get_singleton()->get_importers_for_extension(p_path.get_extension(), &importers);
+	ResourceFormatImporter::get_singleton()->get_importers_for_file(p_path, &importers);
 	List<Pair<String, String>> importer_names;
 
 	for (const Ref<ResourceImporter> &E : importers) {
@@ -316,7 +316,7 @@ void ImportDock::set_edit_multiple_paths(const Vector<String> &p_paths) {
 	params->update();
 
 	List<Ref<ResourceImporter>> importers;
-	ResourceFormatImporter::get_singleton()->get_importers_for_extension(p_paths[0].get_extension(), &importers);
+	ResourceFormatImporter::get_singleton()->get_importers_for_file(p_paths[0], &importers);
 	List<Pair<String, String>> importer_names;
 
 	for (const Ref<ResourceImporter> &E : importers) {


Test patch: