From 6f0e210093dbd3f20bec34a4e60861dcceabd484 Mon Sep 17 00:00:00 2001
From: Juan Linietsky <reduzio@gmail.com>
Date: Fri, 13 Jan 2023 13:16:49 +0100
Subject: [PATCH] Refactor ProjectSetting overrides

* Overrides no longer happen for set/get.
* They must be checked with a new function: `ProjectSettings::get_setting_with_override()`.
* GLOBAL_DEF/GLOBAL_GET updated to use this

This change solves many problems:
* General confusion about getting the actual or overriden setting.
* Feature tags available after settings are loaded were being ignored, they are now considered.
* Hacks required for the Project Settings editor to work.

Fixes #64100. Fixes #64014. Fixes #61908.
---
 core/config/project_settings.cpp            | 59 ++++++++++++---------
 core/config/project_settings.h              |  9 ++--
 doc/classes/ProjectSettings.xml             | 19 +++++++
 main/main.cpp                               |  1 -
 modules/mono/csharp_script.cpp              |  2 +-
 modules/mono/mono_gd/gd_mono.cpp            |  4 +-
 modules/text_server_adv/text_server_adv.cpp |  2 +-
 modules/text_server_fb/text_server_fb.cpp   |  2 +-
 platform/web/export/export_plugin.cpp       |  4 +-
 9 files changed, 65 insertions(+), 37 deletions(-)

diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp
index 0bf7430d842..73d7d7420fd 100644
--- a/core/config/project_settings.cpp
+++ b/core/config/project_settings.cpp
@@ -291,31 +291,26 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
 			return true;
 		}
 
-		if (!disable_feature_overrides) {
+		{ // Feature overrides.
 			int dot = p_name.operator String().find(".");
 			if (dot != -1) {
 				Vector<String> s = p_name.operator String().split(".");
 
-				bool override_valid = false;
 				for (int i = 1; i < s.size(); i++) {
 					String feature = s[i].strip_edges();
-					if (OS::get_singleton()->has_feature(feature) || custom_features.has(feature)) {
-						override_valid = true;
-						break;
-					}
-				}
+					Pair<StringName, StringName> fo(feature, p_name);
 
-				if (override_valid) {
-					feature_overrides[s[0]] = p_name;
+					if (!feature_overrides.has(s[0])) {
+						feature_overrides[s[0]] = LocalVector<Pair<StringName, StringName>>();
+					}
+
+					feature_overrides[s[0]].push_back(fo);
 				}
 			}
 		}
 
 		if (props.has(p_name)) {
-			if (!props[p_name].overridden) {
-				props[p_name].variant = p_value;
-			}
-
+			props[p_name].variant = p_value;
 		} else {
 			props[p_name] = VariantContainer(p_value, last_order++);
 		}
@@ -340,18 +335,37 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
 bool ProjectSettings::_get(const StringName &p_name, Variant &r_ret) const {
 	_THREAD_SAFE_METHOD_
 
-	StringName name = p_name;
-	if (!disable_feature_overrides && feature_overrides.has(name)) {
-		name = feature_overrides[name];
-	}
-	if (!props.has(name)) {
-		WARN_PRINT("Property not found: " + String(name));
+	if (!props.has(p_name)) {
+		WARN_PRINT("Property not found: " + String(p_name));
 		return false;
 	}
-	r_ret = props[name].variant;
+	r_ret = props[p_name].variant;
 	return true;
 }
 
+Variant ProjectSettings::get_setting_with_override(const StringName &p_name) const {
+	_THREAD_SAFE_METHOD_
+
+	StringName name = p_name;
+	if (feature_overrides.has(name)) {
+		const LocalVector<Pair<StringName, StringName>> &overrides = feature_overrides[name];
+		for (uint32_t i = 0; i < overrides.size(); i++) {
+			if (OS::get_singleton()->has_feature(overrides[i].first)) { // Custom features are checked in OS.has_feature() already. No need to check twice.
+				if (props.has(overrides[i].second)) {
+					name = overrides[i].second;
+					break;
+				}
+			}
+		}
+	}
+
+	if (!props.has(name)) {
+		WARN_PRINT("Property not found: " + String(name));
+		return Variant();
+	}
+	return props[name].variant;
+}
+
 struct _VCSort {
 	String name;
 	Variant::Type type = Variant::VARIANT_MAX;
@@ -1101,10 +1115,6 @@ const HashMap<StringName, PropertyInfo> &ProjectSettings::get_custom_property_in
 	return custom_prop_info;
 }
 
-void ProjectSettings::set_disable_feature_overrides(bool p_disable) {
-	disable_feature_overrides = p_disable;
-}
-
 bool ProjectSettings::is_using_datapack() const {
 	return using_datapack;
 }
@@ -1169,6 +1179,7 @@ void ProjectSettings::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("has_setting", "name"), &ProjectSettings::has_setting);
 	ClassDB::bind_method(D_METHOD("set_setting", "name", "value"), &ProjectSettings::set_setting);
 	ClassDB::bind_method(D_METHOD("get_setting", "name", "default_value"), &ProjectSettings::get_setting, DEFVAL(Variant()));
+	ClassDB::bind_method(D_METHOD("get_setting_with_override", "name"), &ProjectSettings::get_setting_with_override);
 	ClassDB::bind_method(D_METHOD("set_order", "name", "position"), &ProjectSettings::set_order);
 	ClassDB::bind_method(D_METHOD("get_order", "name"), &ProjectSettings::get_order);
 	ClassDB::bind_method(D_METHOD("set_initial_value", "name", "value"), &ProjectSettings::set_initial_value);
diff --git a/core/config/project_settings.h b/core/config/project_settings.h
index 2aca1e76916..353e298919f 100644
--- a/core/config/project_settings.h
+++ b/core/config/project_settings.h
@@ -34,6 +34,7 @@
 #include "core/object/class_db.h"
 #include "core/os/thread_safe.h"
 #include "core/templates/hash_map.h"
+#include "core/templates/local_vector.h"
 #include "core/templates/rb_set.h"
 
 class ProjectSettings : public Object {
@@ -69,7 +70,6 @@ protected:
 		Variant variant;
 		Variant initial;
 		bool hide_from_editor = false;
-		bool overridden = false;
 		bool restart_if_changed = false;
 #ifdef DEBUG_METHODS_ENABLED
 		bool ignore_value_in_docs = false;
@@ -91,12 +91,11 @@ protected:
 	RBMap<StringName, VariantContainer> props; // NOTE: Key order is used e.g. in the save_custom method.
 	String resource_path;
 	HashMap<StringName, PropertyInfo> custom_prop_info;
-	bool disable_feature_overrides = false;
 	bool using_datapack = false;
 	List<String> input_presets;
 
 	HashSet<String> custom_features;
-	HashMap<StringName, StringName> feature_overrides;
+	HashMap<StringName, LocalVector<Pair<StringName, StringName>>> feature_overrides;
 
 	HashMap<StringName, AutoloadInfo> autoloads;
 
@@ -181,7 +180,7 @@ public:
 
 	List<String> get_input_presets() const { return input_presets; }
 
-	void set_disable_feature_overrides(bool p_disable);
+	Variant get_setting_with_override(const StringName &p_name) const;
 
 	bool is_using_datapack() const;
 
@@ -205,7 +204,7 @@ Variant _GLOBAL_DEF(const PropertyInfo &p_info, const Variant &p_default, bool p
 #define GLOBAL_DEF_RST(m_var, m_value) _GLOBAL_DEF(m_var, m_value, true)
 #define GLOBAL_DEF_NOVAL(m_var, m_value) _GLOBAL_DEF(m_var, m_value, false, true)
 #define GLOBAL_DEF_RST_NOVAL(m_var, m_value) _GLOBAL_DEF(m_var, m_value, true, true)
-#define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get(m_var)
+#define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get_setting_with_override(m_var)
 
 #define GLOBAL_DEF_BASIC(m_var, m_value) _GLOBAL_DEF(m_var, m_value, false, false, true)
 #define GLOBAL_DEF_RST_BASIC(m_var, m_value) _GLOBAL_DEF(m_var, m_value, true, false, true)
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index cfcfca98808..f954e8bca8f 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -84,6 +84,25 @@
 				GD.Print(ProjectSettings.GetSetting("application/config/custom_description", "No description specified."));
 				[/csharp]
 				[/codeblocks]
+				[b]Note:[/b] This method doesn't take potential feature overrides into account automatically. Use [method get_setting_with_override] to handle seamlessly.
+			</description>
+		</method>
+		<method name="get_setting_with_override" qualifiers="const">
+			<return type="Variant" />
+			<param index="0" name="name" type="StringName" />
+			<description>
+				Similar to [method get_setting], but applies feature tag overrides if any exists and is valid.
+				[b]Example:[/b]
+				If the following setting override exists "application/config/name.windows", and the following code is executed:
+				[codeblocks]
+				[gdscript]
+				print(ProjectSettings.get_setting_with_override("application/config/name"))
+				[/gdscript]
+				[csharp]
+				GD.Print(ProjectSettings.GetSettingWithOverride("application/config/name"));
+				[/csharp]
+				[/codeblocks]
+				Then the overridden setting will be returned instead if the project is running on the [i]Windows[/i] operating system.
 			</description>
 		</method>
 		<method name="globalize_path" qualifiers="const">
diff --git a/main/main.cpp b/main/main.cpp
index e38669f1618..7ba5ffab2a6 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -1385,7 +1385,6 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
 #ifdef TOOLS_ENABLED
 	if (editor) {
 		packed_data->set_disabled(true);
-		globals->set_disable_feature_overrides(true);
 		Engine::get_singleton()->set_editor_hint(true);
 		main_args.push_back("--editor");
 		if (!init_windowed) {
diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp
index c45fdda75cf..e39127e0a14 100644
--- a/modules/mono/csharp_script.cpp
+++ b/modules/mono/csharp_script.cpp
@@ -688,7 +688,7 @@ bool CSharpLanguage::is_assembly_reloading_needed() {
 			return false; // Already up to date
 		}
 	} else {
-		String assembly_name = ProjectSettings::get_singleton()->get_setting("dotnet/project/assembly_name");
+		String assembly_name = GLOBAL_GET("dotnet/project/assembly_name");
 
 		if (assembly_name.is_empty()) {
 			assembly_name = ProjectSettings::get_singleton()->get_safe_project_name();
diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp
index 6664dfb2dd9..86b0e042069 100644
--- a/modules/mono/mono_gd/gd_mono.cpp
+++ b/modules/mono/mono_gd/gd_mono.cpp
@@ -289,7 +289,7 @@ godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime
 }
 #else
 static String get_assembly_name() {
-	String assembly_name = ProjectSettings::get_singleton()->get_setting("dotnet/project/assembly_name");
+	String assembly_name = GLOBAL_GET("dotnet/project/assembly_name");
 
 	if (assembly_name.is_empty()) {
 		assembly_name = ProjectSettings::get_singleton()->get_safe_project_name();
@@ -466,7 +466,7 @@ void GDMono::_init_godot_api_hashes() {
 
 #ifdef TOOLS_ENABLED
 bool GDMono::_load_project_assembly() {
-	String assembly_name = ProjectSettings::get_singleton()->get_setting("dotnet/project/assembly_name");
+	String assembly_name = GLOBAL_GET("dotnet/project/assembly_name");
 
 	if (assembly_name.is_empty()) {
 		assembly_name = ProjectSettings::get_singleton()->get_safe_project_name();
diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp
index 44a2c767275..e52b87741e4 100644
--- a/modules/text_server_adv/text_server_adv.cpp
+++ b/modules/text_server_adv/text_server_adv.cpp
@@ -42,7 +42,7 @@
 
 using namespace godot;
 
-#define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get(m_var)
+#define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get_setting_with_override(m_var)
 
 #else
 // Headers for building as built-in module.
diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp
index 809bfbe41a2..034f88e3879 100644
--- a/modules/text_server_fb/text_server_fb.cpp
+++ b/modules/text_server_fb/text_server_fb.cpp
@@ -42,7 +42,7 @@
 
 using namespace godot;
 
-#define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get(m_var)
+#define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get_setting_with_override(m_var)
 
 #else
 // Headers for building as built-in module.
diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp
index 3e11db68874..05959c4fd4d 100644
--- a/platform/web/export/export_plugin.cpp
+++ b/platform/web/export/export_plugin.cpp
@@ -153,7 +153,7 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito
 	const String custom_head_include = p_preset->get("html/head_include");
 	HashMap<String, String> replaces;
 	replaces["$GODOT_URL"] = p_name + ".js";
-	replaces["$GODOT_PROJECT_NAME"] = ProjectSettings::get_singleton()->get_setting("application/config/name");
+	replaces["$GODOT_PROJECT_NAME"] = GLOBAL_GET("application/config/name");
 	replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;
 	replaces["$GODOT_CONFIG"] = str_config;
 	_replace_strings(replaces, p_html);
@@ -193,7 +193,7 @@ Error EditorExportPlatformWeb::_add_manifest_icon(const String &p_path, const St
 }
 
 Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects) {
-	String proj_name = ProjectSettings::get_singleton()->get_setting("application/config/name");
+	String proj_name = GLOBAL_GET("application/config/name");
 	if (proj_name.is_empty()) {
 		proj_name = "Godot Game";
 	}