Repository: godotengine/godot (C++)

Issue:
Animation's data in *.tscn file always change keys order in Dictionary
### Tested versions

- Reproducible in v4.4.dev2.official [97ef3c837]

### System information

Windows 11, Vulkan API 1.3.205 - Forward+ - Using Vulkan Device #0: NVIDIA - NVIDIA GeForce RTX 3060 Ti

### Issue description

When save (without any modification) a scene which contains AnimationPlayers, .tscn file always change for the StringName keys of Dictionary has changed. It's not friendly to version control.

<img width="1223" alt="屏幕截图 2024-09-27 112244" src="https://github.com/user-attachments/assets/641f56ae-f1f1-42ce-a5b6-8b80ed201fa8">


### Steps to reproduce

create a scene contains a AnimationPlayer, and make some animation. Save the scene and open the saved tscn file with other txt editor and remember the data( dictionary )'s keys order. Then back to the engine and save the scene again, then recheck the data's keys order.

### Minimal reproduction project (MRP)

NA


Discussion: Update: not only the data of AnimationLibrary changes keys order, the order between several Animation also changes. It may due to the keys order has changed in AnimationLibrary's data.

<img width="1233" alt="屏幕截图 2024-09-27 115429" src="https://github.com/user-attachments/assets/54cc4837-a903-4868-acd4-52bc284d0e47">

Haven't tested in older versions but I suspect this is a regression from:
* https://github.com/godotengine/godot/pull/70096

Similar to:
* https://github.com/godotengine/godot/issues/96889

Can you test with dev1?
dev1 is good
Then this is likely from that PR, and due to that `StringName` are sorted by pointers, so special treatment needs to be done when serialising this, might need some changes to the core feature

CC @rune-scape 
Thinking more on this I think this is indeed more serious, and the solution might be to change the serialisation of Dictionaries instead of adding workarounds 
I find it's pretty hard to reproduce this issue by a simple MRP with dev2, maybe due to the pointers' creation time are unsure in complex circumstances. Anyway, if  "StringName are sorted by pointers"(in Dictionary?) is true, then it is surely a problem.
I was about to report the same bug.
Made MRP:
[animationshuffle.zip](https://github.com/user-attachments/files/17164473/animationshuffle.zip)
Look at the order of animations every time the scene is reopened and saved.
* Likely related: #58878.

In `.tscn`/`.tres` it's here:

https://github.com/godotengine/godot/blob/76a135926aef1f02f27e4e09093787f2c670956d/core/variant/variant_parser.cpp#L2246-L2248
I'll write up a fix for the sorting to make this workable
See also:

* #96915.

https://github.com/godotengine/godot/blob/76a135926aef1f02f27e4e09093787f2c670956d/core/io/json.cpp#L123-L125

https://github.com/godotengine/godot/blob/76a135926aef1f02f27e4e09093787f2c670956d/core/io/packed_data_container.cpp#L282
Will see about a unified fix and will add a helper class, starting in the variant parser code to save it and looking at the remaining cases as well, will initially keep the sorting of `StringName` and `String` apart as opposed to how things were done in #96915 but can change either that approach or change the code in that PR to use the same solution depending on opinions

Will push soon

Edit: Since string keys and StringName keys are unified I'll merge the sorting, ~and will replace the code in multiplayer with a centralized implementation~ It uses `likely` because of its situation, so will leave it alone and make the centralized implementation not optimize in that way



Fix (patch):
diff --git a/core/io/json.cpp b/core/io/json.cpp
index 664ff7857b6b..22219fca295a 100644
--- a/core/io/json.cpp
+++ b/core/io/json.cpp
@@ -121,7 +121,7 @@ String JSON::_stringify(const Variant &p_var, const String &p_indent, int p_cur_
 			d.get_key_list(&keys);
 
 			if (p_sort_keys) {
-				keys.sort();
+				keys.sort_custom<StringLikeVariantOrder>();
 			}
 
 			bool first_key = true;
diff --git a/core/variant/variant.h b/core/variant/variant.h
index c76b849abdb7..3b1924e8eabb 100644
--- a/core/variant/variant.h
+++ b/core/variant/variant.h
@@ -854,6 +854,19 @@ struct StringLikeVariantComparator {
 	static bool compare(const Variant &p_lhs, const Variant &p_rhs);
 };
 
+struct StringLikeVariantOrder {
+	static _ALWAYS_INLINE_ bool compare(const Variant &p_lhs, const Variant &p_rhs) {
+		if (p_lhs.is_string() && p_rhs.is_string()) {
+			return p_lhs.operator String() < p_rhs.operator String();
+		}
+		return p_lhs < p_rhs;
+	}
+
+	_ALWAYS_INLINE_ bool operator()(const Variant &p_lhs, const Variant &p_rhs) const {
+		return compare(p_lhs, p_rhs);
+	}
+};
+
 Variant::ObjData &Variant::_get_obj() {
 	return *reinterpret_cast<ObjData *>(&_data._mem[0]);
 }
diff --git a/core/variant/variant_parser.cpp b/core/variant/variant_parser.cpp
index f5f96456d327..f05b9cd83aba 100644
--- a/core/variant/variant_parser.cpp
+++ b/core/variant/variant_parser.cpp
@@ -2245,7 +2245,7 @@ Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_str
 			} else {
 				List<Variant> keys;
 				dict.get_key_list(&keys);
-				keys.sort();
+				keys.sort_custom<StringLikeVariantOrder>();
 
 				if (keys.is_empty()) {
 					// Avoid unnecessary line break.
diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp
index ede8351e413e..a5df9edcf027 100644
--- a/editor/plugins/visual_shader_editor_plugin.cpp
+++ b/editor/plugins/visual_shader_editor_plugin.cpp
@@ -2128,12 +2128,11 @@ void VisualShaderEditor::_update_nodes() {
 		}
 	}
 
-	Array keys = added.keys();
-	keys.sort();
-
-	for (int i = 0; i < keys.size(); i++) {
-		const Variant &key = keys.get(i);
+	List<Variant> keys;
+	added.get_key_list(&keys);
+	keys.sort_custom<StringLikeVariantOrder>();
 
+	for (const Variant &key : keys) {
 		const Dictionary &value = (Dictionary)added[key];
 
 		add_custom_type(value["name"], value["type"], value["script"], value["description"], value["return_icon_type"], value["category"], value["highend"]);
diff --git a/modules/gdscript/editor/gdscript_docgen.cpp b/modules/gdscript/editor/gdscript_docgen.cpp
index 32ef429b0d47..758887a7236e 100644
--- a/modules/gdscript/editor/gdscript_docgen.cpp
+++ b/modules/gdscript/editor/gdscript_docgen.cpp
@@ -217,7 +217,7 @@ String GDScriptDocGen::_docvalue_from_variant(const Variant &p_variant, int p_re
 
 				List<Variant> keys;
 				dict.get_key_list(&keys);
-				keys.sort();
+				keys.sort_custom<StringLikeVariantOrder>();
 
 				for (List<Variant>::Element *E = keys.front(); E; E = E->next()) {
 					if (E->prev()) {