Browse Source

Merge pull request #31171 from aaronfranke/feature-system

Rémi Verschelde 3 years ago
parent
commit
e49b127b41
4 changed files with 252 additions and 52 deletions
  1. 78 0
      core/config/project_settings.cpp
  2. 7 1
      core/config/project_settings.h
  3. 3 1
      core/io/config_file.cpp
  4. 164 50
      editor/project_manager.cpp

+ 78 - 0
core/config/project_settings.cpp

@@ -41,6 +41,9 @@
 #include "core/os/keyboard.h"
 #include "core/os/os.h"
 #include "core/variant/variant_parser.h"
+#include "core/version.h"
+
+#include "modules/modules_enabled.gen.h" // For mono.
 
 const String ProjectSettings::PROJECT_DATA_DIR_NAME_SUFFIX = "godot";
 
@@ -66,6 +69,69 @@ String ProjectSettings::get_imported_files_path() const {
 	return get_project_data_path().plus_file("imported");
 }
 
+// Returns the features that a project must have when opened with this build of Godot.
+// This is used by the project manager to provide the initial_settings for config/features.
+const PackedStringArray ProjectSettings::get_required_features() {
+	PackedStringArray features = PackedStringArray();
+	features.append(VERSION_BRANCH);
+#ifdef REAL_T_IS_DOUBLE
+	features.append("Double Precision");
+#endif
+	return features;
+}
+
+// Returns the features supported by this build of Godot. Includes all required features.
+const PackedStringArray ProjectSettings::_get_supported_features() {
+	PackedStringArray features = get_required_features();
+#ifdef MODULE_MONO_ENABLED
+	features.append("C#");
+#endif
+	// Allow pinning to a specific patch number or build type by marking
+	// them as supported. They're only used if the user adds them manually.
+	features.append(VERSION_BRANCH "." _MKSTR(VERSION_PATCH));
+	features.append(VERSION_FULL_CONFIG);
+	features.append(VERSION_FULL_BUILD);
+	// For now, assume Vulkan is always supported.
+	// This should be removed if it's possible to build the editor without Vulkan.
+	features.append("Vulkan Clustered");
+	features.append("Vulkan Mobile");
+	return features;
+}
+
+// Returns the features that this project needs but this build of Godot lacks.
+const PackedStringArray ProjectSettings::get_unsupported_features(const PackedStringArray &p_project_features) {
+	PackedStringArray unsupported_features = PackedStringArray();
+	PackedStringArray supported_features = singleton->_get_supported_features();
+	for (int i = 0; i < p_project_features.size(); i++) {
+		if (!supported_features.has(p_project_features[i])) {
+			unsupported_features.append(p_project_features[i]);
+		}
+	}
+	unsupported_features.sort();
+	return unsupported_features;
+}
+
+// Returns the features that both this project has and this build of Godot has, ensuring required features exist.
+const PackedStringArray ProjectSettings::_trim_to_supported_features(const PackedStringArray &p_project_features) {
+	// Remove unsupported features if present.
+	PackedStringArray features = PackedStringArray(p_project_features);
+	PackedStringArray supported_features = _get_supported_features();
+	for (int i = p_project_features.size() - 1; i > -1; i--) {
+		if (!supported_features.has(p_project_features[i])) {
+			features.remove_at(i);
+		}
+	}
+	// Add required features if not present.
+	PackedStringArray required_features = get_required_features();
+	for (int i = 0; i < required_features.size(); i++) {
+		if (!features.has(required_features[i])) {
+			features.append(required_features[i]);
+		}
+	}
+	features.sort();
+	return features;
+}
+
 String ProjectSettings::localize_path(const String &p_path) const {
 	if (resource_path.is_empty() || p_path.begins_with("res://") || p_path.begins_with("user://") ||
 			(p_path.is_absolute_path() && !p_path.begins_with(resource_path))) {
@@ -635,6 +701,11 @@ Error ProjectSettings::_load_settings_text(const String &p_path) {
 			} else {
 				if (section == String()) {
 					set(assign, value);
+				} else if (section == "application" && assign == "config/features") {
+					const PackedStringArray project_features_untrimmed = value;
+					const PackedStringArray project_features = _trim_to_supported_features(project_features_untrimmed);
+					set("application/config/features", project_features);
+					save();
 				} else {
 					set(section + "/" + assign, value);
 				}
@@ -666,6 +737,13 @@ Error ProjectSettings::_load_settings_text_or_binary(const String &p_text_path,
 	return err;
 }
 
+Error ProjectSettings::load_custom(const String &p_path) {
+	if (p_path.ends_with(".binary")) {
+		return _load_settings_binary(p_path);
+	}
+	return _load_settings_text(p_path);
+}
+
 int ProjectSettings::get_order(const String &p_name) const {
 	ERR_FAIL_COND_V_MSG(!props.has(p_name), -1, "Request for nonexistent project setting: " + p_name + ".");
 	return props[p_name].order;

+ 7 - 1
core/config/project_settings.h

@@ -48,6 +48,8 @@ public:
 		//properties that are not for built in values begin from this value, so builtin ones are displayed first
 		NO_BUILTIN_ORDER_BASE = 1 << 16
 	};
+	const static PackedStringArray get_required_features();
+	const static PackedStringArray get_unsupported_features(const PackedStringArray &p_project_features);
 
 	struct AutoloadInfo {
 		StringName name;
@@ -111,6 +113,9 @@ protected:
 
 	Error _save_custom_bnd(const String &p_file);
 
+	const static PackedStringArray _get_supported_features();
+	const static PackedStringArray _trim_to_supported_features(const PackedStringArray &p_project_features);
+
 	void _convert_to_last_version(int p_from_version);
 
 	bool _load_resource_pack(const String &p_pack, bool p_replace_files = true, int p_offset = 0);
@@ -125,7 +130,7 @@ protected:
 	static void _bind_methods();
 
 public:
-	static const int CONFIG_VERSION = 4;
+	static const int CONFIG_VERSION = 5;
 
 	void set_setting(const String &p_setting, const Variant &p_value);
 	Variant get_setting(const String &p_setting) const;
@@ -158,6 +163,7 @@ public:
 
 	Error setup(const String &p_path, const String &p_main_pack, bool p_upwards = false, bool p_ignore_override = false);
 
+	Error load_custom(const String &p_path);
 	Error save_custom(const String &p_path = "", const CustomMap &p_custom = CustomMap(), const Vector<String> &p_custom_features = Vector<String>(), bool p_merge_with_current = true);
 	Error save();
 	void set_custom_property_info(const String &p_prop, const PropertyInfo &p_info);

+ 3 - 1
core/io/config_file.cpp

@@ -183,7 +183,9 @@ Error ConfigFile::_internal_save(FileAccess *file) {
 		if (E != values.front()) {
 			file->store_string("\n");
 		}
-		file->store_string("[" + E.key() + "]\n\n");
+		if (E.key() != "") {
+			file->store_string("[" + E.key() + "]\n\n");
+		}
 
 		for (OrderedHashMap<String, Variant>::Element F = E.get().front(); F; F = F.next()) {
 			String vstr;

+ 164 - 50
editor/project_manager.cpp

@@ -478,8 +478,20 @@ private:
 						cd->grab_focus();
 						return;
 					}
+					PackedStringArray project_features = ProjectSettings::get_required_features();
 					ProjectSettings::CustomMap initial_settings;
-					initial_settings["rendering/vulkan/rendering/back_end"] = rasterizer_button_group->get_pressed_button()->get_meta(SNAME("driver_name"));
+					// Be sure to change this code if/when renderers are changed.
+					int renderer_type = rasterizer_button_group->get_pressed_button()->get_meta(SNAME("driver_name"));
+					initial_settings["rendering/vulkan/rendering/back_end"] = renderer_type;
+					if (renderer_type == 0) {
+						project_features.push_back("Vulkan Clustered");
+					} else if (renderer_type == 1) {
+						project_features.push_back("Vulkan Mobile");
+					} else {
+						WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
+					}
+					project_features.sort();
+					initial_settings["application/config/features"] = project_features;
 					initial_settings["application/config/name"] = project_name->get_text().strip_edges();
 					initial_settings["application/config/icon"] = "res://icon.png";
 
@@ -1019,6 +1031,7 @@ public:
 		String path;
 		String icon;
 		String main_scene;
+		PackedStringArray unsupported_features;
 		uint64_t last_edited = 0;
 		bool favorite = false;
 		bool grayed = false;
@@ -1035,6 +1048,7 @@ public:
 				const String &p_path,
 				const String &p_icon,
 				const String &p_main_scene,
+				const PackedStringArray &p_unsupported_features,
 				uint64_t p_last_edited,
 				bool p_favorite,
 				bool p_grayed,
@@ -1046,6 +1060,7 @@ public:
 			path = p_path;
 			icon = p_icon;
 			main_scene = p_main_scene;
+			unsupported_features = p_unsupported_features;
 			last_edited = p_last_edited;
 			favorite = p_favorite;
 			grayed = p_grayed;
@@ -1097,8 +1112,7 @@ private:
 	void remove_project(int p_index, bool p_update_settings);
 	void update_icons_async();
 	void load_project_icon(int p_index);
-
-	static void load_project_data(const String &p_property_key, Item &p_item, bool p_favorite);
+	static Item load_project_data(const String &p_property_key, bool p_favorite);
 
 	String _search_term;
 	FilterOption _order_option;
@@ -1189,7 +1203,8 @@ void ProjectList::load_project_icon(int p_index) {
 	item.control->icon_needs_reload = false;
 }
 
-void ProjectList::load_project_data(const String &p_property_key, Item &p_item, bool p_favorite) {
+// Load project data from p_property_key and return it in a ProjectList::Item. p_favorite is passed directly into the Item.
+ProjectList::Item ProjectList::load_project_data(const String &p_property_key, bool p_favorite) {
 	String path = EditorSettings::get_singleton()->get(p_property_key);
 	String conf = path.plus_file("project.godot");
 	bool grayed = false;
@@ -1209,13 +1224,56 @@ void ProjectList::load_project_data(const String &p_property_key, Item &p_item,
 	}
 
 	if (config_version > ProjectSettings::CONFIG_VERSION) {
-		// Comes from an incompatible (more recent) Godot version, grey it out
+		// Comes from an incompatible (more recent) Godot version, gray it out.
 		grayed = true;
 	}
 
-	String description = cf->get_value("application", "config/description", "");
-	String icon = cf->get_value("application", "config/icon", "");
-	String main_scene = cf->get_value("application", "run/main_scene", "");
+	const String description = cf->get_value("application", "config/description", "");
+	const String icon = cf->get_value("application", "config/icon", "");
+	const String main_scene = cf->get_value("application", "run/main_scene", "");
+
+	PackedStringArray project_features = cf->get_value("application", "config/features", PackedStringArray());
+	bool project_features_dirty = false;
+	// If there is no feature list currently present, force one to generate.
+	if (project_features.is_empty()) {
+		project_features = ProjectSettings::get_required_features();
+		project_features_dirty = true;
+	}
+	// Check the rendering API.
+	const String rendering_api = cf->get_value("rendering", "quality/driver/driver_name", "");
+	if (rendering_api != "") {
+		// Add the rendering API as a project feature if it doesn't already exist.
+		if (!project_features.has(rendering_api)) {
+			project_features.append(rendering_api);
+			project_features_dirty = true;
+		}
+	}
+	// Check for the existence of a csproj file.
+	if (FileAccess::exists(path.plus_file(project_name + ".csproj"))) {
+		// If there is a csproj file, add the C# feature if it doesn't already exist.
+		if (!project_features.has("C#")) {
+			project_features.append("C#");
+			project_features_dirty = true;
+		}
+	} else {
+		// If there isn't a csproj file, remove the C# feature if it exists.
+		if (project_features.has("C#")) {
+			project_features.remove_at(project_features.find("C#"));
+			project_features_dirty = true;
+		}
+	}
+	if (project_features_dirty) {
+		project_features.sort();
+		// Write the updated feature list, but only if the project config version is the same.
+		// Never write to project files with a different config version!
+		if (config_version == ProjectSettings::CONFIG_VERSION) {
+			ProjectSettings *ps = ProjectSettings::get_singleton();
+			ps->load_custom(conf);
+			ps->set("application/config/features", project_features);
+			ps->save_custom(conf);
+		}
+	}
+	PackedStringArray unsupported_features = ProjectSettings::get_unsupported_features(project_features);
 
 	uint64_t last_edited = 0;
 	if (FileAccess::exists(conf)) {
@@ -1237,9 +1295,9 @@ void ProjectList::load_project_data(const String &p_property_key, Item &p_item,
 		print_line("Project is missing: " + conf);
 	}
 
-	String project_key = p_property_key.get_slice("/", 1);
+	const String project_key = p_property_key.get_slice("/", 1);
 
-	p_item = Item(project_key, project_name, description, path, icon, main_scene, last_edited, p_favorite, grayed, missing, config_version);
+	return Item(project_key, project_name, description, path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
 }
 
 void ProjectList::load_projects() {
@@ -1282,8 +1340,7 @@ void ProjectList::load_projects() {
 		String project_key = property_key.get_slice("/", 1);
 		bool favorite = favorites.has("favorite_projects/" + project_key);
 
-		Item item;
-		load_project_data(property_key, item, favorite);
+		Item item = load_project_data(property_key, favorite);
 
 		_projects.push_back(item);
 	}
@@ -1366,7 +1423,7 @@ void ProjectList::create_project_item_control(int p_index) {
 	TextureButton *favorite = memnew(TextureButton);
 	favorite->set_name("FavoriteButton");
 	favorite->set_normal_texture(favorite_icon);
-	// This makes the project's "hover" style display correctly when hovering the favorite icon
+	// This makes the project's "hover" style display correctly when hovering the favorite icon.
 	favorite->set_mouse_filter(MOUSE_FILTER_PASS);
 	favorite->connect("pressed", callable_mp(this, &ProjectList::_favorite_pressed), varray(hb));
 	favorite_box->add_child(favorite);
@@ -1396,40 +1453,65 @@ void ProjectList::create_project_item_control(int p_index) {
 	ec->set_custom_minimum_size(Size2(0, 1));
 	ec->set_mouse_filter(MOUSE_FILTER_PASS);
 	vb->add_child(ec);
-	Label *title = memnew(Label(!item.missing ? item.project_name : TTR("Missing Project")));
-	title->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
-	title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), SNAME("EditorFonts")));
-	title->add_theme_color_override("font_color", font_color);
-	title->set_clip_text(true);
-	vb->add_child(title);
-
-	HBoxContainer *path_hb = memnew(HBoxContainer);
-	path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-	vb->add_child(path_hb);
-
-	Button *show = memnew(Button);
-	// Display a folder icon if the project directory can be opened, or a "broken file" icon if it can't.
-	show->set_icon(get_theme_icon(!item.missing ? "Load" : "FileBroken", "EditorIcons"));
-	if (!item.grayed) {
-		// Don't make the icon less prominent if the parent is already grayed out.
-		show->set_modulate(Color(1, 1, 1, 0.5));
-	}
-	path_hb->add_child(show);
-
-	if (!item.missing) {
-		show->connect("pressed", callable_mp(this, &ProjectList::_show_project), varray(item.path));
-		show->set_tooltip(TTR("Show in File Manager"));
-	} else {
-		show->set_tooltip(TTR("Error: Project is missing on the filesystem."));
-	}
 
-	Label *fpath = memnew(Label(item.path));
-	fpath->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE);
-	path_hb->add_child(fpath);
-	fpath->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-	fpath->set_modulate(Color(1, 1, 1, 0.5));
-	fpath->add_theme_color_override("font_color", font_color);
-	fpath->set_clip_text(true);
+	{ // Top half, title and unsupported features labels.
+		HBoxContainer *title_hb = memnew(HBoxContainer);
+		vb->add_child(title_hb);
+
+		Label *title = memnew(Label(!item.missing ? item.project_name : TTR("Missing Project")));
+		title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+		title->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
+		title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), SNAME("EditorFonts")));
+		title->add_theme_color_override("font_color", font_color);
+		title->set_clip_text(true);
+		title_hb->add_child(title);
+
+		String unsupported_features_str = Variant(item.unsupported_features).operator String().trim_prefix("[").trim_suffix("]");
+		int length = unsupported_features_str.length();
+		if (length > 0) {
+			Label *unsupported_label = memnew(Label(unsupported_features_str));
+			unsupported_label->set_custom_minimum_size(Size2(length * 15, 10));
+			unsupported_label->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
+			unsupported_label->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), SNAME("Editor")));
+			unsupported_label->set_clip_text(true);
+			unsupported_label->set_align(Label::ALIGN_RIGHT);
+			title_hb->add_child(unsupported_label);
+			Control *spacer = memnew(Control());
+			spacer->set_custom_minimum_size(Size2(10, 10));
+			title_hb->add_child(spacer);
+		}
+	}
+
+	{ // Bottom half, containing the path and view folder button.
+		HBoxContainer *path_hb = memnew(HBoxContainer);
+		path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+		vb->add_child(path_hb);
+
+		Button *show = memnew(Button);
+		// Display a folder icon if the project directory can be opened, or a "broken file" icon if it can't.
+		show->set_icon(get_theme_icon(!item.missing ? "Load" : "FileBroken", "EditorIcons"));
+		show->set_flat(true);
+		if (!item.grayed) {
+			// Don't make the icon less prominent if the parent is already grayed out.
+			show->set_modulate(Color(1, 1, 1, 0.5));
+		}
+		path_hb->add_child(show);
+
+		if (!item.missing) {
+			show->connect("pressed", callable_mp(this, &ProjectList::_show_project), varray(item.path));
+			show->set_tooltip(TTR("Show in File Manager"));
+		} else {
+			show->set_tooltip(TTR("Error: Project is missing on the filesystem."));
+		}
+
+		Label *fpath = memnew(Label(item.path));
+		fpath->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE);
+		path_hb->add_child(fpath);
+		fpath->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+		fpath->set_modulate(Color(1, 1, 1, 0.5));
+		fpath->add_theme_color_override("font_color", font_color);
+		fpath->set_clip_text(true);
+	}
 
 	_scroll_children->add_child(hb);
 	item.control = hb;
@@ -1634,8 +1716,7 @@ int ProjectList::refresh_project(const String &dir_path) {
 	if (should_be_in_list) {
 		// Recreate it with updated info
 
-		Item item;
-		load_project_data(property_key, item, is_favourite);
+		Item item = load_project_data(property_key, is_favourite);
 
 		_projects.push_back(item);
 		create_project_item_control(_projects.size() - 1);
@@ -2114,8 +2195,12 @@ void ProjectManager::_open_selected_projects_ask() {
 	}
 
 	// Update the project settings or don't open
-	String conf = project.path.plus_file("project.godot");
-	int config_version = project.version;
+	const String conf = project.path.plus_file("project.godot");
+	const int config_version = project.version;
+	PackedStringArray unsupported_features = project.unsupported_features;
+
+	Label *ask_update_label = ask_update_settings->get_label();
+	ask_update_label->set_align(Label::ALIGN_LEFT); // Reset in case of previous center align.
 
 	// Check if the config_version property was empty or 0
 	if (config_version == 0) {
@@ -2135,6 +2220,35 @@ void ProjectManager::_open_selected_projects_ask() {
 		dialog_error->popup_centered();
 		return;
 	}
+	// Check if the project is using features not supported by this build of Godot.
+	if (!unsupported_features.is_empty()) {
+		String warning_message = "";
+		for (int i = 0; i < unsupported_features.size(); i++) {
+			String feature = unsupported_features[i];
+			if (feature == "Double Precision") {
+				warning_message += TTR("Warning: This project uses double precision floats, but this version of\nGodot uses single precision floats. Opening this project may cause data loss.\n\n");
+				unsupported_features.remove_at(i);
+				i--;
+			} else if (feature == "C#") {
+				warning_message += TTR("Warning: This project uses C#, but this build of Godot does not have\nthe Mono module. If you proceed you will not be able to use any C# scripts.\n\n");
+				unsupported_features.remove_at(i);
+				i--;
+			} else if (feature.substr(0, 3).is_numeric()) {
+				warning_message += vformat(TTR("Warning: This project was built in Godot %s.\nOpening will upgrade or downgrade the project to Godot %s.\n\n"), Variant(feature), Variant(VERSION_BRANCH));
+				unsupported_features.remove_at(i);
+				i--;
+			}
+		}
+		if (!unsupported_features.is_empty()) {
+			String unsupported_features_str = Variant(unsupported_features).operator String().trim_prefix("[").trim_suffix("]");
+			warning_message += vformat(TTR("Warning: This project uses the following features not supported by this build of Godot:\n\n%s\n\n"), unsupported_features_str);
+		}
+		warning_message += TTR("Open anyway? Project will be modified.");
+		ask_update_label->set_align(Label::ALIGN_CENTER);
+		ask_update_settings->set_text(warning_message);
+		ask_update_settings->popup_centered();
+		return;
+	}
 
 	// Open if the project is up-to-date
 	_open_selected_projects();