From e84b5410ec95f0630782ac3626d2511fbea26185 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 22:59:35 +0100 Subject: [PATCH] Fix C# Json serialization to use proper value comparison for structures with Scene Object references #3499 --- Source/Engine/Level/MeshReference.cs | 2 +- Source/Engine/Level/Prefabs/Prefab.Apply.cpp | 4 +-- Source/Engine/Serialization/JsonSerializer.cs | 34 ++++++++++++++++--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Level/MeshReference.cs b/Source/Engine/Level/MeshReference.cs index 14ef0c72b..cb87e9769 100644 --- a/Source/Engine/Level/MeshReference.cs +++ b/Source/Engine/Level/MeshReference.cs @@ -13,7 +13,7 @@ namespace FlaxEngine public bool ValueEquals(object other) { var o = (MeshReference)other; - return JsonSerializer.ValueEquals(Actor, o.Actor) && + return JsonSerializer.SceneObjectEquals(Actor, o.Actor) && LODIndex == o.LODIndex && MeshIndex == o.MeshIndex; } diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index 0bed02f0b..378b706ed 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -227,9 +227,9 @@ public: void PrefabInstanceData::CollectPrefabInstances(PrefabInstancesData& prefabInstancesData, const Guid& prefabId, Actor* defaultInstance, Actor* targetActor) { ScopeLock lock(PrefabManager::PrefabsReferencesLocker); - if (PrefabManager::PrefabsReferences.ContainsKey(prefabId)) + if (auto instancesPtr = PrefabManager::PrefabsReferences.TryGet(prefabId)) { - auto& instances = PrefabManager::PrefabsReferences[prefabId]; + auto& instances = *instancesPtr; int32 usedCount = 0; for (int32 instanceIndex = 0; instanceIndex < instances.Count(); instanceIndex++) { diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index 4321ffa36..86e4ead8b 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -270,8 +270,8 @@ namespace FlaxEngine.Json // Special case when saving reference to prefab object and the objects are different but the point to the same prefab object // In that case, skip saving reference as it's defined in prefab (will be populated via IdsMapping during deserialization) - if (objA is SceneObject sceneA && objB is SceneObject sceneB && sceneA && sceneB && sceneA.HasPrefabLink && sceneB.HasPrefabLink) - return sceneA.PrefabObjectID == sceneB.PrefabObjectID; + if (objA is SceneObject sceneObjA && objB is SceneObject sceneObjB && sceneObjA && sceneObjB && sceneObjA.HasPrefabLink && sceneObjB.HasPrefabLink) + return sceneObjA.PrefabObjectID == sceneObjB.PrefabObjectID; // Comparing an Int32 and Int64 both of the same value returns false, make types the same then compare if (objA.GetType() != objB.GetType()) @@ -286,7 +286,6 @@ namespace FlaxEngine.Json type == typeof(Int32) || type == typeof(UInt32) || type == typeof(Int64) || - type == typeof(SByte) || type == typeof(UInt64); } if (IsInteger(objA) && IsInteger(objB)) @@ -301,6 +300,12 @@ namespace FlaxEngine.Json { if (aList.Count != bList.Count) return false; + for (int i = 0; i < aList.Count; i++) + { + if (!ValueEquals(aList[i], bList[i])) + return false; + } + return true; } if (objA is IEnumerable aEnumerable && objB is IEnumerable bEnumerable) { @@ -316,8 +321,29 @@ namespace FlaxEngine.Json return !bEnumerator.MoveNext(); } - if (objA is ICustomValueEquals customValueEquals && objA.GetType() == objB.GetType()) + // Custom comparer + if (objA is ICustomValueEquals customValueEquals) return customValueEquals.ValueEquals(objB); + + // If type contains SceneObject references then it needs to use custom comparision that handles prefab links (see SceneObjectEquals) + if (objA.GetType().IsStructure()) + { + var contract = Settings.ContractResolver.ResolveContract(objA.GetType()); + if (contract is JsonObjectContract objContract) + { + foreach (var property in objContract.Properties) + { + var valueProvider = property.ValueProvider; + var propA = valueProvider.GetValue(objA); + var propB = valueProvider.GetValue(objB); + if (!ValueEquals(propA, propB)) + return false; + } + return true; + } + } + + // Generic fallback return objA.Equals(objB); #endif }