Ok, so I’ve figured it out… Here is the story:
UObject::Serialize calls operator<<(FArchive, FName) to serialize property names, and uses FName “None” as a “null-terminator” to indicate the end of the property list. That’s why in the correctly serialized file above “None” appears at the end of the property list to indicate it’s end. The property list is followed by an optional GUID which, when empty, is a 32-bit bool all zeros. That’s why there is 4 bytes of zeros after the None property in the corretly serialized version, to indicate no object GUID is present.
The reason that it doesn’t work with FArchive is that the default implementation of operator<<(FArchive, FName) is a no-op:
virtual FArchive& FArchive::operator<<(FName& Value)
{
return *this;
}
consequently, when Saving, FArchive is told the property names and goes “yep got it”, and then places nothing in the file for the property names.
And now the fun part: Then later when Loading from FArchive, UObject::Serialize goes to load the first property and asks to deserialize the FName of the first property, passing in a default-constructed FName which is also the value of “None” (0). FArchive doesn’t touch the FName and returns (again, its a no-op - same operator<< function shown above). UObject::Serialize then takes this to mean it has read FName None from the archive indicating the end of the property list.
Thinking it has successfully read the (seemingly empty) property list, UObject::Serialize then goes onto read the optional GUID bool (with the file position at the start of the file), and it gets something other than 0 or 1. (It gets 10 which is the value of the first four bytes of the file little-endian: 0x0A) and then croaks with “Invalid boolean encountered while reading archive” in the Guid reading code.
The solution is the use FNameAsStringProxyArchive which is basically a tiny wrapper around FArchive that implements operator<<(FName) Rather than a no-op it writes it out a string:
FArchive& FNameAsStringProxyArchive::operator<<( class FName& N )
{
if (IsLoading())
{
FString LoadedString;
InnerArchive << LoadedString;
N = FName(*LoadedString);
}
else
{
FString SavedString(N.ToString());
InnerArchive << SavedString;
}
return *this;
}
I consider it a bug that UObject::Serialize is interpretting the default no-op implementation of FArchive::operator<<(FName) returning None indicating the end-of-the-property list and continuing on. They should be distinct paths, and it should provoke a proper error message.