Jelajahi Sumber

Add support for scene/resource customization in export plugins

EditorExportPlugin adds a set of callbacks to allow customizing scenes, resources or subresources in all files exported:
* Can take scene files, resource files and subresources in all of them.
* Uses a cache for the converted files if nothing changes, so this work only happens if a file is modified.
* Uses hashing to differentiate export configuration caches.
* Removed the previous conversion code to binary, as this one uses existing stuff.

This API is useful in several scenarios:
* Needed by the "server" export platform to get rid of textures, meshes, audio, etc.
* Needed by text to binary converters.
* Needed by eventual optimizations such as shader precompiling on export, mesh merging and optimization, etc.

This is a draft, feedback is very welcome.
Juan Linietsky 3 tahun lalu
induk
melakukan
ef17c4668a

+ 26 - 0
core/io/config_file.cpp

@@ -32,6 +32,7 @@
 
 #include "core/io/file_access_encrypted.h"
 #include "core/os/keyboard.h"
+#include "core/string/string_builder.h"
 #include "core/variant/variant_parser.h"
 
 PackedStringArray ConfigFile::_get_sections() const {
@@ -130,6 +131,28 @@ void ConfigFile::erase_section_key(const String &p_section, const String &p_key)
 	}
 }
 
+String ConfigFile::encode_to_text() const {
+	StringBuilder sb;
+	bool first = true;
+	for (const KeyValue<String, HashMap<String, Variant>> &E : values) {
+		if (first) {
+			first = false;
+		} else {
+			sb.append("\n");
+		}
+		if (!E.key.is_empty()) {
+			sb.append("[" + E.key + "]\n\n");
+		}
+
+		for (const KeyValue<String, Variant> &F : E.value) {
+			String vstr;
+			VariantWriter::write_to_string(F.value, vstr);
+			sb.append(F.key.property_name_encode() + "=" + vstr + "\n");
+		}
+	}
+	return sb.as_string();
+}
+
 Error ConfigFile::save(const String &p_path) {
 	Error err;
 	Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err);
@@ -295,6 +318,7 @@ Error ConfigFile::_parse(const String &p_path, VariantParser::Stream *p_stream)
 void ConfigFile::clear() {
 	values.clear();
 }
+
 void ConfigFile::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_value", "section", "key", "value"), &ConfigFile::set_value);
 	ClassDB::bind_method(D_METHOD("get_value", "section", "key", "default"), &ConfigFile::get_value, DEFVAL(Variant()));
@@ -312,6 +336,8 @@ void ConfigFile::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("parse", "data"), &ConfigFile::parse);
 	ClassDB::bind_method(D_METHOD("save", "path"), &ConfigFile::save);
 
+	ClassDB::bind_method(D_METHOD("encode_to_text"), &ConfigFile::encode_to_text);
+
 	BIND_METHOD_ERR_RETURN_DOC("load", ERR_FILE_CANT_OPEN);
 
 	ClassDB::bind_method(D_METHOD("load_encrypted", "path", "key"), &ConfigFile::load_encrypted);

+ 2 - 0
core/io/config_file.h

@@ -68,6 +68,8 @@ public:
 	Error load(const String &p_path);
 	Error parse(const String &p_data);
 
+	String encode_to_text() const; // used by exporter
+
 	void clear();
 
 	Error load_encrypted(const String &p_path, const Vector<uint8_t> &p_key);

+ 6 - 0
doc/classes/ConfigFile.xml

@@ -98,6 +98,12 @@
 				Removes the entire contents of the config.
 			</description>
 		</method>
+		<method name="encode_to_text" qualifiers="const">
+			<return type="String" />
+			<description>
+				Obtain the text version of this config file (the same text that would be written to a file).
+			</description>
+		</method>
 		<method name="erase_section">
 			<return type="void" />
 			<param index="0" name="section" type="String" />

+ 9 - 0
doc/classes/EditorExportPlatform.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="EditorExportPlatform" inherits="RefCounted" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+	</brief_description>
+	<description>
+	</description>
+	<tutorials>
+	</tutorials>
+</class>

+ 57 - 0
doc/classes/EditorExportPlugin.xml

@@ -10,6 +10,51 @@
 	<tutorials>
 	</tutorials>
 	<methods>
+		<method name="_begin_customize_resources" qualifiers="virtual const">
+			<return type="bool" />
+			<param index="0" name="platform" type="EditorExportPlatform" />
+			<param index="1" name="features" type="PackedStringArray" />
+			<description>
+				Return true if this plugin will customize resources based on the platform and features used.
+			</description>
+		</method>
+		<method name="_begin_customize_scenes" qualifiers="virtual const">
+			<return type="bool" />
+			<param index="0" name="platform" type="EditorExportPlatform" />
+			<param index="1" name="features" type="PackedStringArray" />
+			<description>
+				Return true if this plugin will customize scenes based on the platform and features used.
+			</description>
+		</method>
+		<method name="_customize_resource" qualifiers="virtual">
+			<return type="Resource" />
+			<param index="0" name="resource" type="Resource" />
+			<param index="1" name="path" type="String" />
+			<description>
+				Customize a resource. If changes are made to it, return the same or a new resource. Otherwise, return [code]null[/code].
+				The [i]path[/i] argument is only used when customizing an actual file, otherwise this means that this resource is part of another one and it will be empty.
+			</description>
+		</method>
+		<method name="_customize_scene" qualifiers="virtual">
+			<return type="Node" />
+			<param index="0" name="scene" type="Node" />
+			<param index="1" name="path" type="String" />
+			<description>
+				Customize a scene. If changes are made to it, return the same or a new scene. Otherwise, return [code]null[/code]. If a new scene is returned, it is up to you to dispose of the old one.
+			</description>
+		</method>
+		<method name="_end_customize_resources" qualifiers="virtual">
+			<return type="void" />
+			<description>
+				This is called when the customization process for resources ends.
+			</description>
+		</method>
+		<method name="_end_customize_scenes" qualifiers="virtual">
+			<return type="void" />
+			<description>
+				This is called when the customization process for scenes ends.
+			</description>
+		</method>
 		<method name="_export_begin" qualifiers="virtual">
 			<return type="void" />
 			<param index="0" name="features" type="PackedStringArray" />
@@ -36,6 +81,18 @@
 				Calling [method skip] inside this callback will make the file not included in the export.
 			</description>
 		</method>
+		<method name="_get_customization_configuration_hash" qualifiers="virtual const">
+			<return type="int" />
+			<description>
+				Return a hash based on the configuration passed (for both scenes and resources). This helps keep separate caches for separate export configurations.
+			</description>
+		</method>
+		<method name="_get_name" qualifiers="virtual const">
+			<return type="String" />
+			<description>
+				Return the name identifier of this plugin (for future identification by the exporter).
+			</description>
+		</method>
 		<method name="add_file">
 			<return type="void" />
 			<param index="0" name="path" type="String" />

+ 1 - 5
editor/editor_node.cpp

@@ -4107,6 +4107,7 @@ void EditorNode::register_editor_types() {
 	GDREGISTER_CLASS(EditorSyntaxHighlighter);
 	GDREGISTER_ABSTRACT_CLASS(EditorInterface);
 	GDREGISTER_CLASS(EditorExportPlugin);
+	GDREGISTER_ABSTRACT_CLASS(EditorExportPlatform);
 	GDREGISTER_CLASS(EditorResourceConversionPlugin);
 	GDREGISTER_CLASS(EditorSceneFormatImporter);
 	GDREGISTER_CLASS(EditorScenePostImportPlugin);
@@ -7418,11 +7419,6 @@ EditorNode::EditorNode() {
 	editor_plugins_force_over = memnew(EditorPluginList);
 	editor_plugins_force_input_forwarding = memnew(EditorPluginList);
 
-	Ref<EditorExportTextSceneToBinaryPlugin> export_text_to_binary_plugin;
-	export_text_to_binary_plugin.instantiate();
-
-	EditorExport::get_singleton()->add_export_plugin(export_text_to_binary_plugin);
-
 	Ref<GDExtensionExportPlugin> gdextension_export_plugin;
 	gdextension_export_plugin.instantiate();
 

+ 2 - 0
editor/export/editor_export.cpp

@@ -351,6 +351,8 @@ EditorExport::EditorExport() {
 
 	singleton = this;
 	set_process(true);
+
+	GLOBAL_DEF("editor/export/convert_text_resources_to_binary", true);
 }
 
 EditorExport::~EditorExport() {

+ 492 - 51
editor/export/editor_export_platform.cpp

@@ -44,6 +44,7 @@
 #include "editor/editor_settings.h"
 #include "editor/plugins/script_editor_plugin.h"
 #include "editor_export_plugin.h"
+#include "scene/resources/packed_scene.h"
 
 static int _get_pad(int p_alignment, int p_n) {
 	int rest = p_n % p_alignment;
@@ -488,6 +489,295 @@ EditorExportPlatform::ExportNotifier::~ExportNotifier() {
 	}
 }
 
+bool EditorExportPlatform::_export_customize_dictionary(Dictionary &dict, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
+	bool changed = false;
+
+	List<Variant> keys;
+	dict.get_key_list(&keys);
+	for (const Variant &K : keys) {
+		Variant v = dict[K];
+		switch (v.get_type()) {
+			case Variant::OBJECT: {
+				Ref<Resource> res = v;
+				if (res.is_valid()) {
+					for (uint32_t j = 0; j < customize_resources_plugins.size(); j++) {
+						Ref<Resource> new_res = customize_resources_plugins[j]->_customize_resource(res, "");
+						if (new_res.is_valid()) {
+							changed = true;
+							if (new_res != res) {
+								dict[K] = new_res;
+								res = new_res;
+							}
+							break;
+						}
+					}
+
+					// If it was not replaced, go through and see if there is something to replace.
+					if (res.is_valid() && !res->get_path().is_resource_file() && _export_customize_object(res.ptr(), customize_resources_plugins), true) {
+						changed = true;
+					}
+				}
+
+			} break;
+			case Variant::DICTIONARY: {
+				Dictionary d = v;
+				if (_export_customize_dictionary(d, customize_resources_plugins)) {
+					changed = true;
+				}
+			} break;
+			case Variant::ARRAY: {
+				Array a = v;
+				if (_export_customize_array(a, customize_resources_plugins)) {
+					changed = true;
+				}
+			} break;
+			default: {
+			}
+		}
+	}
+	return changed;
+}
+
+bool EditorExportPlatform::_export_customize_array(Array &arr, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
+	bool changed = false;
+
+	for (int i = 0; i < arr.size(); i++) {
+		Variant v = arr.get(i);
+		switch (v.get_type()) {
+			case Variant::OBJECT: {
+				Ref<Resource> res = v;
+				if (res.is_valid()) {
+					for (uint32_t j = 0; j < customize_resources_plugins.size(); j++) {
+						Ref<Resource> new_res = customize_resources_plugins[j]->_customize_resource(res, "");
+						if (new_res.is_valid()) {
+							changed = true;
+							if (new_res != res) {
+								arr.set(i, new_res);
+								res = new_res;
+							}
+							break;
+						}
+					}
+
+					// If it was not replaced, go through and see if there is something to replace.
+					if (res.is_valid() && !res->get_path().is_resource_file() && _export_customize_object(res.ptr(), customize_resources_plugins), true) {
+						changed = true;
+					}
+				}
+			} break;
+			case Variant::DICTIONARY: {
+				Dictionary d = v;
+				if (_export_customize_dictionary(d, customize_resources_plugins)) {
+					changed = true;
+				}
+			} break;
+			case Variant::ARRAY: {
+				Array a = v;
+				if (_export_customize_array(a, customize_resources_plugins)) {
+					changed = true;
+				}
+			} break;
+			default: {
+			}
+		}
+	}
+	return changed;
+}
+
+bool EditorExportPlatform::_export_customize_object(Object *p_object, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
+	bool changed = false;
+
+	List<PropertyInfo> props;
+	p_object->get_property_list(&props);
+	for (const PropertyInfo &E : props) {
+		switch (E.type) {
+			case Variant::OBJECT: {
+				Ref<Resource> res = p_object->get(E.name);
+				if (res.is_valid()) {
+					for (uint32_t j = 0; j < customize_resources_plugins.size(); j++) {
+						Ref<Resource> new_res = customize_resources_plugins[j]->_customize_resource(res, "");
+						if (new_res.is_valid()) {
+							changed = true;
+							if (new_res != res) {
+								p_object->set(E.name, new_res);
+								res = new_res;
+							}
+							break;
+						}
+					}
+
+					// If it was not replaced, go through and see if there is something to replace.
+					if (res.is_valid() && !res->get_path().is_resource_file() && _export_customize_object(res.ptr(), customize_resources_plugins), true) {
+						changed = true;
+					}
+				}
+
+			} break;
+			case Variant::DICTIONARY: {
+				Dictionary d = p_object->get(E.name);
+				if (_export_customize_dictionary(d, customize_resources_plugins)) {
+					// May have been generated, so set back just in case
+					p_object->set(E.name, d);
+					changed = true;
+				}
+			} break;
+			case Variant::ARRAY: {
+				Array a = p_object->get(E.name);
+				if (_export_customize_array(a, customize_resources_plugins)) {
+					// May have been generated, so set back just in case
+					p_object->set(E.name, a);
+					changed = true;
+				}
+			} break;
+			default: {
+			}
+		}
+	}
+	return changed;
+}
+
+bool EditorExportPlatform::_export_customize_scene_resources(Node *p_root, Node *p_node, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
+	bool changed = false;
+
+	if (p_node == p_root || p_node->get_owner() == p_root) {
+		if (_export_customize_object(p_node, customize_resources_plugins)) {
+			changed = true;
+		}
+	}
+
+	for (int i = 0; i < p_node->get_child_count(); i++) {
+		if (_export_customize_scene_resources(p_root, p_node->get_child(i), customize_resources_plugins)) {
+			changed = true;
+		}
+	}
+
+	return changed;
+}
+
+String EditorExportPlatform::_export_customize(const String &p_path, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins, LocalVector<Ref<EditorExportPlugin>> &customize_scenes_plugins, HashMap<String, FileExportCache> &export_cache, const String &export_base_path, bool p_force_save) {
+	if (!p_force_save && customize_resources_plugins.is_empty() && customize_scenes_plugins.is_empty()) {
+		return p_path; // do none
+	}
+
+	// Check if a cache exists
+	if (export_cache.has(p_path)) {
+		FileExportCache &fec = export_cache[p_path];
+
+		if (fec.saved_path.is_empty() || FileAccess::exists(fec.saved_path)) {
+			// Destination file exists (was not erased) or not needed
+
+			uint64_t mod_time = FileAccess::get_modified_time(p_path);
+			if (fec.source_modified_time == mod_time) {
+				// Cached (modified time matches).
+				fec.used = true;
+				return fec.saved_path.is_empty() ? p_path : fec.saved_path;
+			}
+
+			String md5 = FileAccess::get_md5(p_path);
+			if (FileAccess::exists(p_path + ".import")) {
+				// Also consider the import file in the string
+				md5 += FileAccess::get_md5(p_path + ".import");
+			}
+			if (fec.source_md5 == md5) {
+				// Cached (md5 matches).
+				fec.source_modified_time = mod_time;
+				fec.used = true;
+				return fec.saved_path.is_empty() ? p_path : fec.saved_path;
+			}
+		}
+	}
+
+	FileExportCache fec;
+	fec.used = true;
+	fec.source_modified_time = FileAccess::get_modified_time(p_path);
+
+	String md5 = FileAccess::get_md5(p_path);
+	if (FileAccess::exists(p_path + ".import")) {
+		// Also consider the import file in the string
+		md5 += FileAccess::get_md5(p_path + ".import");
+	}
+
+	fec.source_md5 = md5;
+
+	// Check if it should convert
+
+	String type = ResourceLoader::get_resource_type(p_path);
+
+	bool modified = false;
+
+	String save_path;
+
+	if (type == "PackedScene") { // Its a scene.
+		Ref<PackedScene> ps = ResourceLoader::load(p_path, "PackedScene", ResourceFormatLoader::CACHE_MODE_IGNORE);
+		ERR_FAIL_COND_V(ps.is_null(), p_path);
+		Node *node = ps->instantiate();
+		ERR_FAIL_COND_V(node == nullptr, p_path);
+		if (customize_scenes_plugins.size()) {
+			for (uint32_t i = 0; i < customize_scenes_plugins.size(); i++) {
+				Node *customized = customize_scenes_plugins[i]->_customize_scene(node, p_path);
+				if (customized != nullptr) {
+					node = customized;
+					modified = true;
+				}
+			}
+		}
+		if (customize_resources_plugins.size()) {
+			if (_export_customize_scene_resources(node, node, customize_resources_plugins)) {
+				modified = true;
+			}
+		}
+
+		if (modified || p_force_save) {
+			// If modified, save it again. This is also used for TSCN -> SCN conversion on export.
+
+			String base_file = p_path.get_file().get_basename() + ".scn"; // use SCN for saving (binary) and repack (If conversting, TSCN PackedScene representation is inefficient, so repacking is also desired).
+			save_path = export_base_path.path_join("export-" + p_path.md5_text() + "-" + base_file);
+
+			Ref<PackedScene> s;
+			s.instantiate();
+			s->pack(node);
+			Error err = ResourceSaver::save(s, save_path);
+			ERR_FAIL_COND_V_MSG(err != OK, p_path, "Unable to save export scene file to: " + save_path);
+		}
+	} else {
+		Ref<Resource> res = ResourceLoader::load(p_path, "", ResourceFormatLoader::CACHE_MODE_IGNORE);
+		ERR_FAIL_COND_V(res.is_null(), p_path);
+
+		if (customize_resources_plugins.size()) {
+			for (uint32_t i = 0; i < customize_resources_plugins.size(); i++) {
+				Ref<Resource> new_res = customize_resources_plugins[i]->_customize_resource(res, p_path);
+				if (new_res.is_valid()) {
+					modified = true;
+					if (new_res != res) {
+						res = new_res;
+					}
+					break;
+				}
+			}
+
+			if (_export_customize_object(res.ptr(), customize_resources_plugins)) {
+				modified = true;
+			}
+		}
+
+		if (modified || p_force_save) {
+			// If modified, save it again. This is also used for TRES -> RES conversion on export.
+
+			String base_file = p_path.get_file().get_basename() + ".res"; // use RES for saving (binary)
+			save_path = export_base_path.path_join("export-" + p_path.md5_text() + "-" + base_file);
+
+			Error err = ResourceSaver::save(res, save_path);
+			ERR_FAIL_COND_V_MSG(err != OK, p_path, "Unable to save export resource file to: " + save_path);
+		}
+	}
+
+	fec.saved_path = save_path;
+
+	export_cache[p_path] = fec;
+
+	return save_path.is_empty() ? p_path : save_path;
+}
+
 Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &p_preset, bool p_debug, EditorExportSaveFunction p_func, void *p_udata, EditorExportSaveSharedObject p_so_func) {
 	//figure out paths of files that will be exported
 	HashSet<String> paths;
@@ -601,6 +891,15 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &
 	Error err = OK;
 	Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
 
+	struct SortByName {
+		bool operator()(const Ref<EditorExportPlugin> &left, const Ref<EditorExportPlugin> &right) const {
+			return left->_get_name() < right->_get_name();
+		}
+	};
+
+	// Always sort by name, to so if for some reason theya are re-arranged, it still works.
+	export_plugins.sort_custom<SortByName>();
+
 	for (int i = 0; i < export_plugins.size(); i++) {
 		export_plugins.write[i]->set_export_preset(p_preset);
 
@@ -623,6 +922,65 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &
 	}
 
 	HashSet<String> features = get_features(p_preset, p_debug);
+	PackedStringArray features_psa;
+	for (const String &feature : features) {
+		features_psa.push_back(feature);
+	}
+
+	// Check if custom processing is needed
+	uint32_t custom_resources_hash = HASH_MURMUR3_SEED;
+	uint32_t custom_scene_hash = HASH_MURMUR3_SEED;
+
+	LocalVector<Ref<EditorExportPlugin>> customize_resources_plugins;
+	LocalVector<Ref<EditorExportPlugin>> customize_scenes_plugins;
+
+	for (int i = 0; i < export_plugins.size(); i++) {
+		if (export_plugins[i]->_begin_customize_resources(Ref<EditorExportPlatform>(this), features_psa)) {
+			customize_resources_plugins.push_back(export_plugins[i]);
+
+			custom_resources_hash = hash_murmur3_one_64(export_plugins[i]->_get_name().hash64(), custom_resources_hash);
+			uint64_t hash = export_plugins[i]->_get_customization_configuration_hash();
+			custom_resources_hash = hash_murmur3_one_64(hash, custom_resources_hash);
+		}
+		if (export_plugins[i]->_begin_customize_scenes(Ref<EditorExportPlatform>(this), features_psa)) {
+			customize_scenes_plugins.push_back(export_plugins[i]);
+
+			custom_resources_hash = hash_murmur3_one_64(export_plugins[i]->_get_name().hash64(), custom_resources_hash);
+			uint64_t hash = export_plugins[i]->_get_customization_configuration_hash();
+			custom_scene_hash = hash_murmur3_one_64(hash, custom_scene_hash);
+		}
+	}
+
+	HashMap<String, FileExportCache> export_cache;
+	String export_base_path = ProjectSettings::get_singleton()->get_project_data_path().path_join("exported/") + itos(custom_resources_hash);
+
+	bool convert_text_to_binary = GLOBAL_GET("editor/export/convert_text_resources_to_binary");
+
+	if (convert_text_to_binary || customize_resources_plugins.size() || customize_scenes_plugins.size()) {
+		// See if we have something to open
+		Ref<FileAccess> f = FileAccess::open(export_base_path.path_join("file_cache"), FileAccess::READ);
+		if (f.is_valid()) {
+			String l = f->get_line();
+			while (l != String()) {
+				Vector<String> fields = l.split("::");
+				if (fields.size() == 4) {
+					FileExportCache fec;
+					String path = fields[0];
+					fec.source_md5 = fields[1].strip_edges();
+					fec.source_modified_time = fields[2].strip_edges().to_int();
+					fec.saved_path = fields[3];
+					fec.used = false; // Assume unused until used.
+					export_cache[path] = fec;
+				}
+				l = f->get_line();
+			}
+		} else {
+			// create the path
+			Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);
+			d->change_dir(ProjectSettings::get_singleton()->get_project_data_path());
+			d->make_dir_recursive("exported/" + itos(custom_resources_hash));
+		}
+	}
 
 	//store everything in the export medium
 	int idx = 0;
@@ -633,85 +991,133 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &
 		String type = ResourceLoader::get_resource_type(path);
 
 		if (FileAccess::exists(path + ".import")) {
-			//file is imported, replace by what it imports
-			Ref<ConfigFile> config;
-			config.instantiate();
-			err = config->load(path + ".import");
-			if (err != OK) {
-				ERR_PRINT("Could not parse: '" + path + "', not exported.");
-				continue;
-			}
+			// Before doing this, try to see if it can be customized
 
-			String importer_type = config->get_value("remap", "importer");
+			String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, false);
 
-			if (importer_type == "keep") {
-				//just keep file as-is
-				Vector<uint8_t> array = FileAccess::get_file_as_array(path);
-				err = p_func(p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key);
+			if (export_path != path) {
+				// It was actually customized..
+				// Since the original file is likely not recognized, just use the import system
 
+				Ref<ConfigFile> config;
+				config.instantiate();
+				err = config->load(path + ".import");
+				if (err != OK) {
+					ERR_PRINT("Could not parse: '" + path + "', not exported.");
+					continue;
+				}
+				config->set_value("remap", "type", ResourceLoader::get_resource_type(export_path));
+
+				// Erase all PAths
+				List<String> keys;
+				config->get_section_keys("remap", &keys);
+				for (const String &K : keys) {
+					if (E.begins_with("path")) {
+						config->erase_section_key("remap", K);
+					}
+				}
+				// Set actual converted path.
+				config->set_value("remap", "path", export_path);
+
+				// erase useless sections
+				config->erase_section("deps");
+				config->erase_section("params");
+
+				String import_text = config->encode_to_text();
+				CharString cs = import_text.utf8();
+				Vector<uint8_t> sarr;
+				sarr.resize(cs.size());
+				memcpy(sarr.ptrw(), cs.ptr(), sarr.size());
+
+				err = p_func(p_udata, path + ".import", sarr, idx, total, enc_in_filters, enc_ex_filters, key);
 				if (err != OK) {
 					return err;
 				}
+				// Now actual remapped file:
+				sarr = FileAccess::get_file_as_array(export_path);
+				err = p_func(p_udata, export_path, sarr, idx, total, enc_in_filters, enc_ex_filters, key);
+				if (err != OK) {
+					return err;
+				}
+			} else {
+				// file is imported and not customized, replace by what it imports
+				Ref<ConfigFile> config;
+				config.instantiate();
+				err = config->load(path + ".import");
+				if (err != OK) {
+					ERR_PRINT("Could not parse: '" + path + "', not exported.");
+					continue;
+				}
 
-				continue;
-			}
+				String importer_type = config->get_value("remap", "importer");
 
-			List<String> remaps;
-			config->get_section_keys("remap", &remaps);
+				if (importer_type == "keep") {
+					//just keep file as-is
+					Vector<uint8_t> array = FileAccess::get_file_as_array(path);
+					err = p_func(p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key);
 
-			HashSet<String> remap_features;
+					if (err != OK) {
+						return err;
+					}
 
-			for (const String &F : remaps) {
-				String remap = F;
-				String feature = remap.get_slice(".", 1);
-				if (features.has(feature)) {
-					remap_features.insert(feature);
+					continue;
 				}
-			}
 
-			if (remap_features.size() > 1) {
-				this->resolve_platform_feature_priorities(p_preset, remap_features);
-			}
+				List<String> remaps;
+				config->get_section_keys("remap", &remaps);
 
-			err = OK;
+				HashSet<String> remap_features;
 
-			for (const String &F : remaps) {
-				String remap = F;
-				if (remap == "path") {
-					String remapped_path = config->get_value("remap", remap);
-					Vector<uint8_t> array = FileAccess::get_file_as_array(remapped_path);
-					err = p_func(p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key);
-				} else if (remap.begins_with("path.")) {
+				for (const String &F : remaps) {
+					String remap = F;
 					String feature = remap.get_slice(".", 1);
+					if (features.has(feature)) {
+						remap_features.insert(feature);
+					}
+				}
 
-					if (remap_features.has(feature)) {
+				if (remap_features.size() > 1) {
+					this->resolve_platform_feature_priorities(p_preset, remap_features);
+				}
+
+				err = OK;
+
+				for (const String &F : remaps) {
+					String remap = F;
+					if (remap == "path") {
 						String remapped_path = config->get_value("remap", remap);
 						Vector<uint8_t> array = FileAccess::get_file_as_array(remapped_path);
 						err = p_func(p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key);
+					} else if (remap.begins_with("path.")) {
+						String feature = remap.get_slice(".", 1);
+
+						if (remap_features.has(feature)) {
+							String remapped_path = config->get_value("remap", remap);
+							Vector<uint8_t> array = FileAccess::get_file_as_array(remapped_path);
+							err = p_func(p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key);
+						}
 					}
 				}
-			}
 
-			if (err != OK) {
-				return err;
-			}
+				if (err != OK) {
+					return err;
+				}
 
-			//also save the .import file
-			Vector<uint8_t> array = FileAccess::get_file_as_array(path + ".import");
-			err = p_func(p_udata, path + ".import", array, idx, total, enc_in_filters, enc_ex_filters, key);
+				//also save the .import file
+				Vector<uint8_t> array = FileAccess::get_file_as_array(path + ".import");
+				err = p_func(p_udata, path + ".import", array, idx, total, enc_in_filters, enc_ex_filters, key);
 
-			if (err != OK) {
-				return err;
+				if (err != OK) {
+					return err;
+				}
 			}
 
 		} else {
+			// Customize
+
 			bool do_export = true;
 			for (int i = 0; i < export_plugins.size(); i++) {
 				if (export_plugins[i]->get_script_instance()) { //script based
-					PackedStringArray features_psa;
-					for (const String &feature : features) {
-						features_psa.push_back(feature);
-					}
 					export_plugins.write[i]->_export_file_script(path, type, features_psa);
 				} else {
 					export_plugins.write[i]->_export_file(path, type, features);
@@ -748,8 +1154,18 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &
 			}
 			//just store it as it comes
 			if (do_export) {
-				Vector<uint8_t> array = FileAccess::get_file_as_array(path);
-				err = p_func(p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key);
+				// Customization only happens if plugins did not take care of it before
+				bool force_binary = convert_text_to_binary && (path.get_extension().to_lower() == "tres" || path.get_extension().to_lower() == "tscn");
+				String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, force_binary);
+
+				if (export_path != path) {
+					// Add a remap entry
+					path_remaps.push_back(path);
+					path_remaps.push_back(export_path);
+				}
+
+				Vector<uint8_t> array = FileAccess::get_file_as_array(export_path);
+				err = p_func(p_udata, export_path, array, idx, total, enc_in_filters, enc_ex_filters, key);
 				if (err != OK) {
 					return err;
 				}
@@ -759,6 +1175,31 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &
 		idx++;
 	}
 
+	if (convert_text_to_binary || customize_resources_plugins.size() || customize_scenes_plugins.size()) {
+		// End scene customization
+
+		String fcache = export_base_path.path_join("file_cache");
+		Ref<FileAccess> f = FileAccess::open(fcache, FileAccess::WRITE);
+
+		if (f.is_valid()) {
+			for (const KeyValue<String, FileExportCache> &E : export_cache) {
+				if (E.value.used) { // May be old, unused
+					String l = E.key + "::" + E.value.source_md5 + "::" + itos(E.value.source_modified_time) + "::" + E.value.saved_path;
+					f->store_line(l);
+				}
+			}
+		} else {
+			ERR_PRINT("Error opening export file cache: " + fcache);
+		}
+
+		for (uint32_t i = 0; i < customize_resources_plugins.size(); i++) {
+			customize_resources_plugins[i]->_end_customize_resources();
+		}
+
+		for (uint32_t i = 0; i < customize_scenes_plugins.size(); i++) {
+			customize_scenes_plugins[i]->_end_customize_scenes();
+		}
+	}
 	//save config!
 
 	Vector<String> custom_list;

+ 16 - 0
editor/export/editor_export_platform.h

@@ -40,6 +40,8 @@ struct EditorProgress;
 #include "scene/gui/rich_text_label.h"
 #include "scene/main/node.h"
 
+class EditorExportPlugin;
+
 class EditorExportPlatform : public RefCounted {
 	GDCLASS(EditorExportPlatform, RefCounted);
 
@@ -99,6 +101,20 @@ private:
 
 	static Error _add_shared_object(void *p_userdata, const SharedObject &p_so);
 
+	struct FileExportCache {
+		uint64_t source_modified_time = 0;
+		String source_md5;
+		String saved_path;
+		bool used = false;
+	};
+
+	bool _export_customize_dictionary(Dictionary &dict, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins);
+	bool _export_customize_array(Array &array, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins);
+	bool _export_customize_object(Object *p_object, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins);
+	bool _export_customize_scene_resources(Node *p_root, Node *p_node, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins);
+
+	String _export_customize(const String &p_path, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins, LocalVector<Ref<EditorExportPlugin>> &customize_scenes_plugins, HashMap<String, FileExportCache> &export_cache, const String &export_base_path, bool p_force_save);
+
 protected:
 	struct ExportNotifier {
 		ExportNotifier(EditorExportPlatform &p_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags);

+ 68 - 28
editor/export/editor_export_plugin.cpp

@@ -138,6 +138,64 @@ void EditorExportPlugin::_export_end_script() {
 	GDVIRTUAL_CALL(_export_end);
 }
 
+// Customization
+
+bool EditorExportPlugin::_begin_customize_resources(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) const {
+	bool ret = false;
+	if (GDVIRTUAL_CALL(_begin_customize_resources, p_platform, p_features, ret)) {
+		return ret;
+	}
+	return false;
+}
+
+Ref<Resource> EditorExportPlugin::_customize_resource(const Ref<Resource> &p_resource, const String &p_path) {
+	Ref<Resource> ret;
+	if (GDVIRTUAL_REQUIRED_CALL(_customize_resource, p_resource, p_path, ret)) {
+		return ret;
+	}
+	return Ref<Resource>();
+}
+
+bool EditorExportPlugin::_begin_customize_scenes(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) const {
+	bool ret = false;
+	if (GDVIRTUAL_CALL(_begin_customize_scenes, p_platform, p_features, ret)) {
+		return ret;
+	}
+	return false;
+}
+
+Node *EditorExportPlugin::_customize_scene(Node *p_root, const String &p_path) {
+	Node *ret = nullptr;
+	if (GDVIRTUAL_REQUIRED_CALL(_customize_scene, p_root, p_path, ret)) {
+		return ret;
+	}
+	return nullptr;
+}
+
+uint64_t EditorExportPlugin::_get_customization_configuration_hash() const {
+	uint64_t ret = 0;
+	if (GDVIRTUAL_REQUIRED_CALL(_get_customization_configuration_hash, ret)) {
+		return ret;
+	}
+	return 0;
+}
+
+void EditorExportPlugin::_end_customize_scenes() {
+	GDVIRTUAL_CALL(_end_customize_scenes);
+}
+
+void EditorExportPlugin::_end_customize_resources() {
+	GDVIRTUAL_CALL(_end_customize_resources);
+}
+
+String EditorExportPlugin::_get_name() const {
+	String ret;
+	if (GDVIRTUAL_REQUIRED_CALL(_get_name, ret)) {
+		return ret;
+	}
+	return "";
+}
+
 void EditorExportPlugin::_export_file(const String &p_path, const String &p_type, const HashSet<String> &p_features) {
 }
 
@@ -164,38 +222,20 @@ void EditorExportPlugin::_bind_methods() {
 	GDVIRTUAL_BIND(_export_file, "path", "type", "features");
 	GDVIRTUAL_BIND(_export_begin, "features", "is_debug", "path", "flags");
 	GDVIRTUAL_BIND(_export_end);
-}
 
-EditorExportPlugin::EditorExportPlugin() {
-}
+	GDVIRTUAL_BIND(_begin_customize_resources, "platform", "features");
+	GDVIRTUAL_BIND(_customize_resource, "resource", "path");
 
-///////////////////////
+	GDVIRTUAL_BIND(_begin_customize_scenes, "platform", "features");
+	GDVIRTUAL_BIND(_customize_scene, "scene", "path");
 
-void EditorExportTextSceneToBinaryPlugin::_export_file(const String &p_path, const String &p_type, const HashSet<String> &p_features) {
-	String extension = p_path.get_extension().to_lower();
-	if (extension != "tres" && extension != "tscn") {
-		return;
-	}
+	GDVIRTUAL_BIND(_get_customization_configuration_hash);
 
-	bool convert = GLOBAL_GET("editor/export/convert_text_resources_to_binary");
-	if (!convert) {
-		return;
-	}
-	String tmp_path = EditorPaths::get_singleton()->get_cache_dir().path_join("tmpfile.res");
-	Error err = ResourceFormatLoaderText::convert_file_to_binary(p_path, tmp_path);
-	if (err != OK) {
-		DirAccess::remove_file_or_error(tmp_path);
-		ERR_FAIL();
-	}
-	Vector<uint8_t> data = FileAccess::get_file_as_array(tmp_path);
-	if (data.size() == 0) {
-		DirAccess::remove_file_or_error(tmp_path);
-		ERR_FAIL();
-	}
-	DirAccess::remove_file_or_error(tmp_path);
-	add_file(p_path + ".converted.res", data, true);
+	GDVIRTUAL_BIND(_end_customize_scenes);
+	GDVIRTUAL_BIND(_end_customize_resources);
+
+	GDVIRTUAL_BIND(_get_name);
 }
 
-EditorExportTextSceneToBinaryPlugin::EditorExportTextSceneToBinaryPlugin() {
-	GLOBAL_DEF("editor/export/convert_text_resources_to_binary", false);
+EditorExportPlugin::EditorExportPlugin() {
 }

+ 27 - 8
editor/export/editor_export_plugin.h

@@ -34,6 +34,7 @@
 #include "core/extension/native_extension.h"
 #include "editor_export_preset.h"
 #include "editor_export_shared_object.h"
+#include "scene/main/node.h"
 
 class EditorExportPlugin : public RefCounted {
 	GDCLASS(EditorExportPlugin, RefCounted);
@@ -77,6 +78,7 @@ class EditorExportPlugin : public RefCounted {
 		macos_plugin_files.clear();
 	}
 
+	// Export
 	void _export_file_script(const String &p_path, const String &p_type, const Vector<String> &p_features);
 	void _export_begin_script(const Vector<String> &p_features, bool p_debug, const String &p_path, int p_flags);
 	void _export_end_script();
@@ -108,6 +110,31 @@ protected:
 	GDVIRTUAL4(_export_begin, Vector<String>, bool, String, uint32_t)
 	GDVIRTUAL0(_export_end)
 
+	GDVIRTUAL2RC(bool, _begin_customize_resources, const Ref<EditorExportPlatform> &, const Vector<String> &)
+	GDVIRTUAL2R(Ref<Resource>, _customize_resource, const Ref<Resource> &, String)
+
+	GDVIRTUAL2RC(bool, _begin_customize_scenes, const Ref<EditorExportPlatform> &, const Vector<String> &)
+	GDVIRTUAL2R(Node *, _customize_scene, Node *, String)
+	GDVIRTUAL0RC(uint64_t, _get_customization_configuration_hash)
+
+	GDVIRTUAL0(_end_customize_scenes)
+	GDVIRTUAL0(_end_customize_resources)
+
+	GDVIRTUAL0RC(String, _get_name)
+
+	bool _begin_customize_resources(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) const; // Return true if this plugin does property export customization
+	Ref<Resource> _customize_resource(const Ref<Resource> &p_resource, const String &p_path); // If nothing is returned, it means do not touch (nothing changed). If something is returned (either the same or a different resource) it means changes are made.
+
+	bool _begin_customize_scenes(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) const; // Return true if this plugin does property export customization
+	Node *_customize_scene(Node *p_root, const String &p_path); // Return true if a change was made
+
+	uint64_t _get_customization_configuration_hash() const; // Hash used for caching customized resources and scenes.
+
+	void _end_customize_scenes();
+	void _end_customize_resources();
+
+	virtual String _get_name() const;
+
 public:
 	Vector<String> get_ios_frameworks() const;
 	Vector<String> get_ios_embedded_frameworks() const;
@@ -121,12 +148,4 @@ public:
 	EditorExportPlugin();
 };
 
-class EditorExportTextSceneToBinaryPlugin : public EditorExportPlugin {
-	GDCLASS(EditorExportTextSceneToBinaryPlugin, EditorExportPlugin);
-
-public:
-	virtual void _export_file(const String &p_path, const String &p_type, const HashSet<String> &p_features) override;
-	EditorExportTextSceneToBinaryPlugin();
-};
-
 #endif // EDITOR_EXPORT_PLUGIN_H

+ 1 - 0
editor/plugins/gdextension_export_plugin.h

@@ -36,6 +36,7 @@
 class GDExtensionExportPlugin : public EditorExportPlugin {
 protected:
 	virtual void _export_file(const String &p_path, const String &p_type, const HashSet<String> &p_features);
+	virtual String _get_name() const { return "GDExtension"; }
 };
 
 void GDExtensionExportPlugin::_export_file(const String &p_path, const String &p_type, const HashSet<String> &p_features) {

+ 2 - 0
modules/gdscript/register_types.cpp

@@ -88,6 +88,8 @@ public:
 		// TODO: Re-add compiled GDScript on export.
 		return;
 	}
+
+	virtual String _get_name() const override { return "GDScript"; }
 };
 
 static void _editor_init() {