Răsfoiți Sursa

Add "dedicated server" export mode which can strip unneeded visual resources

David Snopek 2 ani în urmă
părinte
comite
74458b6e9b

+ 8 - 0
doc/classes/Cubemap.xml

@@ -11,4 +11,12 @@
 	</description>
 	</description>
 	<tutorials>
 	<tutorials>
 	</tutorials>
 	</tutorials>
+	<methods>
+		<method name="create_placeholder" qualifiers="const">
+			<return type="Resource" />
+			<description>
+				Creates a placeholder version of this resource ([PlaceholderCubemap]).
+			</description>
+		</method>
+	</methods>
 </class>
 </class>

+ 8 - 0
doc/classes/CubemapArray.xml

@@ -12,4 +12,12 @@
 	</description>
 	</description>
 	<tutorials>
 	<tutorials>
 	</tutorials>
 	</tutorials>
+	<methods>
+		<method name="create_placeholder" qualifiers="const">
+			<return type="Resource" />
+			<description>
+				Creates a placeholder version of this resource ([PlaceholderCubemapArray]).
+			</description>
+		</method>
+	</methods>
 </class>
 </class>

+ 6 - 0
doc/classes/Material.xml

@@ -31,6 +31,12 @@
 			<description>
 			<description>
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="create_placeholder" qualifiers="const">
+			<return type="Resource" />
+			<description>
+				Creates a placeholder version of this resource ([PlaceholderMaterial]).
+			</description>
+		</method>
 		<method name="inspect_native_shader_code">
 		<method name="inspect_native_shader_code">
 			<return type="void" />
 			<return type="void" />
 			<description>
 			<description>

+ 6 - 0
doc/classes/Mesh.xml

@@ -114,6 +114,12 @@
 				[b]Note:[/b] This method typically returns the vertices in reverse order (e.g. clockwise to counterclockwise).
 				[b]Note:[/b] This method typically returns the vertices in reverse order (e.g. clockwise to counterclockwise).
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="create_placeholder" qualifiers="const">
+			<return type="Resource" />
+			<description>
+				Creates a placeholder version of this resource ([PlaceholderMesh]).
+			</description>
+		</method>
 		<method name="create_trimesh_shape" qualifiers="const">
 		<method name="create_trimesh_shape" qualifiers="const">
 			<return type="ConcavePolygonShape3D" />
 			<return type="ConcavePolygonShape3D" />
 			<description>
 			<description>

+ 6 - 0
doc/classes/Texture2D.xml

@@ -74,6 +74,12 @@
 				Called when a pixel's opaque state in the [Texture2D] is queried at the specified [code](x, y)[/code] position.
 				Called when a pixel's opaque state in the [Texture2D] is queried at the specified [code](x, y)[/code] position.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="create_placeholder" qualifiers="const">
+			<return type="Resource" />
+			<description>
+				Creates a placeholder version of this resource ([PlaceholderTexture2D]).
+			</description>
+		</method>
 		<method name="draw" qualifiers="const">
 		<method name="draw" qualifiers="const">
 			<return type="void" />
 			<return type="void" />
 			<param index="0" name="canvas_item" type="RID" />
 			<param index="0" name="canvas_item" type="RID" />

+ 8 - 0
doc/classes/Texture2DArray.xml

@@ -10,4 +10,12 @@
 	</description>
 	</description>
 	<tutorials>
 	<tutorials>
 	</tutorials>
 	</tutorials>
+	<methods>
+		<method name="create_placeholder" qualifiers="const">
+			<return type="Resource" />
+			<description>
+				Creates a placeholder version of this resource ([PlaceholderTexture2DArray]).
+			</description>
+		</method>
+	</methods>
 </class>
 </class>

+ 6 - 0
doc/classes/Texture3D.xml

@@ -47,6 +47,12 @@
 				Called when the presence of mipmaps in the [Texture3D] is queried.
 				Called when the presence of mipmaps in the [Texture3D] is queried.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="create_placeholder" qualifiers="const">
+			<return type="Resource" />
+			<description>
+				Creates a placeholder version of this resource ([PlaceholderTexture3D]).
+			</description>
+		</method>
 		<method name="get_data" qualifiers="const">
 		<method name="get_data" qualifiers="const">
 			<return type="Image[]" />
 			<return type="Image[]" />
 			<description>
 			<description>

+ 6 - 0
editor/editor_node.cpp

@@ -124,6 +124,7 @@
 #include "editor/plugins/asset_library_editor_plugin.h"
 #include "editor/plugins/asset_library_editor_plugin.h"
 #include "editor/plugins/canvas_item_editor_plugin.h"
 #include "editor/plugins/canvas_item_editor_plugin.h"
 #include "editor/plugins/debugger_editor_plugin.h"
 #include "editor/plugins/debugger_editor_plugin.h"
+#include "editor/plugins/dedicated_server_export_plugin.h"
 #include "editor/plugins/editor_preview_plugins.h"
 #include "editor/plugins/editor_preview_plugins.h"
 #include "editor/plugins/editor_resource_conversion_plugin.h"
 #include "editor/plugins/editor_resource_conversion_plugin.h"
 #include "editor/plugins/gdextension_export_plugin.h"
 #include "editor/plugins/gdextension_export_plugin.h"
@@ -7350,6 +7351,11 @@ EditorNode::EditorNode() {
 
 
 	EditorExport::get_singleton()->add_export_plugin(gdextension_export_plugin);
 	EditorExport::get_singleton()->add_export_plugin(gdextension_export_plugin);
 
 
+	Ref<DedicatedServerExportPlugin> dedicated_server_export_plugin;
+	dedicated_server_export_plugin.instantiate();
+
+	EditorExport::get_singleton()->add_export_plugin(dedicated_server_export_plugin);
+
 	Ref<PackedSceneEditorTranslationParserPlugin> packed_scene_translation_parser_plugin;
 	Ref<PackedSceneEditorTranslationParserPlugin> packed_scene_translation_parser_plugin;
 	packed_scene_translation_parser_plugin.instantiate();
 	packed_scene_translation_parser_plugin.instantiate();
 	EditorTranslationParser::get_singleton()->add_parser(packed_scene_translation_parser_plugin, EditorTranslationParser::STANDARD);
 	EditorTranslationParser::get_singleton()->add_parser(packed_scene_translation_parser_plugin, EditorTranslationParser::STANDARD);

+ 11 - 0
editor/export/editor_export.cpp

@@ -45,6 +45,7 @@ void EditorExport::_save() {
 		config->set_value(section, "name", preset->get_name());
 		config->set_value(section, "name", preset->get_name());
 		config->set_value(section, "platform", preset->get_platform()->get_name());
 		config->set_value(section, "platform", preset->get_platform()->get_name());
 		config->set_value(section, "runnable", preset->is_runnable());
 		config->set_value(section, "runnable", preset->is_runnable());
+		config->set_value(section, "dedicated_server", preset->is_dedicated_server());
 		config->set_value(section, "custom_features", preset->get_custom_features());
 		config->set_value(section, "custom_features", preset->get_custom_features());
 
 
 		bool save_files = false;
 		bool save_files = false;
@@ -64,6 +65,11 @@ void EditorExport::_save() {
 				config->set_value(section, "export_filter", "exclude");
 				config->set_value(section, "export_filter", "exclude");
 				save_files = true;
 				save_files = true;
 			} break;
 			} break;
+			case EditorExportPreset::EXPORT_CUSTOMIZED: {
+				config->set_value(section, "export_filter", "customized");
+				config->set_value(section, "customized_files", preset->get_customized_files());
+				save_files = false;
+			};
 		}
 		}
 
 
 		if (save_files) {
 		if (save_files) {
@@ -208,6 +214,7 @@ void EditorExport::load_config() {
 
 
 		preset->set_name(config->get_value(section, "name"));
 		preset->set_name(config->get_value(section, "name"));
 		preset->set_runnable(config->get_value(section, "runnable"));
 		preset->set_runnable(config->get_value(section, "runnable"));
+		preset->set_dedicated_server(config->get_value(section, "dedicated_server", false));
 
 
 		if (config->has_section_key(section, "custom_features")) {
 		if (config->has_section_key(section, "custom_features")) {
 			preset->set_custom_features(config->get_value(section, "custom_features"));
 			preset->set_custom_features(config->get_value(section, "custom_features"));
@@ -228,6 +235,10 @@ void EditorExport::load_config() {
 		} else if (export_filter == "exclude") {
 		} else if (export_filter == "exclude") {
 			preset->set_export_filter(EditorExportPreset::EXCLUDE_SELECTED_RESOURCES);
 			preset->set_export_filter(EditorExportPreset::EXCLUDE_SELECTED_RESOURCES);
 			get_files = true;
 			get_files = true;
+		} else if (export_filter == "customized") {
+			preset->set_export_filter(EditorExportPreset::EXPORT_CUSTOMIZED);
+			preset->set_customized_files(config->get_value(section, "customized_files", Dictionary()));
+			get_files = false;
 		}
 		}
 
 
 		if (get_files) {
 		if (get_files) {

+ 39 - 6
editor/export/editor_export_platform.cpp

@@ -343,6 +343,24 @@ void EditorExportPlatform::_export_find_resources(EditorFileSystemDirectory *p_d
 	}
 	}
 }
 }
 
 
+void EditorExportPlatform::_export_find_customized_resources(const Ref<EditorExportPreset> &p_preset, EditorFileSystemDirectory *p_dir, EditorExportPreset::FileExportMode p_mode, HashSet<String> &p_paths) {
+	for (int i = 0; i < p_dir->get_subdir_count(); i++) {
+		EditorFileSystemDirectory *subdir = p_dir->get_subdir(i);
+		_export_find_customized_resources(p_preset, subdir, p_preset->get_file_export_mode(subdir->get_path(), p_mode), p_paths);
+	}
+
+	for (int i = 0; i < p_dir->get_file_count(); i++) {
+		if (p_dir->get_file_type(i) == "TextFile") {
+			continue;
+		}
+		String path = p_dir->get_file_path(i);
+		EditorExportPreset::FileExportMode file_mode = p_preset->get_file_export_mode(path, p_mode);
+		if (file_mode != EditorExportPreset::MODE_FILE_REMOVE) {
+			p_paths.insert(path);
+		}
+	}
+}
+
 void EditorExportPlatform::_export_find_dependencies(const String &p_path, HashSet<String> &p_paths) {
 void EditorExportPlatform::_export_find_dependencies(const String &p_path, HashSet<String> &p_paths) {
 	if (p_paths.has(p_path)) {
 	if (p_paths.has(p_path)) {
 		return;
 		return;
@@ -637,10 +655,20 @@ bool EditorExportPlatform::_export_customize_object(Object *p_object, LocalVecto
 	return changed;
 	return changed;
 }
 }
 
 
+bool EditorExportPlatform::_is_editable_ancestor(Node *p_root, Node *p_node) {
+	while (p_node != nullptr && p_node != p_root) {
+		if (p_root->is_editable_instance(p_node)) {
+			return true;
+		}
+		p_node = p_node->get_owner();
+	}
+	return false;
+}
+
 bool EditorExportPlatform::_export_customize_scene_resources(Node *p_root, Node *p_node, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
 bool EditorExportPlatform::_export_customize_scene_resources(Node *p_root, Node *p_node, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins) {
 	bool changed = false;
 	bool changed = false;
 
 
-	if (p_node == p_root || p_node->get_owner() == p_root) {
+	if (p_root == p_node || p_node->get_owner() == p_root || _is_editable_ancestor(p_root, p_node)) {
 		if (_export_customize_object(p_node, customize_resources_plugins)) {
 		if (_export_customize_object(p_node, customize_resources_plugins)) {
 			changed = true;
 			changed = true;
 		}
 		}
@@ -755,10 +783,10 @@ String EditorExportPlatform::_export_customize(const String &p_path, LocalVector
 					break;
 					break;
 				}
 				}
 			}
 			}
+		}
 
 
-			if (_export_customize_object(res.ptr(), customize_resources_plugins)) {
-				modified = true;
-			}
+		if (_export_customize_object(res.ptr(), customize_resources_plugins)) {
+			modified = true;
 		}
 		}
 
 
 		if (modified || p_force_save) {
 		if (modified || p_force_save) {
@@ -793,6 +821,8 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &
 		for (int i = 0; i < files.size(); i++) {
 		for (int i = 0; i < files.size(); i++) {
 			paths.erase(files[i]);
 			paths.erase(files[i]);
 		}
 		}
+	} else if (p_preset->get_export_filter() == EditorExportPreset::EXPORT_CUSTOMIZED) {
+		_export_find_customized_resources(p_preset, EditorFileSystem::get_singleton()->get_filesystem(), p_preset->get_file_export_mode("res://"), paths);
 	} else {
 	} else {
 		bool scenes_only = p_preset->get_export_filter() == EditorExportPreset::EXPORT_SELECTED_SCENES;
 		bool scenes_only = p_preset->get_export_filter() == EditorExportPreset::EXPORT_SELECTED_SCENES;
 
 
@@ -936,14 +966,14 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &
 	LocalVector<Ref<EditorExportPlugin>> customize_scenes_plugins;
 	LocalVector<Ref<EditorExportPlugin>> customize_scenes_plugins;
 
 
 	for (int i = 0; i < export_plugins.size(); i++) {
 	for (int i = 0; i < export_plugins.size(); i++) {
-		if (export_plugins[i]->_begin_customize_resources(Ref<EditorExportPlatform>(this), features_psa)) {
+		if (export_plugins.write[i]->_begin_customize_resources(Ref<EditorExportPlatform>(this), features_psa)) {
 			customize_resources_plugins.push_back(export_plugins[i]);
 			customize_resources_plugins.push_back(export_plugins[i]);
 
 
 			custom_resources_hash = hash_murmur3_one_64(export_plugins[i]->_get_name().hash64(), custom_resources_hash);
 			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();
 			uint64_t hash = export_plugins[i]->_get_customization_configuration_hash();
 			custom_resources_hash = hash_murmur3_one_64(hash, custom_resources_hash);
 			custom_resources_hash = hash_murmur3_one_64(hash, custom_resources_hash);
 		}
 		}
-		if (export_plugins[i]->_begin_customize_scenes(Ref<EditorExportPlatform>(this), features_psa)) {
+		if (export_plugins.write[i]->_begin_customize_scenes(Ref<EditorExportPlatform>(this), features_psa)) {
 			customize_scenes_plugins.push_back(export_plugins[i]);
 			customize_scenes_plugins.push_back(export_plugins[i]);
 
 
 			custom_resources_hash = hash_murmur3_one_64(export_plugins[i]->_get_name().hash64(), custom_resources_hash);
 			custom_resources_hash = hash_murmur3_one_64(export_plugins[i]->_get_name().hash64(), custom_resources_hash);
@@ -1215,6 +1245,9 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &
 			}
 			}
 		}
 		}
 	}
 	}
+	for (int i = 0; i < export_plugins.size(); i++) {
+		custom_list.append_array(export_plugins[i]->_get_export_features(Ref<EditorExportPlatform>(this), p_debug));
+	}
 
 
 	ProjectSettings::CustomMap custom_map;
 	ProjectSettings::CustomMap custom_map;
 	if (path_remaps.size()) {
 	if (path_remaps.size()) {

+ 2 - 0
editor/export/editor_export_platform.h

@@ -90,6 +90,7 @@ private:
 	Vector<ExportMessage> messages;
 	Vector<ExportMessage> messages;
 
 
 	void _export_find_resources(EditorFileSystemDirectory *p_dir, HashSet<String> &p_paths);
 	void _export_find_resources(EditorFileSystemDirectory *p_dir, HashSet<String> &p_paths);
+	void _export_find_customized_resources(const Ref<EditorExportPreset> &p_preset, EditorFileSystemDirectory *p_dir, EditorExportPreset::FileExportMode p_mode, HashSet<String> &p_paths);
 	void _export_find_dependencies(const String &p_path, HashSet<String> &p_paths);
 	void _export_find_dependencies(const String &p_path, HashSet<String> &p_paths);
 
 
 	void gen_debug_flags(Vector<String> &r_flags, int p_flags);
 	void gen_debug_flags(Vector<String> &r_flags, int p_flags);
@@ -112,6 +113,7 @@ private:
 	bool _export_customize_array(Array &array, 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_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);
 	bool _export_customize_scene_resources(Node *p_root, Node *p_node, LocalVector<Ref<EditorExportPlugin>> &customize_resources_plugins);
+	bool _is_editable_ancestor(Node *p_root, Node *p_node);
 
 
 	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);
 	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);
 
 

+ 8 - 2
editor/export/editor_export_plugin.cpp

@@ -140,7 +140,7 @@ void EditorExportPlugin::_export_end_script() {
 
 
 // Customization
 // Customization
 
 
-bool EditorExportPlugin::_begin_customize_resources(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) const {
+bool EditorExportPlugin::_begin_customize_resources(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) {
 	bool ret = false;
 	bool ret = false;
 	GDVIRTUAL_CALL(_begin_customize_resources, p_platform, p_features, ret);
 	GDVIRTUAL_CALL(_begin_customize_resources, p_platform, p_features, ret);
 	return ret;
 	return ret;
@@ -152,7 +152,7 @@ Ref<Resource> EditorExportPlugin::_customize_resource(const Ref<Resource> &p_res
 	return ret;
 	return ret;
 }
 }
 
 
-bool EditorExportPlugin::_begin_customize_scenes(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) const {
+bool EditorExportPlugin::_begin_customize_scenes(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) {
 	bool ret = false;
 	bool ret = false;
 	GDVIRTUAL_CALL(_begin_customize_scenes, p_platform, p_features, ret);
 	GDVIRTUAL_CALL(_begin_customize_scenes, p_platform, p_features, ret);
 	return ret;
 	return ret;
@@ -184,6 +184,12 @@ String EditorExportPlugin::_get_name() const {
 	return ret;
 	return ret;
 }
 }
 
 
+PackedStringArray EditorExportPlugin::_get_export_features(const Ref<EditorExportPlatform> &p_platform, bool p_debug) const {
+	PackedStringArray ret;
+	GDVIRTUAL_CALL(_get_export_features, p_platform, p_debug, ret);
+	return ret;
+}
+
 void EditorExportPlugin::_export_file(const String &p_path, const String &p_type, const HashSet<String> &p_features) {
 void EditorExportPlugin::_export_file(const String &p_path, const String &p_type, const HashSet<String> &p_features) {
 }
 }
 
 

+ 11 - 7
editor/export/editor_export_plugin.h

@@ -120,18 +120,22 @@ protected:
 	GDVIRTUAL0(_end_customize_scenes)
 	GDVIRTUAL0(_end_customize_scenes)
 	GDVIRTUAL0(_end_customize_resources)
 	GDVIRTUAL0(_end_customize_resources)
 
 
+	GDVIRTUAL2RC(PackedStringArray, _get_export_features, const Ref<EditorExportPlatform> &, bool);
+
 	GDVIRTUAL0RC(String, _get_name)
 	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.
+	virtual bool _begin_customize_resources(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features); // Return true if this plugin does property export customization
+	virtual 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.
+
+	virtual bool _begin_customize_scenes(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features); // Return true if this plugin does property export customization
+	virtual Node *_customize_scene(Node *p_root, const String &p_path); // Return true if a change was 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
+	virtual uint64_t _get_customization_configuration_hash() const; // Hash used for caching customized resources and scenes.
 
 
-	uint64_t _get_customization_configuration_hash() const; // Hash used for caching customized resources and scenes.
+	virtual void _end_customize_scenes();
+	virtual void _end_customize_resources();
 
 
-	void _end_customize_scenes();
-	void _end_customize_resources();
+	virtual PackedStringArray _get_export_features(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const;
 
 
 	virtual String _get_name() const;
 	virtual String _get_name() const;
 
 

+ 89 - 7
editor/export/editor_export_preset.cpp

@@ -64,15 +64,29 @@ Ref<EditorExportPlatform> EditorExportPreset::get_platform() const {
 	return platform;
 	return platform;
 }
 }
 
 
-void EditorExportPreset::update_files_to_export() {
-	Vector<String> to_remove;
-	for (const String &E : selected_files) {
-		if (!FileAccess::exists(E)) {
-			to_remove.push_back(E);
+void EditorExportPreset::update_files() {
+	{
+		Vector<String> to_remove;
+		for (const String &E : selected_files) {
+			if (!FileAccess::exists(E)) {
+				to_remove.push_back(E);
+			}
+		}
+		for (int i = 0; i < to_remove.size(); ++i) {
+			selected_files.erase(to_remove[i]);
 		}
 		}
 	}
 	}
-	for (int i = 0; i < to_remove.size(); ++i) {
-		selected_files.erase(to_remove[i]);
+
+	{
+		Vector<String> to_remove;
+		for (const KeyValue<String, FileExportMode> &E : customized_files) {
+			if (!FileAccess::exists(E.key) && !DirAccess::exists(E.key)) {
+				to_remove.push_back(E.key);
+			}
+		}
+		for (int i = 0; i < to_remove.size(); ++i) {
+			customized_files.erase(to_remove[i]);
+		}
 	}
 	}
 }
 }
 
 
@@ -84,6 +98,48 @@ Vector<String> EditorExportPreset::get_files_to_export() const {
 	return files;
 	return files;
 }
 }
 
 
+Dictionary EditorExportPreset::get_customized_files() const {
+	Dictionary files;
+	for (const KeyValue<String, FileExportMode> &E : customized_files) {
+		String mode;
+		switch (E.value) {
+			case MODE_FILE_NOT_CUSTOMIZED: {
+				continue;
+			} break;
+			case MODE_FILE_STRIP: {
+				mode = "strip";
+			} break;
+			case MODE_FILE_KEEP: {
+				mode = "keep";
+			} break;
+			case MODE_FILE_REMOVE: {
+				mode = "remove";
+			}
+		}
+		files[E.key] = mode;
+	}
+	return files;
+}
+
+int EditorExportPreset::get_customized_files_count() const {
+	return customized_files.size();
+}
+
+void EditorExportPreset::set_customized_files(const Dictionary &p_files) {
+	for (const Variant *key = p_files.next(nullptr); key; key = p_files.next(key)) {
+		EditorExportPreset::FileExportMode mode = EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED;
+		String value = p_files[*key];
+		if (value == "strip") {
+			mode = EditorExportPreset::MODE_FILE_STRIP;
+		} else if (value == "keep") {
+			mode = EditorExportPreset::MODE_FILE_KEEP;
+		} else if (value == "remove") {
+			mode = EditorExportPreset::MODE_FILE_REMOVE;
+		}
+		set_file_export_mode(*key, mode);
+	}
+}
+
 void EditorExportPreset::set_name(const String &p_name) {
 void EditorExportPreset::set_name(const String &p_name) {
 	name = p_name;
 	name = p_name;
 	EditorExport::singleton->save_presets();
 	EditorExport::singleton->save_presets();
@@ -102,6 +158,15 @@ bool EditorExportPreset::is_runnable() const {
 	return runnable;
 	return runnable;
 }
 }
 
 
+void EditorExportPreset::set_dedicated_server(bool p_enable) {
+	dedicated_server = p_enable;
+	EditorExport::singleton->save_presets();
+}
+
+bool EditorExportPreset::is_dedicated_server() const {
+	return dedicated_server;
+}
+
 void EditorExportPreset::set_export_filter(ExportFilter p_filter) {
 void EditorExportPreset::set_export_filter(ExportFilter p_filter) {
 	export_filter = p_filter;
 	export_filter = p_filter;
 	EditorExport::singleton->save_presets();
 	EditorExport::singleton->save_presets();
@@ -158,6 +223,23 @@ bool EditorExportPreset::has_export_file(const String &p_path) {
 	return selected_files.has(p_path);
 	return selected_files.has(p_path);
 }
 }
 
 
+void EditorExportPreset::set_file_export_mode(const String &p_path, EditorExportPreset::FileExportMode p_mode) {
+	if (p_mode == FileExportMode::MODE_FILE_NOT_CUSTOMIZED) {
+		customized_files.erase(p_path);
+	} else {
+		customized_files.insert(p_path, p_mode);
+	}
+	EditorExport::singleton->save_presets();
+}
+
+EditorExportPreset::FileExportMode EditorExportPreset::get_file_export_mode(const String &p_path, EditorExportPreset::FileExportMode p_default) const {
+	HashMap<String, FileExportMode>::ConstIterator i = customized_files.find(p_path);
+	if (i) {
+		return i->value;
+	}
+	return p_default;
+}
+
 void EditorExportPreset::set_custom_features(const String &p_custom_features) {
 void EditorExportPreset::set_custom_features(const String &p_custom_features) {
 	custom_features = p_custom_features;
 	custom_features = p_custom_features;
 	EditorExport::singleton->save_presets();
 	EditorExport::singleton->save_presets();

+ 20 - 1
editor/export/editor_export_preset.h

@@ -44,6 +44,14 @@ public:
 		EXPORT_SELECTED_SCENES,
 		EXPORT_SELECTED_SCENES,
 		EXPORT_SELECTED_RESOURCES,
 		EXPORT_SELECTED_RESOURCES,
 		EXCLUDE_SELECTED_RESOURCES,
 		EXCLUDE_SELECTED_RESOURCES,
+		EXPORT_CUSTOMIZED,
+	};
+
+	enum FileExportMode {
+		MODE_FILE_NOT_CUSTOMIZED,
+		MODE_FILE_STRIP,
+		MODE_FILE_KEEP,
+		MODE_FILE_REMOVE,
 	};
 	};
 
 
 	enum ScriptExportMode {
 	enum ScriptExportMode {
@@ -60,7 +68,9 @@ private:
 
 
 	String exporter;
 	String exporter;
 	HashSet<String> selected_files;
 	HashSet<String> selected_files;
+	HashMap<String, FileExportMode> customized_files;
 	bool runnable = false;
 	bool runnable = false;
+	bool dedicated_server = false;
 
 
 	friend class EditorExport;
 	friend class EditorExport;
 	friend class EditorExportPlatform;
 	friend class EditorExportPlatform;
@@ -91,20 +101,29 @@ public:
 
 
 	bool has(const StringName &p_property) const { return values.has(p_property); }
 	bool has(const StringName &p_property) const { return values.has(p_property); }
 
 
-	void update_files_to_export();
+	void update_files();
 
 
 	Vector<String> get_files_to_export() const;
 	Vector<String> get_files_to_export() const;
+	Dictionary get_customized_files() const;
+	int get_customized_files_count() const;
+	void set_customized_files(const Dictionary &p_files);
 
 
 	void add_export_file(const String &p_path);
 	void add_export_file(const String &p_path);
 	void remove_export_file(const String &p_path);
 	void remove_export_file(const String &p_path);
 	bool has_export_file(const String &p_path);
 	bool has_export_file(const String &p_path);
 
 
+	void set_file_export_mode(const String &p_path, FileExportMode p_mode);
+	FileExportMode get_file_export_mode(const String &p_path, FileExportMode p_default = MODE_FILE_NOT_CUSTOMIZED) const;
+
 	void set_name(const String &p_name);
 	void set_name(const String &p_name);
 	String get_name() const;
 	String get_name() const;
 
 
 	void set_runnable(bool p_enable);
 	void set_runnable(bool p_enable);
 	bool is_runnable() const;
 	bool is_runnable() const;
 
 
+	void set_dedicated_server(bool p_enable);
+	bool is_dedicated_server() const;
+
 	void set_export_filter(ExportFilter p_filter);
 	void set_export_filter(ExportFilter p_filter);
 	ExportFilter get_export_filter() const;
 	ExportFilter get_export_filter() const;
 
 

+ 122 - 10
editor/export/project_export.cpp

@@ -45,6 +45,7 @@
 #include "scene/gui/link_button.h"
 #include "scene/gui/link_button.h"
 #include "scene/gui/menu_button.h"
 #include "scene/gui/menu_button.h"
 #include "scene/gui/option_button.h"
 #include "scene/gui/option_button.h"
+#include "scene/gui/popup_menu.h"
 #include "scene/gui/split_container.h"
 #include "scene/gui/split_container.h"
 #include "scene/gui/texture_rect.h"
 #include "scene/gui/texture_rect.h"
 #include "scene/gui/tree.h"
 #include "scene/gui/tree.h"
@@ -165,7 +166,7 @@ void ProjectExportDialog::_update_presets() {
 		if (preset->is_runnable()) {
 		if (preset->is_runnable()) {
 			preset_name += " (" + TTR("Runnable") + ")";
 			preset_name += " (" + TTR("Runnable") + ")";
 		}
 		}
-		preset->update_files_to_export();
+		preset->update_files();
 		presets->add_item(preset_name, preset->get_platform()->get_logo());
 		presets->add_item(preset_name, preset->get_platform()->get_logo());
 	}
 	}
 
 
@@ -244,6 +245,7 @@ void ProjectExportDialog::_edit_preset(int p_index) {
 	export_filter->select(current->get_export_filter());
 	export_filter->select(current->get_export_filter());
 	include_filters->set_text(current->get_include_filter());
 	include_filters->set_text(current->get_include_filter());
 	exclude_filters->set_text(current->get_exclude_filter());
 	exclude_filters->set_text(current->get_exclude_filter());
+	server_strip_message->set_visible(current->get_export_filter() == EditorExportPreset::EXPORT_CUSTOMIZED);
 
 
 	_fill_resource_tree();
 	_fill_resource_tree();
 
 
@@ -586,6 +588,7 @@ void ProjectExportDialog::_duplicate_preset() {
 	if (make_runnable) {
 	if (make_runnable) {
 		preset->set_runnable(make_runnable);
 		preset->set_runnable(make_runnable);
 	}
 	}
+	preset->set_dedicated_server(current->is_dedicated_server());
 	preset->set_export_filter(current->get_export_filter());
 	preset->set_export_filter(current->get_export_filter());
 	preset->set_include_filter(current->get_include_filter());
 	preset->set_include_filter(current->get_include_filter());
 	preset->set_exclude_filter(current->get_exclude_filter());
 	preset->set_exclude_filter(current->get_exclude_filter());
@@ -708,7 +711,16 @@ void ProjectExportDialog::_export_type_changed(int p_which) {
 		return;
 		return;
 	}
 	}
 
 
-	current->set_export_filter(EditorExportPreset::ExportFilter(p_which));
+	EditorExportPreset::ExportFilter filter_type = (EditorExportPreset::ExportFilter)p_which;
+	current->set_export_filter(filter_type);
+	current->set_dedicated_server(filter_type == EditorExportPreset::EXPORT_CUSTOMIZED);
+	server_strip_message->set_visible(filter_type == EditorExportPreset::EXPORT_CUSTOMIZED);
+
+	// Default to stripping everything when first switching to server build.
+	if (filter_type == EditorExportPreset::EXPORT_CUSTOMIZED && current->get_customized_files_count() == 0) {
+		current->set_file_export_mode("res://", EditorExportPreset::MODE_FILE_STRIP);
+	}
+
 	updating = true;
 	updating = true;
 	_fill_resource_tree();
 	_fill_resource_tree();
 	updating = false;
 	updating = false;
@@ -744,25 +756,53 @@ void ProjectExportDialog::_fill_resource_tree() {
 		return;
 		return;
 	}
 	}
 
 
+	TreeItem *root = include_files->create_item();
+
+	if (f == EditorExportPreset::EXPORT_CUSTOMIZED) {
+		include_files->set_columns(2);
+		include_files->set_column_expand(1, false);
+		include_files->set_column_custom_minimum_width(1, 250 * EDSCALE);
+	} else {
+		include_files->set_columns(1);
+	}
+
 	include_label->show();
 	include_label->show();
 	include_margin->show();
 	include_margin->show();
 
 
-	TreeItem *root = include_files->create_item();
+	_fill_tree(EditorFileSystem::get_singleton()->get_filesystem(), root, current, f);
+}
 
 
-	_fill_tree(EditorFileSystem::get_singleton()->get_filesystem(), root, current, f == EditorExportPreset::EXPORT_SELECTED_SCENES);
+void ProjectExportDialog::_setup_item_for_file_mode(TreeItem *p_item, EditorExportPreset::FileExportMode p_mode) {
+	if (p_mode == EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED) {
+		p_item->set_checked(0, false);
+		p_item->set_cell_mode(1, TreeItem::CELL_MODE_STRING);
+		p_item->set_text(1, "");
+		p_item->set_editable(1, false);
+		p_item->set_selectable(1, false);
+	} else {
+		p_item->set_checked(0, true);
+		p_item->set_cell_mode(1, TreeItem::CELL_MODE_CUSTOM);
+		p_item->set_text(1, file_mode_popup->get_item_text(file_mode_popup->get_item_index(p_mode)));
+		p_item->set_editable(1, true);
+		p_item->set_selectable(1, true);
+	}
 }
 }
 
 
-bool ProjectExportDialog::_fill_tree(EditorFileSystemDirectory *p_dir, TreeItem *p_item, Ref<EditorExportPreset> &current, bool p_only_scenes) {
+bool ProjectExportDialog::_fill_tree(EditorFileSystemDirectory *p_dir, TreeItem *p_item, Ref<EditorExportPreset> &current, EditorExportPreset::ExportFilter p_export_filter) {
 	p_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
 	p_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
 	p_item->set_icon(0, presets->get_theme_icon(SNAME("folder"), SNAME("FileDialog")));
 	p_item->set_icon(0, presets->get_theme_icon(SNAME("folder"), SNAME("FileDialog")));
 	p_item->set_text(0, p_dir->get_name() + "/");
 	p_item->set_text(0, p_dir->get_name() + "/");
 	p_item->set_editable(0, true);
 	p_item->set_editable(0, true);
 	p_item->set_metadata(0, p_dir->get_path());
 	p_item->set_metadata(0, p_dir->get_path());
 
 
+	if (p_export_filter == EditorExportPreset::EXPORT_CUSTOMIZED) {
+		_setup_item_for_file_mode(p_item, current->get_file_export_mode(p_dir->get_path()));
+	}
+
 	bool used = false;
 	bool used = false;
 	for (int i = 0; i < p_dir->get_subdir_count(); i++) {
 	for (int i = 0; i < p_dir->get_subdir_count(); i++) {
 		TreeItem *subdir = include_files->create_item(p_item);
 		TreeItem *subdir = include_files->create_item(p_item);
-		if (_fill_tree(p_dir->get_subdir(i), subdir, current, p_only_scenes)) {
+		if (_fill_tree(p_dir->get_subdir(i), subdir, current, p_export_filter)) {
 			used = true;
 			used = true;
 		} else {
 		} else {
 			memdelete(subdir);
 			memdelete(subdir);
@@ -771,7 +811,7 @@ bool ProjectExportDialog::_fill_tree(EditorFileSystemDirectory *p_dir, TreeItem
 
 
 	for (int i = 0; i < p_dir->get_file_count(); i++) {
 	for (int i = 0; i < p_dir->get_file_count(); i++) {
 		String type = p_dir->get_file_type(i);
 		String type = p_dir->get_file_type(i);
-		if (p_only_scenes && type != "PackedScene") {
+		if (p_export_filter == EditorExportPreset::EXPORT_SELECTED_SCENES && type != "PackedScene") {
 			continue;
 			continue;
 		}
 		}
 		if (type == "TextFile") {
 		if (type == "TextFile") {
@@ -786,9 +826,14 @@ bool ProjectExportDialog::_fill_tree(EditorFileSystemDirectory *p_dir, TreeItem
 
 
 		file->set_icon(0, EditorNode::get_singleton()->get_class_icon(type));
 		file->set_icon(0, EditorNode::get_singleton()->get_class_icon(type));
 		file->set_editable(0, true);
 		file->set_editable(0, true);
-		file->set_checked(0, current->has_export_file(path));
 		file->set_metadata(0, path);
 		file->set_metadata(0, path);
-		file->propagate_check(0);
+
+		if (p_export_filter == EditorExportPreset::EXPORT_CUSTOMIZED) {
+			_setup_item_for_file_mode(file, current->get_file_export_mode(path));
+		} else {
+			file->set_checked(0, current->has_export_file(path));
+			file->propagate_check(0);
+		}
 
 
 		used = true;
 		used = true;
 	}
 	}
@@ -810,7 +855,19 @@ void ProjectExportDialog::_tree_changed() {
 		return;
 		return;
 	}
 	}
 
 
-	item->propagate_check(0);
+	if (current->get_export_filter() == EditorExportPreset::EXPORT_CUSTOMIZED) {
+		EditorExportPreset::FileExportMode file_mode = EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED;
+		String path = item->get_metadata(0);
+
+		if (item->is_checked(0)) {
+			file_mode = current->get_file_export_mode(path, EditorExportPreset::MODE_FILE_STRIP);
+		}
+
+		_setup_item_for_file_mode(item, file_mode);
+		current->set_file_export_mode(path, file_mode);
+	} else {
+		item->propagate_check(0);
+	}
 }
 }
 
 
 void ProjectExportDialog::_check_propagated_to_item(Object *p_obj, int column) {
 void ProjectExportDialog::_check_propagated_to_item(Object *p_obj, int column) {
@@ -830,6 +887,30 @@ void ProjectExportDialog::_check_propagated_to_item(Object *p_obj, int column) {
 	}
 	}
 }
 }
 
 
+void ProjectExportDialog::_tree_popup_edited(bool p_arrow_clicked) {
+	Rect2 bounds = include_files->get_custom_popup_rect();
+	bounds.position += get_global_canvas_transform().get_origin();
+	bounds.size *= get_global_canvas_transform().get_scale();
+	if (!is_embedding_subwindows()) {
+		bounds.position += get_position();
+	}
+	file_mode_popup->popup(bounds);
+}
+
+void ProjectExportDialog::_set_file_export_mode(int p_id) {
+	Ref<EditorExportPreset> current = get_current_preset();
+	if (current.is_null()) {
+		return;
+	}
+
+	TreeItem *item = include_files->get_edited();
+	String path = item->get_metadata(0);
+
+	current->set_file_export_mode(path, (EditorExportPreset::FileExportMode)p_id);
+
+	item->set_text(1, file_mode_popup->get_item_text(file_mode_popup->get_item_index(p_id)));
+}
+
 void ProjectExportDialog::_export_pck_zip() {
 void ProjectExportDialog::_export_pck_zip() {
 	Ref<EditorExportPreset> current = get_current_preset();
 	Ref<EditorExportPreset> current = get_current_preset();
 	ERR_FAIL_COND(current.is_null());
 	ERR_FAIL_COND(current.is_null());
@@ -1088,6 +1169,7 @@ ProjectExportDialog::ProjectExportDialog() {
 	export_filter->add_item(TTR("Export selected scenes (and dependencies)"));
 	export_filter->add_item(TTR("Export selected scenes (and dependencies)"));
 	export_filter->add_item(TTR("Export selected resources (and dependencies)"));
 	export_filter->add_item(TTR("Export selected resources (and dependencies)"));
 	export_filter->add_item(TTR("Export all resources in the project except resources checked below"));
 	export_filter->add_item(TTR("Export all resources in the project except resources checked below"));
+	export_filter->add_item(TTR("Export as dedicated server"));
 	resources_vb->add_margin_child(TTR("Export Mode:"), export_filter);
 	resources_vb->add_margin_child(TTR("Export Mode:"), export_filter);
 	export_filter->connect("item_selected", callable_mp(this, &ProjectExportDialog::_export_type_changed));
 	export_filter->connect("item_selected", callable_mp(this, &ProjectExportDialog::_export_type_changed));
 
 
@@ -1102,6 +1184,36 @@ ProjectExportDialog::ProjectExportDialog() {
 	include_margin->add_child(include_files);
 	include_margin->add_child(include_files);
 	include_files->connect("item_edited", callable_mp(this, &ProjectExportDialog::_tree_changed));
 	include_files->connect("item_edited", callable_mp(this, &ProjectExportDialog::_tree_changed));
 	include_files->connect("check_propagated_to_item", callable_mp(this, &ProjectExportDialog::_check_propagated_to_item));
 	include_files->connect("check_propagated_to_item", callable_mp(this, &ProjectExportDialog::_check_propagated_to_item));
+	include_files->connect("custom_popup_edited", callable_mp(this, &ProjectExportDialog::_tree_popup_edited));
+
+	server_strip_message = memnew(Label);
+	server_strip_message->set_visible(false);
+	server_strip_message->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
+	resources_vb->add_child(server_strip_message);
+
+	{
+		List<StringName> resource_names;
+		ClassDB::get_inheriters_from_class("Resource", &resource_names);
+
+		PackedStringArray strippable;
+		for (StringName resource_name : resource_names) {
+			if (ClassDB::has_method(resource_name, "create_placeholder", true)) {
+				strippable.push_back(resource_name);
+			}
+		}
+		strippable.sort();
+
+		String message = TTR("\"Strip Visuals\" will replace the following resources with placeholders:") + " ";
+		message += String(", ").join(strippable);
+		server_strip_message->set_text(message);
+	}
+
+	file_mode_popup = memnew(PopupMenu);
+	add_child(file_mode_popup);
+	file_mode_popup->add_item(TTR("Strip Visuals"), EditorExportPreset::MODE_FILE_STRIP);
+	file_mode_popup->add_item(TTR("Keep"), EditorExportPreset::MODE_FILE_KEEP);
+	file_mode_popup->add_item(TTR("Remove"), EditorExportPreset::MODE_FILE_REMOVE);
+	file_mode_popup->connect("id_pressed", callable_mp(this, &ProjectExportDialog::_set_file_export_mode));
 
 
 	include_filters = memnew(LineEdit);
 	include_filters = memnew(LineEdit);
 	resources_vb->add_margin_child(
 	resources_vb->add_margin_child(

+ 8 - 2
editor/export/project_export.h

@@ -31,11 +31,11 @@
 #ifndef PROJECT_EXPORT_H
 #ifndef PROJECT_EXPORT_H
 #define PROJECT_EXPORT_H
 #define PROJECT_EXPORT_H
 
 
+#include "editor/export/editor_export_preset.h"
 #include "scene/gui/dialogs.h"
 #include "scene/gui/dialogs.h"
 
 
 class CheckBox;
 class CheckBox;
 class CheckButton;
 class CheckButton;
-class EditorExportPreset;
 class EditorFileDialog;
 class EditorFileDialog;
 class EditorFileSystemDirectory;
 class EditorFileSystemDirectory;
 class EditorInspector;
 class EditorInspector;
@@ -43,6 +43,7 @@ class EditorPropertyPath;
 class ItemList;
 class ItemList;
 class MenuButton;
 class MenuButton;
 class OptionButton;
 class OptionButton;
+class PopupMenu;
 class RichTextLabel;
 class RichTextLabel;
 class TabContainer;
 class TabContainer;
 class Tree;
 class Tree;
@@ -75,6 +76,8 @@ private:
 	LineEdit *include_filters = nullptr;
 	LineEdit *include_filters = nullptr;
 	LineEdit *exclude_filters = nullptr;
 	LineEdit *exclude_filters = nullptr;
 	Tree *include_files = nullptr;
 	Tree *include_files = nullptr;
+	Label *server_strip_message = nullptr;
+	PopupMenu *file_mode_popup = nullptr;
 
 
 	Label *include_label = nullptr;
 	Label *include_label = nullptr;
 	MarginContainer *include_margin = nullptr;
 	MarginContainer *include_margin = nullptr;
@@ -114,9 +117,12 @@ private:
 	void _export_type_changed(int p_which);
 	void _export_type_changed(int p_which);
 	void _filter_changed(const String &p_filter);
 	void _filter_changed(const String &p_filter);
 	void _fill_resource_tree();
 	void _fill_resource_tree();
-	bool _fill_tree(EditorFileSystemDirectory *p_dir, TreeItem *p_item, Ref<EditorExportPreset> &current, bool p_only_scenes);
+	void _setup_item_for_file_mode(TreeItem *p_item, EditorExportPreset::FileExportMode p_mode);
+	bool _fill_tree(EditorFileSystemDirectory *p_dir, TreeItem *p_item, Ref<EditorExportPreset> &current, EditorExportPreset::ExportFilter p_export_filter);
 	void _tree_changed();
 	void _tree_changed();
 	void _check_propagated_to_item(Object *p_obj, int column);
 	void _check_propagated_to_item(Object *p_obj, int column);
+	void _tree_popup_edited(bool p_arrow_clicked);
+	void _set_file_export_mode(int p_id);
 
 
 	Variant get_drag_data_fw(const Point2 &p_point, Control *p_from);
 	Variant get_drag_data_fw(const Point2 &p_point, Control *p_from);
 	bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const;
 	bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const;

+ 139 - 0
editor/plugins/dedicated_server_export_plugin.cpp

@@ -0,0 +1,139 @@
+/**************************************************************************/
+/*  dedicated_server_export_plugin.cpp                                    */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "dedicated_server_export_plugin.h"
+
+EditorExportPreset::FileExportMode DedicatedServerExportPlugin::_get_export_mode_for_path(const String &p_path) {
+	Ref<EditorExportPreset> preset = get_export_preset();
+	ERR_FAIL_COND_V(preset.is_null(), EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED);
+
+	EditorExportPreset::FileExportMode mode = preset->get_file_export_mode(p_path);
+	if (mode != EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED) {
+		return mode;
+	}
+
+	String path = p_path;
+	if (path.begins_with("res://")) {
+		path = path.substr(6);
+	}
+
+	Vector<String> parts = path.split("/");
+
+	while (parts.size() > 0) {
+		parts.resize(parts.size() - 1);
+
+		String test_path = "res://";
+		if (parts.size() > 0) {
+			test_path += String("/").join(parts) + "/";
+		}
+
+		mode = preset->get_file_export_mode(test_path);
+		if (mode != EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED) {
+			break;
+		}
+	}
+
+	return mode;
+}
+
+PackedStringArray DedicatedServerExportPlugin::_get_export_features(const Ref<EditorExportPlatform> &p_platform, bool p_debug) const {
+	PackedStringArray ret;
+
+	Ref<EditorExportPreset> preset = get_export_preset();
+	ERR_FAIL_COND_V(preset.is_null(), ret);
+
+	if (preset->is_dedicated_server()) {
+		ret.append("dedicated_server");
+	}
+	return ret;
+}
+
+uint64_t DedicatedServerExportPlugin::_get_customization_configuration_hash() const {
+	Ref<EditorExportPreset> preset = get_export_preset();
+	ERR_FAIL_COND_V(preset.is_null(), 0);
+
+	if (preset->get_export_filter() != EditorExportPreset::EXPORT_CUSTOMIZED) {
+		return 0;
+	}
+
+	return preset->get_customized_files().hash();
+}
+
+bool DedicatedServerExportPlugin::_begin_customize_scenes(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) {
+	Ref<EditorExportPreset> preset = get_export_preset();
+	ERR_FAIL_COND_V(preset.is_null(), false);
+
+	current_export_mode = EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED;
+
+	return preset->get_export_filter() == EditorExportPreset::EXPORT_CUSTOMIZED;
+}
+
+bool DedicatedServerExportPlugin::_begin_customize_resources(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) {
+	Ref<EditorExportPreset> preset = get_export_preset();
+	ERR_FAIL_COND_V(preset.is_null(), false);
+
+	current_export_mode = EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED;
+
+	return preset->get_export_filter() == EditorExportPreset::EXPORT_CUSTOMIZED;
+}
+
+Node *DedicatedServerExportPlugin::_customize_scene(Node *p_root, const String &p_path) {
+	// Simply set the export mode based on the scene path. All the real
+	// customization happens in _customize_resource().
+	current_export_mode = _get_export_mode_for_path(p_path);
+	return nullptr;
+}
+
+Ref<Resource> DedicatedServerExportPlugin::_customize_resource(const Ref<Resource> &p_resource, const String &p_path) {
+	// If the resource has a path, we use that to get our export mode. But if it
+	// doesn't, we assume that this resource is embedded in the last resource with
+	// a path.
+	if (p_path != "") {
+		current_export_mode = _get_export_mode_for_path(p_path);
+	}
+
+	if (p_resource.is_valid() && current_export_mode == EditorExportPreset::MODE_FILE_STRIP && p_resource->has_method("create_placeholder")) {
+		Callable::CallError err;
+		Ref<Resource> result = const_cast<Resource *>(p_resource.ptr())->callp("create_placeholder", nullptr, 0, err);
+		if (err.error == Callable::CallError::CALL_OK) {
+			return result;
+		}
+	}
+
+	return Ref<Resource>();
+}
+
+void DedicatedServerExportPlugin::_end_customize_scenes() {
+	current_export_mode = EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED;
+}
+
+void DedicatedServerExportPlugin::_end_customize_resources() {
+	current_export_mode = EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED;
+}

+ 58 - 0
editor/plugins/dedicated_server_export_plugin.h

@@ -0,0 +1,58 @@
+/**************************************************************************/
+/*  dedicated_server_export_plugin.h                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#ifndef DEDICATED_SERVER_EXPORT_PLUGIN_H
+#define DEDICATED_SERVER_EXPORT_PLUGIN_H
+
+#include "editor/export/editor_export.h"
+
+class DedicatedServerExportPlugin : public EditorExportPlugin {
+private:
+	EditorExportPreset::FileExportMode current_export_mode;
+
+	EditorExportPreset::FileExportMode _get_export_mode_for_path(const String &p_path);
+
+protected:
+	String _get_name() const override { return "DedicatedServer"; }
+
+	PackedStringArray _get_export_features(const Ref<EditorExportPlatform> &p_platform, bool p_debug) const override;
+	uint64_t _get_customization_configuration_hash() const override;
+
+	bool _begin_customize_scenes(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) override;
+	bool _begin_customize_resources(const Ref<EditorExportPlatform> &p_platform, const Vector<String> &p_features) override;
+
+	Node *_customize_scene(Node *p_root, const String &p_path) override;
+	Ref<Resource> _customize_resource(const Ref<Resource> &p_resource, const String &p_path) override;
+
+	void _end_customize_scenes() override;
+	void _end_customize_resources() override;
+};
+
+#endif // DEDICATED_SERVER_EXPORT_PLUGIN_H

+ 16 - 6
main/main.cpp

@@ -207,6 +207,11 @@ static bool dump_extension_api = false;
 #endif
 #endif
 bool profile_gpu = false;
 bool profile_gpu = false;
 
 
+// Constants.
+
+static const String NULL_DISPLAY_DRIVER("headless");
+static const String NULL_AUDIO_DRIVER("Dummy");
+
 /* Helper methods */
 /* Helper methods */
 
 
 bool Main::is_cmdline_tool() {
 bool Main::is_cmdline_tool() {
@@ -997,8 +1002,8 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
 
 
 		} else if (I->get() == "--headless") { // enable headless mode (no audio, no rendering).
 		} else if (I->get() == "--headless") { // enable headless mode (no audio, no rendering).
 
 
-			audio_driver = "Dummy";
-			display_driver = "headless";
+			audio_driver = NULL_AUDIO_DRIVER;
+			display_driver = NULL_DISPLAY_DRIVER;
 
 
 		} else if (I->get() == "--profiling") { // enable profiling
 		} else if (I->get() == "--profiling") { // enable profiling
 
 
@@ -1133,8 +1138,8 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
 
 
 			// `--doctool` implies `--headless` to avoid spawning an unnecessary window
 			// `--doctool` implies `--headless` to avoid spawning an unnecessary window
 			// and speed up class reference generation.
 			// and speed up class reference generation.
-			audio_driver = "Dummy";
-			display_driver = "headless";
+			audio_driver = NULL_AUDIO_DRIVER;
+			display_driver = NULL_DISPLAY_DRIVER;
 			main_args.push_back(I->get());
 			main_args.push_back(I->get());
 #endif
 #endif
 		} else if (I->get() == "--path") { // set path of project to start or edit
 		} else if (I->get() == "--path") { // set path of project to start or edit
@@ -1371,6 +1376,11 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
 
 
 	ResourceUID::get_singleton()->load_from_cache(); // load UUIDs from cache.
 	ResourceUID::get_singleton()->load_from_cache(); // load UUIDs from cache.
 
 
+	if (ProjectSettings::get_singleton()->has_custom_feature("dedicated_server")) {
+		audio_driver = NULL_AUDIO_DRIVER;
+		display_driver = NULL_DISPLAY_DRIVER;
+	}
+
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "network/limits/debugger/max_chars_per_second", PROPERTY_HINT_RANGE, "0, 4096, 1, or_greater"), 32768);
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "network/limits/debugger/max_chars_per_second", PROPERTY_HINT_RANGE, "0, 4096, 1, or_greater"), 32768);
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "network/limits/debugger/max_queued_messages", PROPERTY_HINT_RANGE, "0, 8192, 1, or_greater"), 2048);
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "network/limits/debugger/max_queued_messages", PROPERTY_HINT_RANGE, "0, 8192, 1, or_greater"), 2048);
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "network/limits/debugger/max_errors_per_second", PROPERTY_HINT_RANGE, "0, 200, 1, or_greater"), 400);
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "network/limits/debugger/max_errors_per_second", PROPERTY_HINT_RANGE, "0, 200, 1, or_greater"), 400);
@@ -1698,7 +1708,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
 
 
 	// Display driver, e.g. X11, Wayland.
 	// Display driver, e.g. X11, Wayland.
 	// Make sure that headless is the last one, which it is assumed to be by design.
 	// Make sure that headless is the last one, which it is assumed to be by design.
-	DEV_ASSERT(String("headless") == DisplayServer::get_create_function_name(DisplayServer::get_create_function_count() - 1));
+	DEV_ASSERT(NULL_DISPLAY_DRIVER == DisplayServer::get_create_function_name(DisplayServer::get_create_function_count() - 1));
 	for (int i = 0; i < DisplayServer::get_create_function_count(); i++) {
 	for (int i = 0; i < DisplayServer::get_create_function_count(); i++) {
 		String name = DisplayServer::get_create_function_name(i);
 		String name = DisplayServer::get_create_function_name(i);
 		if (display_driver == name) {
 		if (display_driver == name) {
@@ -1723,7 +1733,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
 	}
 	}
 
 
 	// Make sure that dummy is the last one, which it is assumed to be by design.
 	// Make sure that dummy is the last one, which it is assumed to be by design.
-	DEV_ASSERT(String("Dummy") == AudioDriverManager::get_driver(AudioDriverManager::get_driver_count() - 1)->get_name());
+	DEV_ASSERT(NULL_AUDIO_DRIVER == AudioDriverManager::get_driver(AudioDriverManager::get_driver_count() - 1)->get_name());
 	for (int i = 0; i < AudioDriverManager::get_driver_count(); i++) {
 	for (int i = 0; i < AudioDriverManager::get_driver_count(); i++) {
 		if (audio_driver == AudioDriverManager::get_driver(i)->get_name()) {
 		if (audio_driver == AudioDriverManager::get_driver(i)->get_name()) {
 			audio_driver_idx = i;
 			audio_driver_idx = i;

+ 8 - 0
scene/resources/material.cpp

@@ -113,6 +113,12 @@ bool Material::_can_use_render_priority() const {
 	return ret;
 	return ret;
 }
 }
 
 
+Ref<Resource> Material::create_placeholder() const {
+	Ref<PlaceholderMaterial> placeholder;
+	placeholder.instantiate();
+	return placeholder;
+}
+
 void Material::_bind_methods() {
 void Material::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_next_pass", "next_pass"), &Material::set_next_pass);
 	ClassDB::bind_method(D_METHOD("set_next_pass", "next_pass"), &Material::set_next_pass);
 	ClassDB::bind_method(D_METHOD("get_next_pass"), &Material::get_next_pass);
 	ClassDB::bind_method(D_METHOD("get_next_pass"), &Material::get_next_pass);
@@ -123,6 +129,8 @@ void Material::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("inspect_native_shader_code"), &Material::inspect_native_shader_code);
 	ClassDB::bind_method(D_METHOD("inspect_native_shader_code"), &Material::inspect_native_shader_code);
 	ClassDB::set_method_flags(get_class_static(), _scs_create("inspect_native_shader_code"), METHOD_FLAGS_DEFAULT | METHOD_FLAG_EDITOR);
 	ClassDB::set_method_flags(get_class_static(), _scs_create("inspect_native_shader_code"), METHOD_FLAGS_DEFAULT | METHOD_FLAG_EDITOR);
 
 
+	ClassDB::bind_method(D_METHOD("create_placeholder"), &Material::create_placeholder);
+
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "render_priority", PROPERTY_HINT_RANGE, itos(RENDER_PRIORITY_MIN) + "," + itos(RENDER_PRIORITY_MAX) + ",1"), "set_render_priority", "get_render_priority");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "render_priority", PROPERTY_HINT_RANGE, itos(RENDER_PRIORITY_MIN) + "," + itos(RENDER_PRIORITY_MAX) + ",1"), "set_render_priority", "get_render_priority");
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "next_pass", PROPERTY_HINT_RESOURCE_TYPE, "Material"), "set_next_pass", "get_next_pass");
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "next_pass", PROPERTY_HINT_RESOURCE_TYPE, "Material"), "set_next_pass", "get_next_pass");
 
 

+ 3 - 0
scene/resources/material.h

@@ -74,6 +74,9 @@ public:
 	virtual RID get_rid() const override;
 	virtual RID get_rid() const override;
 	virtual RID get_shader_rid() const;
 	virtual RID get_shader_rid() const;
 	virtual Shader::Mode get_shader_mode() const;
 	virtual Shader::Mode get_shader_mode() const;
+
+	virtual Ref<Resource> create_placeholder() const;
+
 	Material();
 	Material();
 	virtual ~Material();
 	virtual ~Material();
 };
 };

+ 8 - 0
scene/resources/mesh.cpp

@@ -614,6 +614,13 @@ Size2i Mesh::get_lightmap_size_hint() const {
 	return lightmap_size_hint;
 	return lightmap_size_hint;
 }
 }
 
 
+Ref<Resource> Mesh::create_placeholder() const {
+	Ref<PlaceholderMesh> placeholder;
+	placeholder.instantiate();
+	placeholder->set_aabb(get_aabb());
+	return placeholder;
+}
+
 void Mesh::_bind_methods() {
 void Mesh::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_lightmap_size_hint", "size"), &Mesh::set_lightmap_size_hint);
 	ClassDB::bind_method(D_METHOD("set_lightmap_size_hint", "size"), &Mesh::set_lightmap_size_hint);
 	ClassDB::bind_method(D_METHOD("get_lightmap_size_hint"), &Mesh::get_lightmap_size_hint);
 	ClassDB::bind_method(D_METHOD("get_lightmap_size_hint"), &Mesh::get_lightmap_size_hint);
@@ -626,6 +633,7 @@ void Mesh::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("surface_get_blend_shape_arrays", "surf_idx"), &Mesh::surface_get_blend_shape_arrays);
 	ClassDB::bind_method(D_METHOD("surface_get_blend_shape_arrays", "surf_idx"), &Mesh::surface_get_blend_shape_arrays);
 	ClassDB::bind_method(D_METHOD("surface_set_material", "surf_idx", "material"), &Mesh::surface_set_material);
 	ClassDB::bind_method(D_METHOD("surface_set_material", "surf_idx", "material"), &Mesh::surface_set_material);
 	ClassDB::bind_method(D_METHOD("surface_get_material", "surf_idx"), &Mesh::surface_get_material);
 	ClassDB::bind_method(D_METHOD("surface_get_material", "surf_idx"), &Mesh::surface_get_material);
+	ClassDB::bind_method(D_METHOD("create_placeholder"), &Mesh::create_placeholder);
 
 
 	BIND_ENUM_CONSTANT(PRIMITIVE_POINTS);
 	BIND_ENUM_CONSTANT(PRIMITIVE_POINTS);
 	BIND_ENUM_CONSTANT(PRIMITIVE_LINES);
 	BIND_ENUM_CONSTANT(PRIMITIVE_LINES);

+ 2 - 0
scene/resources/mesh.h

@@ -220,6 +220,8 @@ public:
 	virtual int get_builtin_bind_pose_count() const;
 	virtual int get_builtin_bind_pose_count() const;
 	virtual Transform3D get_builtin_bind_pose(int p_index) const;
 	virtual Transform3D get_builtin_bind_pose(int p_index) const;
 
 
+	virtual Ref<Resource> create_placeholder() const;
+
 	Mesh();
 	Mesh();
 };
 };
 
 

+ 53 - 0
scene/resources/texture.cpp

@@ -94,6 +94,13 @@ bool Texture2D::get_rect_region(const Rect2 &p_rect, const Rect2 &p_src_rect, Re
 	return true;
 	return true;
 }
 }
 
 
+Ref<Resource> Texture2D::create_placeholder() const {
+	Ref<PlaceholderTexture2D> placeholder;
+	placeholder.instantiate();
+	placeholder->set_size(get_size());
+	return placeholder;
+}
+
 void Texture2D::_bind_methods() {
 void Texture2D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_width"), &Texture2D::get_width);
 	ClassDB::bind_method(D_METHOD("get_width"), &Texture2D::get_width);
 	ClassDB::bind_method(D_METHOD("get_height"), &Texture2D::get_height);
 	ClassDB::bind_method(D_METHOD("get_height"), &Texture2D::get_height);
@@ -103,6 +110,7 @@ void Texture2D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("draw_rect", "canvas_item", "rect", "tile", "modulate", "transpose"), &Texture2D::draw_rect, DEFVAL(Color(1, 1, 1)), DEFVAL(false));
 	ClassDB::bind_method(D_METHOD("draw_rect", "canvas_item", "rect", "tile", "modulate", "transpose"), &Texture2D::draw_rect, DEFVAL(Color(1, 1, 1)), DEFVAL(false));
 	ClassDB::bind_method(D_METHOD("draw_rect_region", "canvas_item", "rect", "src_rect", "modulate", "transpose", "clip_uv"), &Texture2D::draw_rect_region, DEFVAL(Color(1, 1, 1)), DEFVAL(false), DEFVAL(true));
 	ClassDB::bind_method(D_METHOD("draw_rect_region", "canvas_item", "rect", "src_rect", "modulate", "transpose", "clip_uv"), &Texture2D::draw_rect_region, DEFVAL(Color(1, 1, 1)), DEFVAL(false), DEFVAL(true));
 	ClassDB::bind_method(D_METHOD("get_image"), &Texture2D::get_image);
 	ClassDB::bind_method(D_METHOD("get_image"), &Texture2D::get_image);
+	ClassDB::bind_method(D_METHOD("create_placeholder"), &Texture2D::create_placeholder);
 
 
 	ADD_GROUP("", "");
 	ADD_GROUP("", "");
 
 
@@ -1137,6 +1145,7 @@ void Texture3D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_depth"), &Texture3D::get_depth);
 	ClassDB::bind_method(D_METHOD("get_depth"), &Texture3D::get_depth);
 	ClassDB::bind_method(D_METHOD("has_mipmaps"), &Texture3D::has_mipmaps);
 	ClassDB::bind_method(D_METHOD("has_mipmaps"), &Texture3D::has_mipmaps);
 	ClassDB::bind_method(D_METHOD("get_data"), &Texture3D::_get_datai);
 	ClassDB::bind_method(D_METHOD("get_data"), &Texture3D::_get_datai);
+	ClassDB::bind_method(D_METHOD("create_placeholder"), &Texture3D::create_placeholder);
 
 
 	GDVIRTUAL_BIND(_get_format);
 	GDVIRTUAL_BIND(_get_format);
 	GDVIRTUAL_BIND(_get_width);
 	GDVIRTUAL_BIND(_get_width);
@@ -1145,6 +1154,14 @@ void Texture3D::_bind_methods() {
 	GDVIRTUAL_BIND(_has_mipmaps);
 	GDVIRTUAL_BIND(_has_mipmaps);
 	GDVIRTUAL_BIND(_get_data);
 	GDVIRTUAL_BIND(_get_data);
 }
 }
+
+Ref<Resource> Texture3D::create_placeholder() const {
+	Ref<PlaceholderTexture3D> placeholder;
+	placeholder.instantiate();
+	placeholder->set_size(Vector3i(get_width(), get_height(), get_depth()));
+	return placeholder;
+}
+
 //////////////////////////////////////////
 //////////////////////////////////////////
 
 
 Image::Format ImageTexture3D::get_format() const {
 Image::Format ImageTexture3D::get_format() const {
@@ -3048,6 +3065,42 @@ ImageTextureLayered::~ImageTextureLayered() {
 	}
 	}
 }
 }
 
 
+void Texture2DArray::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("create_placeholder"), &Texture2DArray::create_placeholder);
+}
+
+Ref<Resource> Texture2DArray::create_placeholder() const {
+	Ref<PlaceholderTexture2DArray> placeholder;
+	placeholder.instantiate();
+	placeholder->set_size(Size2i(get_width(), get_height()));
+	placeholder->set_layers(get_layers());
+	return placeholder;
+}
+
+void Cubemap::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("create_placeholder"), &Cubemap::create_placeholder);
+}
+
+Ref<Resource> Cubemap::create_placeholder() const {
+	Ref<PlaceholderCubemap> placeholder;
+	placeholder.instantiate();
+	placeholder->set_size(Size2i(get_width(), get_height()));
+	placeholder->set_layers(get_layers());
+	return placeholder;
+}
+
+void CubemapArray::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("create_placeholder"), &CubemapArray::create_placeholder);
+}
+
+Ref<Resource> CubemapArray::create_placeholder() const {
+	Ref<PlaceholderCubemapArray> placeholder;
+	placeholder.instantiate();
+	placeholder->set_size(Size2i(get_width(), get_height()));
+	placeholder->set_layers(get_layers());
+	return placeholder;
+}
+
 ///////////////////////////////////////////
 ///////////////////////////////////////////
 
 
 void CompressedTextureLayered::set_path(const String &p_path, bool p_take_over) {
 void CompressedTextureLayered::set_path(const String &p_path, bool p_take_over) {

+ 19 - 0
scene/resources/texture.h

@@ -82,6 +82,8 @@ public:
 
 
 	virtual Ref<Image> get_image() const { return Ref<Image>(); }
 	virtual Ref<Image> get_image() const { return Ref<Image>(); }
 
 
+	virtual Ref<Resource> create_placeholder() const;
+
 	Texture2D();
 	Texture2D();
 };
 };
 
 
@@ -450,25 +452,41 @@ public:
 
 
 class Texture2DArray : public ImageTextureLayered {
 class Texture2DArray : public ImageTextureLayered {
 	GDCLASS(Texture2DArray, ImageTextureLayered)
 	GDCLASS(Texture2DArray, ImageTextureLayered)
+
+protected:
+	static void _bind_methods();
+
 public:
 public:
 	Texture2DArray() :
 	Texture2DArray() :
 			ImageTextureLayered(LAYERED_TYPE_2D_ARRAY) {}
 			ImageTextureLayered(LAYERED_TYPE_2D_ARRAY) {}
+
+	virtual Ref<Resource> create_placeholder() const;
 };
 };
 
 
 class Cubemap : public ImageTextureLayered {
 class Cubemap : public ImageTextureLayered {
 	GDCLASS(Cubemap, ImageTextureLayered);
 	GDCLASS(Cubemap, ImageTextureLayered);
 
 
+protected:
+	static void _bind_methods();
+
 public:
 public:
 	Cubemap() :
 	Cubemap() :
 			ImageTextureLayered(LAYERED_TYPE_CUBEMAP) {}
 			ImageTextureLayered(LAYERED_TYPE_CUBEMAP) {}
+
+	virtual Ref<Resource> create_placeholder() const;
 };
 };
 
 
 class CubemapArray : public ImageTextureLayered {
 class CubemapArray : public ImageTextureLayered {
 	GDCLASS(CubemapArray, ImageTextureLayered);
 	GDCLASS(CubemapArray, ImageTextureLayered);
 
 
+protected:
+	static void _bind_methods();
+
 public:
 public:
 	CubemapArray() :
 	CubemapArray() :
 			ImageTextureLayered(LAYERED_TYPE_CUBEMAP_ARRAY) {}
 			ImageTextureLayered(LAYERED_TYPE_CUBEMAP_ARRAY) {}
+
+	virtual Ref<Resource> create_placeholder() const;
 };
 };
 
 
 class CompressedTextureLayered : public TextureLayered {
 class CompressedTextureLayered : public TextureLayered {
@@ -580,6 +598,7 @@ public:
 	virtual int get_depth() const;
 	virtual int get_depth() const;
 	virtual bool has_mipmaps() const;
 	virtual bool has_mipmaps() const;
 	virtual Vector<Ref<Image>> get_data() const;
 	virtual Vector<Ref<Image>> get_data() const;
+	virtual Ref<Resource> create_placeholder() const;
 };
 };
 
 
 class ImageTexture3D : public Texture3D {
 class ImageTexture3D : public Texture3D {