Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions core/config/project_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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/")) {
Expand All @@ -298,7 +305,7 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
}

_version++;
_queue_changed();
_queue_changed(p_name);
return true;
}

Expand Down Expand Up @@ -344,7 +351,7 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
}

_version++;
_queue_changed();
_queue_changed(p_name);
return true;
}

Expand Down Expand Up @@ -528,12 +535,31 @@ void ProjectSettings::_queue_changed() {
callable_mp(this, &ProjectSettings::_emit_changed).call_deferred();
}

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;
}

// 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() {
if (!is_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) {
Expand Down Expand Up @@ -1313,6 +1339,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.
Expand Down Expand Up @@ -1509,6 +1552,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"));
}

Expand Down
8 changes: 8 additions & 0 deletions core/config/project_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<StringName> changed_settings;

public:
typedef HashMap<String, Variant> CustomMap;
static inline const String PROJECT_DATA_DIR_NAME_SUFFIX = "godot";
Expand Down Expand Up @@ -119,6 +122,7 @@ class ProjectSettings : public Object {
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;
Expand Down Expand Up @@ -209,6 +213,10 @@ class ProjectSettings : public Object {

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<StringName, AutoloadInfo> &get_autoload_list() const;
void add_autoload(const AutoloadInfo &p_autoload);
void remove_autoload(const StringName &p_autoload);
Expand Down
13 changes: 13 additions & 0 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,26 @@
[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.
</description>
</method>
<method name="check_changed_settings_in_group" qualifiers="const">
<return type="bool" />
<param index="0" name="setting_prefix" type="String" />
<description>
Checks if any settings with the prefix [param setting_prefix] exist in the set of changed settings. See also [method get_changed_settings].
</description>
</method>
<method name="clear">
<return type="void" />
<param index="0" name="name" type="String" />
<description>
Clears the whole configuration (not recommended, may break things).
</description>
</method>
<method name="get_changed_settings" qualifiers="const">
<return type="PackedStringArray" />
<description>
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].
</description>
</method>
<method name="get_global_class_list">
<return type="Dictionary[]" />
<description>
Expand Down
76 changes: 76 additions & 0 deletions tests/core/config/test_project_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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