There's a cook determinism issue in the way UObject unversioned serialization handles UObject pointers pointing to garbage. Depending on GC timing, it can either be skipped (as a property matching CDO) or serialized explicitly. In more depth:
- The scenario I've noticed it in was a blueprint that had a construction script that destroys a component.
- When the level is loaded, the property containing component pointer is deserialized as null.
- When later the construction scripts are executed, first simple construction script creates the component (and assigns pointer to it to the property), then user construction script deletes it (and marks component as garbage, not touching the property - so property now points to a UObject pending GC).
Now, one of the two things might happen.
- If GC happens before save:
- GC destroys the component and zeros out all references (the actor's property, in this case)
- Then during serialization the unversioned serialization code finds that the property value (zero) matches CDO (zero) and marks it as 'skipped' in SerializeUnversionedProperties
- If GC does not happen before save:
- During dependency gathering, property is serialized, then FPackageHarvester calls FSaveContext::IsUnsaveable and finds that property is garbage and thus is to be skipped
- During actual serialization, SerializeUnversionedProperties sees that property is not equal to CDO (non-zero vs zero) and marks it as 'serialized'; it's also not zero, so it doesn't use the zero-mask serialization path and does a proper serialization.
- The serialization function then tries to lookup FPackageIndex corresponding to the object, doesn't find it (because FPackageHarvester skipped it) and writes out invalid-index for the data
So ultimately the serialized state is equivalent, but has different representation (`property has default value` vs `property has non-default value, is non-zero, and it's value is zero`).