From ffa2651fbc9245548afb1d023ee425be4a042f86 Mon Sep 17 00:00:00 2001 From: Maurice Butler Date: Sun, 21 Sep 2025 16:44:41 +1000 Subject: [PATCH] Added ability to get a list of project settings changed. --- core/config/project_settings.cpp | 50 +++++++++++++-- core/config/project_settings.h | 9 ++- doc/classes/ProjectSettings.xml | 13 ++++ tests/core/config/test_project_settings.h | 76 +++++++++++++++++++++++ 4 files changed, 141 insertions(+), 7 deletions(-) diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index e9532231c6c..1d49799bc04 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -277,6 +277,13 @@ String ProjectSettings::globalize_path(const String &p_path) const { bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) { _THREAD_SAFE_METHOD_ + // Early return if value hasn't changed (unless it's being deleted) + if (p_value.get_type() != Variant::NIL) { + if (props.has(p_name) && props[p_name].variant == p_value) { + return true; + } + } + if (p_value.get_type() == Variant::NIL) { props.erase(p_name); if (p_name.operator String().begins_with("autoload/")) { @@ -298,7 +305,7 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) { } _version++; - _queue_changed(); + _queue_changed(p_name); return true; } @@ -344,7 +351,7 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) { } _version++; - _queue_changed(); + _queue_changed(p_name); return true; } @@ -520,12 +527,18 @@ void ProjectSettings::_get_property_list(List *p_list) const { } } -void ProjectSettings::_queue_changed() { - if (is_changed || !MessageQueue::get_singleton() || MessageQueue::get_singleton()->get_max_buffer_usage() == 0) { +void ProjectSettings::_queue_changed(const StringName &p_name) { + changed_settings.insert(p_name); + + if (!MessageQueue::get_singleton() || MessageQueue::get_singleton()->get_max_buffer_usage() == 0) { return; } - is_changed = true; - callable_mp(this, &ProjectSettings::_emit_changed).call_deferred(); + + // Only queue the deferred call once per frame. + if (!is_changed) { + is_changed = true; + callable_mp(this, &ProjectSettings::_emit_changed).call_deferred(); + } } void ProjectSettings::_emit_changed() { @@ -533,7 +546,12 @@ void ProjectSettings::_emit_changed() { return; } is_changed = false; + + // Emit the general settings_changed signal to indicate changes are complete. emit_signal("settings_changed"); + + // Clear the changed settings after emitting the signal + changed_settings.clear(); } bool ProjectSettings::load_resource_pack(const String &p_pack, bool p_replace_files, int p_offset) { @@ -1313,6 +1331,23 @@ Variant ProjectSettings::get_setting(const String &p_setting, const Variant &p_d } } +PackedStringArray ProjectSettings::get_changed_settings() const { + PackedStringArray arr; + for (const StringName &setting : changed_settings) { + arr.push_back(setting); + } + return arr; +} + +bool ProjectSettings::check_changed_settings_in_group(const String &p_setting_prefix) const { + for (const StringName &setting : changed_settings) { + if (String(setting).begins_with(p_setting_prefix)) { + return true; + } + } + return false; +} + void ProjectSettings::refresh_global_class_list() { // This is called after mounting a new PCK file to pick up class changes. is_global_class_list_loaded = false; // Make sure we read from the freshly mounted PCK. @@ -1509,6 +1544,9 @@ void ProjectSettings::_bind_methods() { ClassDB::bind_method(D_METHOD("save_custom", "file"), &ProjectSettings::_save_custom_bnd); + // Change tracking methods + ClassDB::bind_method(D_METHOD("get_changed_settings"), &ProjectSettings::get_changed_settings); + ClassDB::bind_method(D_METHOD("check_changed_settings_in_group", "setting_prefix"), &ProjectSettings::check_changed_settings_in_group); ADD_SIGNAL(MethodInfo("settings_changed")); } diff --git a/core/config/project_settings.h b/core/config/project_settings.h index 01228a914b1..54e7c3bd4bf 100644 --- a/core/config/project_settings.h +++ b/core/config/project_settings.h @@ -46,6 +46,9 @@ class ProjectSettings : public Object { // and will always detect the initial project settings as a "change". uint32_t _version = 1; + // Track changed settings for get_changed_settings functionality + HashSet changed_settings; + public: typedef HashMap CustomMap; static inline const String PROJECT_DATA_DIR_NAME_SUFFIX = "godot"; @@ -118,7 +121,7 @@ protected: bool _property_can_revert(const StringName &p_name) const; bool _property_get_revert(const StringName &p_name, Variant &r_property) const; - void _queue_changed(); + void _queue_changed(const StringName &p_name); void _emit_changed(); static inline ProjectSettings *singleton = nullptr; @@ -209,6 +212,10 @@ public: bool has_custom_feature(const String &p_feature) const; + // Change tracking methods + PackedStringArray get_changed_settings() const; + bool check_changed_settings_in_group(const String &p_setting_prefix) const; + const HashMap &get_autoload_list() const; void add_autoload(const AutoloadInfo &p_autoload); void remove_autoload(const StringName &p_autoload); diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 3517967d680..49cb292bdf8 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -54,6 +54,13 @@ [b]Note:[/b] Setting [code]"usage"[/code] for the property is not supported. Use [method set_as_basic], [method set_restart_if_changed], and [method set_as_internal] to modify usage flags. + + + + + Checks if any settings with the prefix [param setting_prefix] exist in the set of changed settings. See also [method get_changed_settings]. + + @@ -61,6 +68,12 @@ Clears the whole configuration (not recommended, may break things). + + + + Gets an array of the settings which have been changed since the last save. Note that internally [code]changed_settings[/code] is cleared after a successful save, so generally the most appropriate place to use this method is when processing [signal settings_changed]. + + diff --git a/tests/core/config/test_project_settings.h b/tests/core/config/test_project_settings.h index 758741b404d..b79d38da135 100644 --- a/tests/core/config/test_project_settings.h +++ b/tests/core/config/test_project_settings.h @@ -155,4 +155,80 @@ TEST_CASE("[ProjectSettings] localize_path") { TestProjectSettingsInternalsAccessor::resource_path() = old_resource_path; } +TEST_CASE("[SceneTree][ProjectSettings] settings_changed signal") { + SIGNAL_WATCH(ProjectSettings::get_singleton(), SNAME("settings_changed")); + + ProjectSettings::get_singleton()->set_setting("test_signal_setting", "test_value"); + MessageQueue::get_singleton()->flush(); + + SIGNAL_CHECK("settings_changed", { {} }); + + SIGNAL_UNWATCH(ProjectSettings::get_singleton(), SNAME("settings_changed")); +} + +TEST_CASE("[ProjectSettings] get_changed_settings basic functionality") { + String setting_name = "test_changed_setting"; + ProjectSettings::get_singleton()->set_setting(setting_name, "test_value"); + + PackedStringArray changes = ProjectSettings::get_singleton()->get_changed_settings(); + CHECK(changes.has(setting_name)); +} + +TEST_CASE("[ProjectSettings] get_changed_settings multiple settings") { + ProjectSettings::get_singleton()->set_setting("test_setting_1", "value1"); + ProjectSettings::get_singleton()->set_setting("test_setting_2", "value2"); + ProjectSettings::get_singleton()->set_setting("another_group/setting", "value3"); + + PackedStringArray changes = ProjectSettings::get_singleton()->get_changed_settings(); + CHECK(changes.has("test_setting_1")); + CHECK(changes.has("test_setting_2")); + CHECK(changes.has("another_group/setting")); +} + +TEST_CASE("[ProjectSettings] check_changed_settings_in_group") { + ProjectSettings::get_singleton()->set_setting("group1/setting1", "value1"); + ProjectSettings::get_singleton()->set_setting("group1/setting2", "value2"); + ProjectSettings::get_singleton()->set_setting("group2/setting1", "value3"); + ProjectSettings::get_singleton()->set_setting("other_setting", "value4"); + + CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("group1/")); + CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("group2/")); + CHECK_FALSE(ProjectSettings::get_singleton()->check_changed_settings_in_group("nonexistent/")); + + CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("group1")); + CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("other_setting")); +} + +TEST_CASE("[SceneTree][ProjectSettings] Changes cleared after settings_changed signal") { + SIGNAL_WATCH(ProjectSettings::get_singleton(), SNAME("settings_changed")); + + ProjectSettings::get_singleton()->set_setting("signal_clear_test", "value"); + + PackedStringArray changes_before = ProjectSettings::get_singleton()->get_changed_settings(); + CHECK(changes_before.has("signal_clear_test")); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_CHECK("settings_changed", { {} }); + + PackedStringArray changes_after = ProjectSettings::get_singleton()->get_changed_settings(); + CHECK_FALSE(changes_after.has("signal_clear_test")); + + SIGNAL_UNWATCH(ProjectSettings::get_singleton(), SNAME("settings_changed")); +} + +TEST_CASE("[ProjectSettings] No tracking when setting same value") { + String setting_name = "same_value_test"; + String test_value = "same_value"; + + ProjectSettings::get_singleton()->set_setting(setting_name, test_value); + int count_before = ProjectSettings::get_singleton()->get_changed_settings().size(); + + // Setting the same value should not be tracked due to early return. + ProjectSettings::get_singleton()->set_setting(setting_name, test_value); + int count_after = ProjectSettings::get_singleton()->get_changed_settings().size(); + + CHECK_EQ(count_before, count_after); +} + } // namespace TestProjectSettings