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.
This commit is contained in:
Juan Linietsky 2022-08-31 11:12:42 +02:00
parent c40855f818
commit ef17c4668a
13 changed files with 727 additions and 110 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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" />

View file

@ -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>

View file

@ -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" />

View file

@ -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();

View file

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

View file

@ -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;
}
String importer_type = config->get_value("remap", "importer");
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 (err != OK) {
return err;
}
continue;
}
List<String> remaps;
config->get_section_keys("remap", &remaps);
HashSet<String> remap_features;
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.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;
}
continue;
}
//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);
List<String> remaps;
config->get_section_keys("remap", &remaps);
HashSet<String> remap_features;
for (const String &F : remaps) {
String remap = F;
String feature = remap.get_slice(".", 1);
if (features.has(feature)) {
remap_features.insert(feature);
if (err != OK) {
return err;
}
}
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;
}
//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;
}
} 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;

View file

@ -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);

View file

@ -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);
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");
GDVIRTUAL_BIND(_get_customization_configuration_hash);
GDVIRTUAL_BIND(_end_customize_scenes);
GDVIRTUAL_BIND(_end_customize_resources);
GDVIRTUAL_BIND(_get_name);
}
EditorExportPlugin::EditorExportPlugin() {
}
///////////////////////
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;
}
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);
}
EditorExportTextSceneToBinaryPlugin::EditorExportTextSceneToBinaryPlugin() {
GLOBAL_DEF("editor/export/convert_text_resources_to_binary", false);
}

View file

@ -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

View file

@ -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) {

View file

@ -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() {