Skip to content

Commit e81c689

Browse files
committedNov 24, 2021
Project feature warning system
1 parent 3e33006 commit e81c689

File tree

4 files changed

+252
-52
lines changed

4 files changed

+252
-52
lines changed
 

‎core/config/project_settings.cpp

+78
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
#include "core/os/keyboard.h"
4242
#include "core/os/os.h"
4343
#include "core/variant/variant_parser.h"
44+
#include "core/version.h"
45+
46+
#include "modules/modules_enabled.gen.h" // For mono.
4447

4548
const String ProjectSettings::PROJECT_DATA_DIR_NAME_SUFFIX = "godot";
4649

@@ -66,6 +69,69 @@ String ProjectSettings::get_imported_files_path() const {
6669
return get_project_data_path().plus_file("imported");
6770
}
6871

72+
// Returns the features that a project must have when opened with this build of Godot.
73+
// This is used by the project manager to provide the initial_settings for config/features.
74+
const PackedStringArray ProjectSettings::get_required_features() {
75+
PackedStringArray features = PackedStringArray();
76+
features.append(VERSION_BRANCH);
77+
#ifdef REAL_T_IS_DOUBLE
78+
features.append("Double Precision");
79+
#endif
80+
return features;
81+
}
82+
83+
// Returns the features supported by this build of Godot. Includes all required features.
84+
const PackedStringArray ProjectSettings::_get_supported_features() {
85+
PackedStringArray features = get_required_features();
86+
#ifdef MODULE_MONO_ENABLED
87+
features.append("C#");
88+
#endif
89+
// Allow pinning to a specific patch number or build type by marking
90+
// them as supported. They're only used if the user adds them manually.
91+
features.append(VERSION_BRANCH "." _MKSTR(VERSION_PATCH));
92+
features.append(VERSION_FULL_CONFIG);
93+
features.append(VERSION_FULL_BUILD);
94+
// For now, assume Vulkan is always supported.
95+
// This should be removed if it's possible to build the editor without Vulkan.
96+
features.append("Vulkan Clustered");
97+
features.append("Vulkan Mobile");
98+
return features;
99+
}
100+
101+
// Returns the features that this project needs but this build of Godot lacks.
102+
const PackedStringArray ProjectSettings::get_unsupported_features(const PackedStringArray &p_project_features) {
103+
PackedStringArray unsupported_features = PackedStringArray();
104+
PackedStringArray supported_features = singleton->_get_supported_features();
105+
for (int i = 0; i < p_project_features.size(); i++) {
106+
if (!supported_features.has(p_project_features[i])) {
107+
unsupported_features.append(p_project_features[i]);
108+
}
109+
}
110+
unsupported_features.sort();
111+
return unsupported_features;
112+
}
113+
114+
// Returns the features that both this project has and this build of Godot has, ensuring required features exist.
115+
const PackedStringArray ProjectSettings::_trim_to_supported_features(const PackedStringArray &p_project_features) {
116+
// Remove unsupported features if present.
117+
PackedStringArray features = PackedStringArray(p_project_features);
118+
PackedStringArray supported_features = _get_supported_features();
119+
for (int i = p_project_features.size() - 1; i > -1; i--) {
120+
if (!supported_features.has(p_project_features[i])) {
121+
features.remove_at(i);
122+
}
123+
}
124+
// Add required features if not present.
125+
PackedStringArray required_features = get_required_features();
126+
for (int i = 0; i < required_features.size(); i++) {
127+
if (!features.has(required_features[i])) {
128+
features.append(required_features[i]);
129+
}
130+
}
131+
features.sort();
132+
return features;
133+
}
134+
69135
String ProjectSettings::localize_path(const String &p_path) const {
70136
if (resource_path.is_empty() || p_path.begins_with("res://") || p_path.begins_with("user://") ||
71137
(p_path.is_absolute_path() && !p_path.begins_with(resource_path))) {
@@ -635,6 +701,11 @@ Error ProjectSettings::_load_settings_text(const String &p_path) {
635701
} else {
636702
if (section == String()) {
637703
set(assign, value);
704+
} else if (section == "application" && assign == "config/features") {
705+
const PackedStringArray project_features_untrimmed = value;
706+
const PackedStringArray project_features = _trim_to_supported_features(project_features_untrimmed);
707+
set("application/config/features", project_features);
708+
save();
638709
} else {
639710
set(section + "/" + assign, value);
640711
}
@@ -666,6 +737,13 @@ Error ProjectSettings::_load_settings_text_or_binary(const String &p_text_path,
666737
return err;
667738
}
668739

740+
Error ProjectSettings::load_custom(const String &p_path) {
741+
if (p_path.ends_with(".binary")) {
742+
return _load_settings_binary(p_path);
743+
}
744+
return _load_settings_text(p_path);
745+
}
746+
669747
int ProjectSettings::get_order(const String &p_name) const {
670748
ERR_FAIL_COND_V_MSG(!props.has(p_name), -1, "Request for nonexistent project setting: " + p_name + ".");
671749
return props[p_name].order;

‎core/config/project_settings.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class ProjectSettings : public Object {
4848
//properties that are not for built in values begin from this value, so builtin ones are displayed first
4949
NO_BUILTIN_ORDER_BASE = 1 << 16
5050
};
51+
const static PackedStringArray get_required_features();
52+
const static PackedStringArray get_unsupported_features(const PackedStringArray &p_project_features);
5153

5254
struct AutoloadInfo {
5355
StringName name;
@@ -111,6 +113,9 @@ class ProjectSettings : public Object {
111113

112114
Error _save_custom_bnd(const String &p_file);
113115

116+
const static PackedStringArray _get_supported_features();
117+
const static PackedStringArray _trim_to_supported_features(const PackedStringArray &p_project_features);
118+
114119
void _convert_to_last_version(int p_from_version);
115120

116121
bool _load_resource_pack(const String &p_pack, bool p_replace_files = true, int p_offset = 0);
@@ -125,7 +130,7 @@ class ProjectSettings : public Object {
125130
static void _bind_methods();
126131

127132
public:
128-
static const int CONFIG_VERSION = 4;
133+
static const int CONFIG_VERSION = 5;
129134

130135
void set_setting(const String &p_setting, const Variant &p_value);
131136
Variant get_setting(const String &p_setting) const;
@@ -158,6 +163,7 @@ class ProjectSettings : public Object {
158163

159164
Error setup(const String &p_path, const String &p_main_pack, bool p_upwards = false, bool p_ignore_override = false);
160165

166+
Error load_custom(const String &p_path);
161167
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);
162168
Error save();
163169
void set_custom_property_info(const String &p_prop, const PropertyInfo &p_info);

‎core/io/config_file.cpp

+3-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,9 @@ Error ConfigFile::_internal_save(FileAccess *file) {
183183
if (E != values.front()) {
184184
file->store_string("\n");
185185
}
186-
file->store_string("[" + E.key() + "]\n\n");
186+
if (E.key() != "") {
187+
file->store_string("[" + E.key() + "]\n\n");
188+
}
187189

188190
for (OrderedHashMap<String, Variant>::Element F = E.get().front(); F; F = F.next()) {
189191
String vstr;

‎editor/project_manager.cpp

+164-50
Original file line numberDiff line numberDiff line change
@@ -478,8 +478,20 @@ class ProjectDialog : public ConfirmationDialog {
478478
cd->grab_focus();
479479
return;
480480
}
481+
PackedStringArray project_features = ProjectSettings::get_required_features();
481482
ProjectSettings::CustomMap initial_settings;
482-
initial_settings["rendering/vulkan/rendering/back_end"] = rasterizer_button_group->get_pressed_button()->get_meta(SNAME("driver_name"));
483+
// Be sure to change this code if/when renderers are changed.
484+
int renderer_type = rasterizer_button_group->get_pressed_button()->get_meta(SNAME("driver_name"));
485+
initial_settings["rendering/vulkan/rendering/back_end"] = renderer_type;
486+
if (renderer_type == 0) {
487+
project_features.push_back("Vulkan Clustered");
488+
} else if (renderer_type == 1) {
489+
project_features.push_back("Vulkan Mobile");
490+
} else {
491+
WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
492+
}
493+
project_features.sort();
494+
initial_settings["application/config/features"] = project_features;
483495
initial_settings["application/config/name"] = project_name->get_text().strip_edges();
484496
initial_settings["application/config/icon"] = "res://icon.png";
485497

@@ -1019,6 +1031,7 @@ class ProjectList : public ScrollContainer {
10191031
String path;
10201032
String icon;
10211033
String main_scene;
1034+
PackedStringArray unsupported_features;
10221035
uint64_t last_edited = 0;
10231036
bool favorite = false;
10241037
bool grayed = false;
@@ -1035,6 +1048,7 @@ class ProjectList : public ScrollContainer {
10351048
const String &p_path,
10361049
const String &p_icon,
10371050
const String &p_main_scene,
1051+
const PackedStringArray &p_unsupported_features,
10381052
uint64_t p_last_edited,
10391053
bool p_favorite,
10401054
bool p_grayed,
@@ -1046,6 +1060,7 @@ class ProjectList : public ScrollContainer {
10461060
path = p_path;
10471061
icon = p_icon;
10481062
main_scene = p_main_scene;
1063+
unsupported_features = p_unsupported_features;
10491064
last_edited = p_last_edited;
10501065
favorite = p_favorite;
10511066
grayed = p_grayed;
@@ -1097,8 +1112,7 @@ class ProjectList : public ScrollContainer {
10971112
void remove_project(int p_index, bool p_update_settings);
10981113
void update_icons_async();
10991114
void load_project_icon(int p_index);
1100-
1101-
static void load_project_data(const String &p_property_key, Item &p_item, bool p_favorite);
1115+
static Item load_project_data(const String &p_property_key, bool p_favorite);
11021116

11031117
String _search_term;
11041118
FilterOption _order_option;
@@ -1189,7 +1203,8 @@ void ProjectList::load_project_icon(int p_index) {
11891203
item.control->icon_needs_reload = false;
11901204
}
11911205

1192-
void ProjectList::load_project_data(const String &p_property_key, Item &p_item, bool p_favorite) {
1206+
// Load project data from p_property_key and return it in a ProjectList::Item. p_favorite is passed directly into the Item.
1207+
ProjectList::Item ProjectList::load_project_data(const String &p_property_key, bool p_favorite) {
11931208
String path = EditorSettings::get_singleton()->get(p_property_key);
11941209
String conf = path.plus_file("project.godot");
11951210
bool grayed = false;
@@ -1209,13 +1224,56 @@ void ProjectList::load_project_data(const String &p_property_key, Item &p_item,
12091224
}
12101225

12111226
if (config_version > ProjectSettings::CONFIG_VERSION) {
1212-
// Comes from an incompatible (more recent) Godot version, grey it out
1227+
// Comes from an incompatible (more recent) Godot version, gray it out.
12131228
grayed = true;
12141229
}
12151230

1216-
String description = cf->get_value("application", "config/description", "");
1217-
String icon = cf->get_value("application", "config/icon", "");
1218-
String main_scene = cf->get_value("application", "run/main_scene", "");
1231+
const String description = cf->get_value("application", "config/description", "");
1232+
const String icon = cf->get_value("application", "config/icon", "");
1233+
const String main_scene = cf->get_value("application", "run/main_scene", "");
1234+
1235+
PackedStringArray project_features = cf->get_value("application", "config/features", PackedStringArray());
1236+
bool project_features_dirty = false;
1237+
// If there is no feature list currently present, force one to generate.
1238+
if (project_features.is_empty()) {
1239+
project_features = ProjectSettings::get_required_features();
1240+
project_features_dirty = true;
1241+
}
1242+
// Check the rendering API.
1243+
const String rendering_api = cf->get_value("rendering", "quality/driver/driver_name", "");
1244+
if (rendering_api != "") {
1245+
// Add the rendering API as a project feature if it doesn't already exist.
1246+
if (!project_features.has(rendering_api)) {
1247+
project_features.append(rendering_api);
1248+
project_features_dirty = true;
1249+
}
1250+
}
1251+
// Check for the existence of a csproj file.
1252+
if (FileAccess::exists(path.plus_file(project_name + ".csproj"))) {
1253+
// If there is a csproj file, add the C# feature if it doesn't already exist.
1254+
if (!project_features.has("C#")) {
1255+
project_features.append("C#");
1256+
project_features_dirty = true;
1257+
}
1258+
} else {
1259+
// If there isn't a csproj file, remove the C# feature if it exists.
1260+
if (project_features.has("C#")) {
1261+
project_features.remove_at(project_features.find("C#"));
1262+
project_features_dirty = true;
1263+
}
1264+
}
1265+
if (project_features_dirty) {
1266+
project_features.sort();
1267+
// Write the updated feature list, but only if the project config version is the same.
1268+
// Never write to project files with a different config version!
1269+
if (config_version == ProjectSettings::CONFIG_VERSION) {
1270+
ProjectSettings *ps = ProjectSettings::get_singleton();
1271+
ps->load_custom(conf);
1272+
ps->set("application/config/features", project_features);
1273+
ps->save_custom(conf);
1274+
}
1275+
}
1276+
PackedStringArray unsupported_features = ProjectSettings::get_unsupported_features(project_features);
12191277

12201278
uint64_t last_edited = 0;
12211279
if (FileAccess::exists(conf)) {
@@ -1237,9 +1295,9 @@ void ProjectList::load_project_data(const String &p_property_key, Item &p_item,
12371295
print_line("Project is missing: " + conf);
12381296
}
12391297

1240-
String project_key = p_property_key.get_slice("/", 1);
1298+
const String project_key = p_property_key.get_slice("/", 1);
12411299

1242-
p_item = Item(project_key, project_name, description, path, icon, main_scene, last_edited, p_favorite, grayed, missing, config_version);
1300+
return Item(project_key, project_name, description, path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
12431301
}
12441302

12451303
void ProjectList::load_projects() {
@@ -1282,8 +1340,7 @@ void ProjectList::load_projects() {
12821340
String project_key = property_key.get_slice("/", 1);
12831341
bool favorite = favorites.has("favorite_projects/" + project_key);
12841342

1285-
Item item;
1286-
load_project_data(property_key, item, favorite);
1343+
Item item = load_project_data(property_key, favorite);
12871344

12881345
_projects.push_back(item);
12891346
}
@@ -1366,7 +1423,7 @@ void ProjectList::create_project_item_control(int p_index) {
13661423
TextureButton *favorite = memnew(TextureButton);
13671424
favorite->set_name("FavoriteButton");
13681425
favorite->set_normal_texture(favorite_icon);
1369-
// This makes the project's "hover" style display correctly when hovering the favorite icon
1426+
// This makes the project's "hover" style display correctly when hovering the favorite icon.
13701427
favorite->set_mouse_filter(MOUSE_FILTER_PASS);
13711428
favorite->connect("pressed", callable_mp(this, &ProjectList::_favorite_pressed), varray(hb));
13721429
favorite_box->add_child(favorite);
@@ -1396,40 +1453,65 @@ void ProjectList::create_project_item_control(int p_index) {
13961453
ec->set_custom_minimum_size(Size2(0, 1));
13971454
ec->set_mouse_filter(MOUSE_FILTER_PASS);
13981455
vb->add_child(ec);
1399-
Label *title = memnew(Label(!item.missing ? item.project_name : TTR("Missing Project")));
1400-
title->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
1401-
title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), SNAME("EditorFonts")));
1402-
title->add_theme_color_override("font_color", font_color);
1403-
title->set_clip_text(true);
1404-
vb->add_child(title);
1405-
1406-
HBoxContainer *path_hb = memnew(HBoxContainer);
1407-
path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1408-
vb->add_child(path_hb);
1409-
1410-
Button *show = memnew(Button);
1411-
// Display a folder icon if the project directory can be opened, or a "broken file" icon if it can't.
1412-
show->set_icon(get_theme_icon(!item.missing ? "Load" : "FileBroken", "EditorIcons"));
1413-
if (!item.grayed) {
1414-
// Don't make the icon less prominent if the parent is already grayed out.
1415-
show->set_modulate(Color(1, 1, 1, 0.5));
1416-
}
1417-
path_hb->add_child(show);
1418-
1419-
if (!item.missing) {
1420-
show->connect("pressed", callable_mp(this, &ProjectList::_show_project), varray(item.path));
1421-
show->set_tooltip(TTR("Show in File Manager"));
1422-
} else {
1423-
show->set_tooltip(TTR("Error: Project is missing on the filesystem."));
1424-
}
14251456

1426-
Label *fpath = memnew(Label(item.path));
1427-
fpath->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE);
1428-
path_hb->add_child(fpath);
1429-
fpath->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1430-
fpath->set_modulate(Color(1, 1, 1, 0.5));
1431-
fpath->add_theme_color_override("font_color", font_color);
1432-
fpath->set_clip_text(true);
1457+
{ // Top half, title and unsupported features labels.
1458+
HBoxContainer *title_hb = memnew(HBoxContainer);
1459+
vb->add_child(title_hb);
1460+
1461+
Label *title = memnew(Label(!item.missing ? item.project_name : TTR("Missing Project")));
1462+
title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1463+
title->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
1464+
title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), SNAME("EditorFonts")));
1465+
title->add_theme_color_override("font_color", font_color);
1466+
title->set_clip_text(true);
1467+
title_hb->add_child(title);
1468+
1469+
String unsupported_features_str = Variant(item.unsupported_features).operator String().trim_prefix("[").trim_suffix("]");
1470+
int length = unsupported_features_str.length();
1471+
if (length > 0) {
1472+
Label *unsupported_label = memnew(Label(unsupported_features_str));
1473+
unsupported_label->set_custom_minimum_size(Size2(length * 15, 10));
1474+
unsupported_label->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
1475+
unsupported_label->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), SNAME("Editor")));
1476+
unsupported_label->set_clip_text(true);
1477+
unsupported_label->set_align(Label::ALIGN_RIGHT);
1478+
title_hb->add_child(unsupported_label);
1479+
Control *spacer = memnew(Control());
1480+
spacer->set_custom_minimum_size(Size2(10, 10));
1481+
title_hb->add_child(spacer);
1482+
}
1483+
}
1484+
1485+
{ // Bottom half, containing the path and view folder button.
1486+
HBoxContainer *path_hb = memnew(HBoxContainer);
1487+
path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1488+
vb->add_child(path_hb);
1489+
1490+
Button *show = memnew(Button);
1491+
// Display a folder icon if the project directory can be opened, or a "broken file" icon if it can't.
1492+
show->set_icon(get_theme_icon(!item.missing ? "Load" : "FileBroken", "EditorIcons"));
1493+
show->set_flat(true);
1494+
if (!item.grayed) {
1495+
// Don't make the icon less prominent if the parent is already grayed out.
1496+
show->set_modulate(Color(1, 1, 1, 0.5));
1497+
}
1498+
path_hb->add_child(show);
1499+
1500+
if (!item.missing) {
1501+
show->connect("pressed", callable_mp(this, &ProjectList::_show_project), varray(item.path));
1502+
show->set_tooltip(TTR("Show in File Manager"));
1503+
} else {
1504+
show->set_tooltip(TTR("Error: Project is missing on the filesystem."));
1505+
}
1506+
1507+
Label *fpath = memnew(Label(item.path));
1508+
fpath->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE);
1509+
path_hb->add_child(fpath);
1510+
fpath->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1511+
fpath->set_modulate(Color(1, 1, 1, 0.5));
1512+
fpath->add_theme_color_override("font_color", font_color);
1513+
fpath->set_clip_text(true);
1514+
}
14331515

14341516
_scroll_children->add_child(hb);
14351517
item.control = hb;
@@ -1634,8 +1716,7 @@ int ProjectList::refresh_project(const String &dir_path) {
16341716
if (should_be_in_list) {
16351717
// Recreate it with updated info
16361718

1637-
Item item;
1638-
load_project_data(property_key, item, is_favourite);
1719+
Item item = load_project_data(property_key, is_favourite);
16391720

16401721
_projects.push_back(item);
16411722
create_project_item_control(_projects.size() - 1);
@@ -2114,8 +2195,12 @@ void ProjectManager::_open_selected_projects_ask() {
21142195
}
21152196

21162197
// Update the project settings or don't open
2117-
String conf = project.path.plus_file("project.godot");
2118-
int config_version = project.version;
2198+
const String conf = project.path.plus_file("project.godot");
2199+
const int config_version = project.version;
2200+
PackedStringArray unsupported_features = project.unsupported_features;
2201+
2202+
Label *ask_update_label = ask_update_settings->get_label();
2203+
ask_update_label->set_align(Label::ALIGN_LEFT); // Reset in case of previous center align.
21192204

21202205
// Check if the config_version property was empty or 0
21212206
if (config_version == 0) {
@@ -2135,6 +2220,35 @@ void ProjectManager::_open_selected_projects_ask() {
21352220
dialog_error->popup_centered();
21362221
return;
21372222
}
2223+
// Check if the project is using features not supported by this build of Godot.
2224+
if (!unsupported_features.is_empty()) {
2225+
String warning_message = "";
2226+
for (int i = 0; i < unsupported_features.size(); i++) {
2227+
String feature = unsupported_features[i];
2228+
if (feature == "Double Precision") {
2229+
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");
2230+
unsupported_features.remove_at(i);
2231+
i--;
2232+
} else if (feature == "C#") {
2233+
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");
2234+
unsupported_features.remove_at(i);
2235+
i--;
2236+
} else if (feature.substr(0, 3).is_numeric()) {
2237+
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));
2238+
unsupported_features.remove_at(i);
2239+
i--;
2240+
}
2241+
}
2242+
if (!unsupported_features.is_empty()) {
2243+
String unsupported_features_str = Variant(unsupported_features).operator String().trim_prefix("[").trim_suffix("]");
2244+
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);
2245+
}
2246+
warning_message += TTR("Open anyway? Project will be modified.");
2247+
ask_update_label->set_align(Label::ALIGN_CENTER);
2248+
ask_update_settings->set_text(warning_message);
2249+
ask_update_settings->popup_centered();
2250+
return;
2251+
}
21382252

21392253
// Open if the project is up-to-date
21402254
_open_selected_projects();

0 commit comments

Comments
 (0)
Please sign in to comment.