981 lines
30 KiB
C++
981 lines
30 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "JsonStringifyImpl.h"
|
|
#include "PrettyJsonWriter.h"
|
|
#include "Algo/Copy.h"
|
|
#include "Algo/ForEach.h"
|
|
#include "Algo/RemoveIf.h"
|
|
#include "Misc/App.h"
|
|
#include "JsonObjectGraphConventions.h"
|
|
#include "JsonStringifyArchive.h"
|
|
#include "JsonStringifyStructuredArchive.h"
|
|
#include "Serialization/StructuredArchive.h"
|
|
#include "Serialization/StructuredArchiveSlots.h"
|
|
#include "Serialization/Formatters/JsonArchiveOutputFormatter.h"
|
|
#include "Serialization/ArchiveUObjectFromStructuredArchive.h"
|
|
#include "StructUtils/InstancedStruct.h"
|
|
#include "StructUtils/PropertyBag.h"
|
|
#include "UObject/EnumProperty.h"
|
|
#include "UObject/PropertyOptional.h"
|
|
#include "UObject/Package.h"
|
|
#include "UObject/TextProperty.h"
|
|
|
|
namespace UE::Private
|
|
{
|
|
|
|
// gathering utils
|
|
struct FPackageReferenceFinder : public FArchiveUObject
|
|
{
|
|
FPackageReferenceFinder(const UObject* Obj, TArray<const UObject*>& InReferences, bool bFilterEditorOnly)
|
|
: FArchiveUObject()
|
|
, References(InReferences)
|
|
{
|
|
// Copying FPackageHarvester:
|
|
SetIsPersistent(true);
|
|
SetIsSaving(true);
|
|
SetFilterEditorOnly(bFilterEditorOnly);
|
|
ArNoDelta = true;
|
|
ArIsObjectReferenceCollector = true;
|
|
ArShouldSkipBulkData = true;
|
|
if (Obj->HasAnyFlags(RF_ClassDefaultObject))
|
|
{
|
|
Obj->GetClass()->SerializeBin(*this, (UObject*)Obj);
|
|
}
|
|
else
|
|
{
|
|
((UObject*)Obj)->Serialize(*this);
|
|
}
|
|
}
|
|
|
|
private:
|
|
virtual FArchive& operator<<(UObject*& ObjRef) override
|
|
{
|
|
if (ObjRef != nullptr &&
|
|
(!ObjRef->HasAnyFlags(RF_Transient) || ObjRef->IsNative()) &&
|
|
!ObjRef->IsA<UPackage>())
|
|
{
|
|
// Set to null any pointer to an external asset
|
|
References.Add(ObjRef);
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
TArray<const UObject*>& References;
|
|
};
|
|
|
|
static const UClass* GetFirstNativeClass(const UClass* Class)
|
|
{
|
|
const UClass* Iter = Class;
|
|
while (Iter && !Iter->HasAnyClassFlags(CLASS_Native))
|
|
{
|
|
Iter = Iter->GetSuperClass();
|
|
}
|
|
return Iter;
|
|
}
|
|
|
|
static void FilterEditorOnlyObjects(
|
|
TArray<UObject*>& Objects
|
|
)
|
|
{
|
|
#if WITH_EDITOR
|
|
Objects.SetNum(Algo::StableRemoveIf(Objects, [](const UObject* Obj) {
|
|
const UClass* NativeClass = GetFirstNativeClass(Obj->GetClass());
|
|
return IsEditorOnlyObject(NativeClass) || !(NativeClass->GetDefaultObject(false)->NeedsLoadForClient() || NativeClass->GetDefaultObject(false)->NeedsLoadForServer());
|
|
}));
|
|
#endif
|
|
}
|
|
|
|
static void GatherExports(
|
|
TConstArrayView<const UObject*> Roots,
|
|
TConstArrayView<const UObject*> DisallowList,
|
|
bool bFilterEditorOnly,
|
|
TArray<const UObject*>& OutRoots,
|
|
TArray<const UObject*>& OutExports)
|
|
{
|
|
// find everything roots references that is within roots and put it into
|
|
// OutExports, unless it is in the disallow list:
|
|
|
|
TSet<const UObject*> DisallowSet;
|
|
DisallowSet.Append(DisallowList);
|
|
for (const UObject* Obj : DisallowList)
|
|
{
|
|
// const hacks and GetObjects expressiveness junk
|
|
TArray<UObject*> DisallowedSubObjects;
|
|
GetObjectsWithOuter(Obj, DisallowedSubObjects, true, RF_Transient);
|
|
for (UObject* DisallowedObj : DisallowedSubObjects)
|
|
{
|
|
DisallowSet.Add(DisallowedObj);
|
|
}
|
|
}
|
|
TSet<const UObject*> AllowedRoots;
|
|
AllowedRoots.Append(Roots);
|
|
for (const UObject* Obj : Roots)
|
|
{
|
|
if (Obj && !Obj->IsA<UPackage>())
|
|
{
|
|
OutRoots.Add(Obj);
|
|
}
|
|
}
|
|
|
|
TArray<const UObject*> PendingRefs;
|
|
Algo::CopyIf(Roots, PendingRefs, [](const UObject* Obj) { return IsValid(Obj); });
|
|
|
|
TSet<const UObject*> RefsProcessed;
|
|
RefsProcessed.Append(PendingRefs);
|
|
|
|
TArray<const UObject*> ScratchRefs; // just keeping this allocation alive across iterations
|
|
while (PendingRefs.Num())
|
|
{
|
|
const UObject* Iter = PendingRefs.Pop();
|
|
if (AllowedRoots.Contains(Iter) && Iter->IsA<UPackage>())
|
|
{
|
|
// add all immediate roots of a package, if filter 'editor only' objects
|
|
// exclude any object that are of an editor only native type:
|
|
TArray<UObject*> PackageInners;
|
|
GetObjectsWithOuter(Iter, PackageInners, false, RF_Transient);
|
|
if (bFilterEditorOnly)
|
|
{
|
|
FilterEditorOnlyObjects(PackageInners);
|
|
}
|
|
PackageInners.Sort([](const UObject& A, const UObject& B) { return A.GetFName().LexicalLess(B.GetFName()); });
|
|
OutRoots.Append(PackageInners);
|
|
PendingRefs.Append(PackageInners);
|
|
continue;
|
|
}
|
|
|
|
OutExports.Add(Iter);
|
|
|
|
ScratchRefs.Add(Iter->GetClass());
|
|
FPackageReferenceFinder ReferencedObjects(Iter, ScratchRefs, bFilterEditorOnly);
|
|
for (const UObject* Obj : ScratchRefs)
|
|
{
|
|
if (RefsProcessed.Contains(Obj))
|
|
{
|
|
continue;
|
|
}
|
|
RefsProcessed.Add(Obj);
|
|
|
|
if (DisallowSet.Contains(Obj))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const UObject* OuterIter = Obj->GetOuter();
|
|
bool bIsInRoot = false;
|
|
while (OuterIter)
|
|
{
|
|
if (AllowedRoots.Contains(OuterIter))
|
|
{
|
|
bIsInRoot = true;
|
|
break;
|
|
}
|
|
OuterIter = OuterIter->GetOuter();
|
|
}
|
|
if (!bIsInRoot)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
PendingRefs.Add(Obj);
|
|
}
|
|
ScratchRefs.Reset();
|
|
}
|
|
}
|
|
|
|
static void WriteObjectPath(FUtf8StringBuilderBase& OutPath, const UObject* ForObject, const UObject* OuterLimit)
|
|
{
|
|
const auto WriteObjectPathImpl = [&OutPath, OuterLimit](const UObject* ForObject, auto Self)
|
|
{
|
|
check(ForObject);
|
|
// ofpa files still have an outer package, but the GetPackage() terminator
|
|
// will not be reachable via the outer chain, replace any encoutered UPackage
|
|
// w/ the OuterLimit (acquired via GetPackage) when serializing:
|
|
if(Cast<UPackage>(ForObject) && ForObject != OuterLimit)
|
|
{
|
|
ForObject = OuterLimit;
|
|
}
|
|
|
|
if (ForObject == OuterLimit)
|
|
{
|
|
return;
|
|
}
|
|
Self(ForObject->GetOuter(), Self);
|
|
OutPath << "/";
|
|
OutPath << ForObject->GetName();
|
|
};
|
|
WriteObjectPathImpl(ForObject, WriteObjectPathImpl);
|
|
}
|
|
|
|
static void WriteFieldPath(FUtf8StringBuilderBase& OutPath, const FField* ForField, const UObject* OuterLimit)
|
|
{
|
|
const auto WriteWriteFieldPathImpl = [&OutPath, OuterLimit](const UObject* ForObject, const FField* ForField, auto Self)
|
|
{
|
|
if (ForObject == OuterLimit)
|
|
{
|
|
return;
|
|
}
|
|
if (ForField)
|
|
{
|
|
FFieldVariant Owner = ForField->GetOwnerVariant();
|
|
Self(Owner.ToUObject(), Owner.ToField(), Self);
|
|
}
|
|
else if (ForObject)
|
|
{
|
|
Self(ForObject->GetOuter(), nullptr, Self);
|
|
}
|
|
OutPath << "/";
|
|
if (ForObject)
|
|
{
|
|
OutPath << ForObject->GetName();
|
|
}
|
|
else
|
|
{
|
|
check(ForField);
|
|
OutPath << ForField->GetName();
|
|
}
|
|
};
|
|
WriteWriteFieldPathImpl(nullptr, ForField, WriteWriteFieldPathImpl);
|
|
}
|
|
|
|
FJsonStringifyImpl::FJsonStringifyImpl(TConstArrayView<const UObject*> Roots, const FJsonStringifyOptions& Options)
|
|
: WriteOptions(Options)
|
|
, MemoryWriter(Result)
|
|
, Writer(FJsonWriter::Create(&MemoryWriter))
|
|
, CurrentObject(nullptr)
|
|
{
|
|
const bool bFilterEditorOnly = (Options.Flags & EJsonStringifyFlags::FilterEditorOnlyData) != EJsonStringifyFlags::Default;
|
|
TArray<const UObject*> Exports;
|
|
GatherExports(Roots, {}, bFilterEditorOnly, RootObjects, Exports);
|
|
|
|
ObjectsToExport = TSet<const UObject*>(Exports);
|
|
|
|
MemoryWriter.SetIsPersistent(true);
|
|
MemoryWriter.SetFilterEditorOnly(bFilterEditorOnly);
|
|
MemoryWriter.SetIsTextFormat(true);
|
|
}
|
|
|
|
FUtf8String FJsonStringifyImpl::ToJson()
|
|
{
|
|
ToJsonBytes();
|
|
// Hacking around TJsonWriter and FMemoryWriter, is an array of bytes by
|
|
// any other name not exceedingly dangerous? Everyone should be working
|
|
// with utf8:
|
|
TArray<ANSICHAR>& ResultTyped = reinterpret_cast<TArray<ANSICHAR>&>(Result);
|
|
return FUtf8String(MoveTemp(ResultTyped));
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteObjectAsJsonToWriter(const UObject* OwningObject, const UObject* InObject, TSharedRef<FJsonWriter> WriterToUse)
|
|
{
|
|
if (CurrentScope)
|
|
{
|
|
CurrentScope->Apply();
|
|
}
|
|
|
|
if (InObject && InObject->IsIn(OwningObject) &&
|
|
ObjectsToExport.Contains(InObject) &&
|
|
!ObjectsExported.Contains(InObject))
|
|
{
|
|
ObjectsExported.Add(InObject);
|
|
TSharedRef<FJsonWriter> RootWriter = Writer;
|
|
Writer = WriterToUse;
|
|
WriteObjectToJson(InObject);
|
|
Writer = RootWriter;
|
|
}
|
|
else
|
|
{
|
|
// we just need to write a path
|
|
WriterToUse->WriteValueInline(FUtf8StringView(WriteObjectReference(InObject)));
|
|
}
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteFieldReferenceTo(const UObject* OwningObject, const FField* Value, TSharedRef<FJsonWriter> WriterToUse)
|
|
{
|
|
WriterToUse->WriteValueInline(FUtf8StringView(WriteFieldReference(Value)));
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteObjectAsJsonToArchive(const UObject* OwningObject, const UObject* InObject, FArchive* ArchiveToUse, int32 InitialIndentLevel)
|
|
{
|
|
TSharedRef<FJsonWriter> JsonWriter = FJsonWriter::Create(ArchiveToUse, InitialIndentLevel);
|
|
JsonWriter->HACK_SetPreviousTokenWritten();
|
|
WriteObjectAsJsonToWriter(OwningObject, InObject, JsonWriter);
|
|
}
|
|
|
|
void FJsonStringifyImpl::ToJsonBytes()
|
|
{
|
|
if (RootObjects.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// main entry point to our writer state machine:
|
|
Writer->WriteObjectStart();
|
|
Writer->WriteIdentifierPrefix(UE_JSON_ROOT_OBJECTS_KEY_TCHAR);
|
|
Writer->WriteArrayStartInline();
|
|
Writer->WriteLineTerminator();
|
|
for (const UObject* Object : RootObjects)
|
|
{
|
|
ObjectsExported.Add(Object);
|
|
WriteObjectToJson(Object);
|
|
}
|
|
Writer->WriteArrayEnd();
|
|
|
|
if(ShouldWritePackageSummary())
|
|
{
|
|
WritePackageSummary();
|
|
}
|
|
|
|
Writer->WriteObjectEnd();
|
|
// Result is now populated with Json representation
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteObjectToJson(const UObject* Object)
|
|
{
|
|
CurrentObject = Object;
|
|
|
|
// loop properties, writing any that have changed:
|
|
const UObject* const Archetype = Object->GetArchetype();
|
|
const UClass* const ArchetypeClass = Archetype ? Archetype->GetClass() : nullptr; // UObject's CDO itself has no archetype
|
|
Writer->WriteObjectStartInline();
|
|
// Write native UObject data - name, type, flags, native user serialize, etc
|
|
WriteNativeObjectData();
|
|
|
|
for (TFieldIterator<FProperty> FieldIt(Object->GetClass()); FieldIt; ++FieldIt)
|
|
{
|
|
FProperty* Property = *FieldIt;
|
|
const UObject* ValidatedArchtype = ArchetypeClass && ArchetypeClass->IsChildOf(Property->GetOwnerClass()) ?
|
|
Archetype : nullptr;
|
|
|
|
WriteIdentifierAndValueToJson(Object, ValidatedArchtype, Property);
|
|
}
|
|
// We may have inner objects that were not referenced directly by this object,
|
|
// but will be referenced by other objects in the graph. We must write them here, or we
|
|
// would have to encode them at the root level, which would disrupt locality.
|
|
// The draw back of recording them here is that we must order them ourselves,
|
|
// which will degrade the stability of the serialized buffer. It feels like 'the best
|
|
// we can do' is to write these objects in alphabetical order:
|
|
WriteIndirectlyReferencedContainedObjects(Object);
|
|
Writer->WriteObjectEnd();
|
|
|
|
// emit stream serializer, omitting tagged properties:
|
|
CurrentObject = CurrentObject->GetOuter();
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteNativeObjectData()
|
|
{
|
|
const UObject* Object = CurrentObject;
|
|
check(Object);
|
|
|
|
// Could use a structured archive to write this more declaratively...
|
|
Writer->WriteIdentifierPrefix(UE_JSON_OBJECT_INSTANCE_KEY_TCHAR);
|
|
Writer->WriteObjectStartInline();
|
|
Writer->WriteValue(UE_JSON_OBJECT_NAME_KEY_TCHAR, Object->GetName());
|
|
Writer->WriteUtf8Value(UE_JSON_OBJECT_CLASS_KEY_TCHAR, WriteObjectReference(Object->GetClass()));
|
|
Writer->WriteValue(UE_JSON_OBJECT_FLAGS_KEY_TCHAR, Object->GetFlags() & RF_Load);
|
|
Writer->WriteObjectEnd();
|
|
|
|
// This is tricky, we have no good mechanism for detecting whether an object wants a structured
|
|
// serialization or a traditional stream serialization. So I've decided to 'try' the structured
|
|
// serializer and if it writes nothing we fall back to the stream (FArchive) serializer. Hopefully
|
|
// some day we get rid of FStructuredArchive as it does not spark joy.
|
|
#if WITH_TEXT_ARCHIVE_SUPPORT
|
|
TArray<uint8> StructuredData = StructuredDataToJson(Object, Writer->GetIndentLevel());
|
|
if (!StructuredData.IsEmpty())
|
|
{
|
|
Writer->WriteIdentifierPrefix(UE_JSON_OBJECT_STRUCTURED_DATA_KEY_TCHAR);
|
|
Writer->WriteJsonRaw(FAnsiStringView((ANSICHAR*)StructuredData.GetData(), StructuredData.Num()));
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
// no useful native structured data, write the native serial data:
|
|
TArray<uint8> SerialData = SerialDataToJson(Object, Writer->GetIndentLevel());
|
|
if (!SerialData.IsEmpty())
|
|
{
|
|
Writer->WriteIdentifierPrefix(UE_JSON_OBJECT_SERIAL_DATA_KEY_TCHAR);
|
|
Writer->WriteJsonRaw(FAnsiStringView((ANSICHAR*)SerialData.GetData(), SerialData.Num()));
|
|
}
|
|
}
|
|
|
|
// Sparse class data is ambiguously serialized as part of SerializeDefaultObject,
|
|
// which our text serializer does not use (except for reference gathering). Lets write
|
|
// the special SparseClassData member here:
|
|
if (const UClass* AsClass = Cast<UClass>(Object))
|
|
{
|
|
if (const void* SCD = const_cast<UClass*>(AsClass)->GetSparseClassData(EGetSparseClassDataMethod::ReturnIfNull))
|
|
{
|
|
const UScriptStruct* SCDStruct = AsClass->GetSparseClassDataStruct();
|
|
const void* DefaultSCD = AsClass->GetArchetypeForSparseClassData();
|
|
const UScriptStruct* DefaultSCDStruct = AsClass->GetSparseClassDataArchetypeStruct();
|
|
WriteStructToJsonWithIdentifier(UE_JSON_OBJECT_SPARSE_CLASS_DATA_KEY_TCHAR, SCD, DefaultSCD, SCDStruct, DefaultSCDStruct);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteIndirectlyReferencedContainedObjects(const UObject* ForObject)
|
|
{
|
|
TArray<const UObject*> UnwrittenInners;
|
|
// @todo: naive implementation - is looping over every object here too slow?
|
|
for (const UObject* ObjectToExport : ObjectsToExport)
|
|
{
|
|
if (ObjectToExport->GetOuter() == ForObject && !ObjectsExported.Contains(ObjectToExport))
|
|
{
|
|
UnwrittenInners.Add(ObjectToExport);
|
|
}
|
|
}
|
|
|
|
if (UnwrittenInners.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UnwrittenInners.Sort([](const UObject& A, const UObject& B) { return A.GetName().Compare(B.GetName()) < 0; });
|
|
|
|
// write all UnwrittenInners into the __IndirectlyReferenced member, for simplicity we
|
|
// will always encode this as an array. We know that these unwritten inners will not be outered
|
|
// to the written objects here, so we don't need to tag them as exported ahead of time:
|
|
Writer->WriteIdentifierPrefix(UE_JSON_OBJECT_INDIRECTLY_REFERENCED_KEY_TCHAR);
|
|
Writer->WriteArrayStartInline();
|
|
Writer->WriteLineTerminator();
|
|
for (const UObject* Object : UnwrittenInners)
|
|
{
|
|
WriteObjectToJson(Object);
|
|
}
|
|
Writer->WriteArrayEnd();
|
|
}
|
|
|
|
FUtf8String FJsonStringifyImpl::WriteObjectReference(const UObject* ForObject) const
|
|
{
|
|
// matching FSaveContext::GetSaveableStatusNoOuter, if we integrate into
|
|
// save package we can reuse the logic from there:
|
|
if (!ForObject ||
|
|
!IsValid(ForObject) ||
|
|
(ForObject->HasAnyFlags(RF_Transient) && !ForObject->IsNative()))
|
|
{
|
|
return UE_JSON_OBJECT_REF_PREFIX UE_JSON_REF_NONE;
|
|
}
|
|
|
|
// Encode objects with the long standing pathname convention, we can
|
|
// encode a basis in the buffer to give users options when an asset
|
|
// is moved on the filesystem:
|
|
FUtf8StringBuilderBase Reference;
|
|
Reference << UE_JSON_OBJECT_REF_PREFIX;
|
|
Reference << ForObject->GetPathName();
|
|
|
|
return Reference.ToString();
|
|
}
|
|
|
|
FUtf8String FJsonStringifyImpl::WriteFieldReference(const FField* Value) const
|
|
{
|
|
if (!Value)
|
|
{
|
|
return UE_JSON_FIELD_REF_PREFIX UE_JSON_REF_NONE;
|
|
}
|
|
|
|
// identical convention for FField as above, albeit with a fieldref prefix
|
|
// instead of a uobject:
|
|
FUtf8StringBuilderBase Reference;
|
|
Reference << UE_JSON_FIELD_REF_PREFIX;
|
|
Reference << Value->GetPathName();
|
|
|
|
return Reference.ToString();
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteIdentifierAndValueToJson(const void* Container, const void* DefaultContainer, const FProperty* Property)
|
|
{
|
|
if (!Property->ShouldSerializeValue(MemoryWriter))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// this is inefficient for structs... but it papers over some
|
|
// problematic identical implementations. We will improve it
|
|
bool bMatchesDefault = DefaultContainer != nullptr && IsDeltaEncoding();
|
|
for (int32 Idx = 0; Idx < Property->ArrayDim && bMatchesDefault; ++Idx)
|
|
{
|
|
const void* Value = Property->ContainerPtrToValuePtr<void>(Container, Idx);
|
|
const void* ArchetypeValue = Property->ContainerPtrToValuePtr<void>(DefaultContainer, Idx);
|
|
|
|
FString StringValue;
|
|
Property->ExportText_Direct(StringValue, Value, Value, nullptr, PPF_ForDiff);
|
|
if (ArchetypeValue)
|
|
{
|
|
FString DefaultStringValue;
|
|
Property->ExportText_Direct(DefaultStringValue, ArchetypeValue, ArchetypeValue, nullptr, PPF_ForDiff);
|
|
bMatchesDefault = DefaultStringValue.Equals(StringValue);
|
|
}
|
|
}
|
|
|
|
if (bMatchesDefault)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FPendingScope PropertyIdentifier(this, [Impl = this, Property]()
|
|
{
|
|
Impl->Writer->WriteIdentifierPrefix(Property->GetName());
|
|
});
|
|
|
|
if (Property->ArrayDim > 1)
|
|
{
|
|
// encode as a static array:
|
|
FPendingScope ArrayIdentifier(this, [Impl = this]()
|
|
{
|
|
Impl->Writer->WriteArrayStartInline();
|
|
Impl->Writer->WriteLineTerminator();
|
|
},
|
|
[Impl = this]()
|
|
{
|
|
Impl->Writer->WriteArrayEnd();
|
|
}
|
|
);
|
|
for (int32 Index = 0; Index < Property->ArrayDim; ++Index)
|
|
{
|
|
WriteValueToJson(
|
|
Property->ContainerPtrToValuePtr<void>(Container, Index),
|
|
DefaultContainer ? Property->ContainerPtrToValuePtr<void>(DefaultContainer, Index) : nullptr,
|
|
Property);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WriteValueToJson(
|
|
Property->ContainerPtrToValuePtr<void>(Container),
|
|
DefaultContainer ? Property->ContainerPtrToValuePtr<void>(DefaultContainer) : nullptr,
|
|
Property);
|
|
}
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteValueToJson(const void* Value, const void* DefaultValue, const FProperty* Property)
|
|
{
|
|
bool bHandledAsAggregate = true;
|
|
if (const FStructProperty* StructProperty = CastField<FStructProperty>(Property))
|
|
{
|
|
// write any tagged data for the struct - but be sure to delta serialize
|
|
WriteStructToJson(Value, DefaultValue, StructProperty->Struct, StructProperty->Struct);
|
|
}
|
|
else if (CastField<FObjectProperty>(Property) || CastField<FClassProperty>(Property))
|
|
{
|
|
const UObject* Object = *(UObject**)Value;
|
|
WriteObjectAsJsonToWriter(CurrentObject, Object, Writer);
|
|
}
|
|
// containers:
|
|
else if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property))
|
|
{
|
|
WriteArrayToJson(Value, ArrayProperty);
|
|
}
|
|
else if (const FSetProperty* SetProperty = CastField<FSetProperty>(Property))
|
|
{
|
|
WriteSetToJson(Value, SetProperty);
|
|
}
|
|
else if (const FMapProperty* MapProperty = CastField<FMapProperty>(Property))
|
|
{
|
|
WriteMapToJson(Value, MapProperty);
|
|
}
|
|
else if (const FOptionalProperty* OptionalProperty = CastField<FOptionalProperty>(Property))
|
|
{
|
|
WriteOptionalToJson(Value, OptionalProperty);
|
|
}
|
|
else
|
|
{
|
|
bHandledAsAggregate = false; // fall back to value serialization
|
|
}
|
|
|
|
if (!bHandledAsAggregate)
|
|
{
|
|
WriteIntrinsicToJson(Value, Property);
|
|
}
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteIntrinsicToJson(const void* Value, const FProperty* Property)
|
|
{
|
|
if (CurrentScope)
|
|
{
|
|
CurrentScope->Apply();
|
|
}
|
|
|
|
// bools are special, because of GetPropertyValue, which can apply a mask
|
|
if (const FBoolProperty* BoolProp = CastField<FBoolProperty>(Property))
|
|
{
|
|
bool bValue = BoolProp->GetPropertyValue(Value);
|
|
Writer->WriteValueInline(bValue);
|
|
}
|
|
else if (const FByteProperty* ByteProperty = CastField<FByteProperty>(Property))
|
|
{
|
|
// byte property is special because of the enum/byte duality:
|
|
uint8 ValueAsIntrinsic = *(uint8*)Value;
|
|
if (const UEnum* EnumDef = ByteProperty->Enum)
|
|
{
|
|
FString StringValue = EnumDef->GetAuthoredNameStringByValue(ValueAsIntrinsic);
|
|
Writer->WriteValueInline(StringValue);
|
|
}
|
|
else
|
|
{
|
|
Writer->WriteValueInline(ValueAsIntrinsic);
|
|
}
|
|
}
|
|
else if (const FEnumProperty* EnumProperty = CastField<FEnumProperty>(Property))
|
|
{
|
|
// export enums as strings
|
|
const UEnum* EnumDef = EnumProperty->GetEnum();
|
|
FString StringValue = EnumDef->GetAuthoredNameStringByValue(
|
|
EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(Value));
|
|
Writer->WriteValueInline(StringValue);
|
|
}
|
|
else if( const FTextProperty* TextProperty = CastField<FTextProperty>(Property))
|
|
{
|
|
FText ValueAsText = *(FText*)Value;
|
|
Writer->WriteValueInline(ValueAsText);
|
|
}
|
|
#define INTRINSIC_TABLE \
|
|
INTRINSIC_ENTRY(FInt8Property, int8) \
|
|
INTRINSIC_ENTRY(FInt16Property, int16) \
|
|
INTRINSIC_ENTRY(FIntProperty, int32) \
|
|
INTRINSIC_ENTRY(FInt64Property, int64) \
|
|
INTRINSIC_ENTRY(FUInt16Property, uint16) \
|
|
INTRINSIC_ENTRY(FUInt32Property, uint32) \
|
|
INTRINSIC_ENTRY(FUInt64Property, uint64) \
|
|
INTRINSIC_ENTRY(FFloatProperty, float) \
|
|
INTRINSIC_ENTRY(FDoubleProperty, double)
|
|
|
|
#define INTRINSIC_ENTRY(FPropertyType, intrinsic_type) \
|
|
else if (const FPropertyType* As##intrinsic_type = CastField<FPropertyType>(Property)) \
|
|
{ \
|
|
intrinsic_type ValueAsIntrinsic = *(intrinsic_type*)Value; \
|
|
Writer->WriteValueInline(ValueAsIntrinsic); \
|
|
}
|
|
INTRINSIC_TABLE
|
|
#undef INTRINSIC_ENTRY
|
|
#undef INTRINSIC_TABLE
|
|
else
|
|
{
|
|
FString StringValue;
|
|
Property->ExportText_Direct(StringValue, Value, Value, nullptr, PPF_None);
|
|
Writer->WriteValueInline(StringValue);
|
|
}
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteStructToJsonWithIdentifier(const TCHAR* Identifier, const void* StructInstance, const void* DefaultInstance, const UScriptStruct* Struct, const UScriptStruct* DefaultStruct)
|
|
{
|
|
check(StructInstance && Struct);
|
|
FPendingScope StructIdentifier(this, [Impl = this, Identifier]()
|
|
{
|
|
Impl->Writer->WriteIdentifierPrefix(Identifier);
|
|
});
|
|
|
|
WriteStructToJson(
|
|
StructInstance,
|
|
DefaultInstance,
|
|
Struct,
|
|
DefaultStruct);
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteStructToJson(const void* StructInstance, const void* DefaultInstance, const UScriptStruct* Struct, const UScriptStruct* DefaultStruct)
|
|
{
|
|
// FInstancedStruct is a core level construct, and we can usefully decompose it, so try:
|
|
const FInstancedStruct* StructInstanceTyped = Struct == FInstancedStruct::StaticStruct() ?
|
|
static_cast<const FInstancedStruct*>(StructInstance) : nullptr;
|
|
const FInstancedStruct* DefaultInstanceTyped = DefaultStruct == FInstancedStruct::StaticStruct() ?
|
|
static_cast<const FInstancedStruct*>(DefaultInstance) : nullptr;
|
|
const UScriptStruct* InstancedStructType = StructInstanceTyped ?
|
|
StructInstanceTyped->GetScriptStruct() : nullptr;
|
|
const UScriptStruct* DefaultInstancedStructType = DefaultInstanceTyped ?
|
|
DefaultInstanceTyped->GetScriptStruct() : nullptr;
|
|
|
|
// For structs with an ambiguous type (like instanced structs), we want to disambiguate
|
|
// the type stored in the json, additionally for UPropertyBag, which is a transient
|
|
// non native struct and is regenerated on load we want to store its current layout.
|
|
// It's possible we could allow structures to customize their representation, but
|
|
// there are enough serialization customization facilities already and I do not want
|
|
// to introduce more.
|
|
const UPropertyBag* const PropertyBag = Cast<UPropertyBag>(InstancedStructType);
|
|
const bool bWriteStructType = InstancedStructType &&
|
|
!PropertyBag;
|
|
|
|
TUniquePtr<FInstancedStruct> FallbackStruct;
|
|
if(InstancedStructType)
|
|
{
|
|
StructInstance = StructInstanceTyped->GetMemory();
|
|
Struct = InstancedStructType;
|
|
if(DefaultInstanceTyped)
|
|
{
|
|
DefaultInstance = DefaultInstanceTyped->GetMemory();
|
|
DefaultStruct = DefaultInstancedStructType;
|
|
}
|
|
else
|
|
{
|
|
// fall back to default constructed struct
|
|
FallbackStruct = MakeUnique<FInstancedStruct>(InstancedStructType);
|
|
DefaultInstance = FallbackStruct.Get()->GetMemory();
|
|
DefaultStruct = InstancedStructType;
|
|
}
|
|
}
|
|
|
|
FPendingScope ObjectStartIdentifier(
|
|
this,
|
|
[Impl = this, InstancedStructType, bWriteStructType, PropertyBag]()
|
|
{
|
|
Impl->Writer->WriteObjectStartInline();
|
|
if(PropertyBag)
|
|
{
|
|
Impl->WritePropertyBagDescToJson(PropertyBag);
|
|
}
|
|
else if(bWriteStructType)
|
|
{
|
|
Impl->Writer->WriteUtf8Value(UE_JSON_SCRIPTSTRUCT_TCHAR, Impl->WriteObjectReference(InstancedStructType));
|
|
}
|
|
},
|
|
[Impl = this]() { Impl->Writer->WriteObjectEnd(); }
|
|
);
|
|
|
|
// @todo: can we improve results for structs that have custom serializers?
|
|
// We do not capture data outside of FProperty because I use this routine
|
|
// for inspecting objects and readability is paramount:
|
|
for (TFieldIterator<FProperty> PropertyIt(Struct); PropertyIt; ++PropertyIt)
|
|
{
|
|
FProperty* Property = *PropertyIt;
|
|
WriteIdentifierAndValueToJson(
|
|
StructInstance,
|
|
Property->IsInContainer(const_cast<UScriptStruct*>(DefaultStruct)) ? DefaultInstance : nullptr,
|
|
Property);
|
|
}
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteArrayToJson(const void* ArrayInstance, const FArrayProperty* Array)
|
|
{
|
|
if (CurrentScope)
|
|
{
|
|
CurrentScope->Apply();
|
|
}
|
|
|
|
Writer->WriteArrayStartInline();
|
|
FScriptArrayHelper ArrayHelper(Array, ArrayInstance);
|
|
const int32 ArrayNum = ArrayHelper.Num();
|
|
if (ArrayNum != 0)
|
|
{
|
|
Writer->WriteLineTerminator();
|
|
}
|
|
for (int32 Index = 0; Index < ArrayNum; ++Index)
|
|
{
|
|
const void* Element = ArrayHelper.GetRawPtr(Index);
|
|
const FProperty* Property = Array->Inner;
|
|
WriteValueToJson(Element, nullptr, Property);
|
|
}
|
|
|
|
Writer->WriteArrayEnd();
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteSetToJson(const void* SetInstance, const FSetProperty* SetProperty)
|
|
{
|
|
if (CurrentScope)
|
|
{
|
|
CurrentScope->Apply();
|
|
}
|
|
|
|
Writer->WriteArrayStartInline();
|
|
FScriptSetHelper SetHelper(SetProperty, SetInstance);
|
|
// Num() is number of elements, max index expensive to calculate, hence the odd loop:
|
|
// @todo: use set iterator
|
|
int32 SetNum = SetHelper.Num();
|
|
if (SetNum != 0)
|
|
{
|
|
Writer->WriteLineTerminator();
|
|
}
|
|
for (int32 Index = 0; SetNum; ++Index)
|
|
{
|
|
if (!SetHelper.IsValidIndex(Index))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
--SetNum;
|
|
const void* Element = SetHelper.GetElementPtr(Index);
|
|
const FProperty* Property = SetProperty->ElementProp;
|
|
WriteValueToJson(Element, nullptr, Property);
|
|
}
|
|
|
|
Writer->WriteArrayEnd();
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteMapToJson(const void* MapInstance, const FMapProperty* MapProperty)
|
|
{
|
|
if (CurrentScope)
|
|
{
|
|
CurrentScope->Apply();
|
|
}
|
|
|
|
Writer->WriteArrayStartInline();
|
|
FScriptMapHelper MapHelper(MapProperty, MapInstance);
|
|
// Num() is number of elements, max index expensive to calculate, hence the odd loop:
|
|
// @todo: use map iterator
|
|
int32 MapNum = MapHelper.Num();
|
|
if (MapNum != 0)
|
|
{
|
|
Writer->WriteLineTerminator();
|
|
}
|
|
for (int32 Index = 0; MapNum; ++Index)
|
|
{
|
|
if (!MapHelper.IsValidIndex(Index))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
--MapNum;
|
|
const void* Key = MapHelper.GetKeyPtr(Index);
|
|
const FProperty* KeyProperty = MapProperty->KeyProp;
|
|
const void* Value = MapHelper.GetValuePtr(Index);
|
|
const FProperty* ValueProperty = MapProperty->ValueProp;
|
|
|
|
// age old question - how do you want to encode a tuple in json? I've
|
|
// chosen named tuple:
|
|
Writer->WriteObjectStartInline();
|
|
Writer->WriteIdentifierPrefix(UE_JSON_TMAP_KEY_KEY_TCHAR);
|
|
WriteValueToJson(Key, nullptr, KeyProperty);
|
|
Writer->WriteIdentifierPrefix(UE_JSON_TMAP_VALUE_KEY_TCHAR);
|
|
WriteValueToJson(Value, nullptr, ValueProperty);
|
|
Writer->WriteObjectEnd();
|
|
}
|
|
|
|
Writer->WriteArrayEnd();
|
|
}
|
|
|
|
void FJsonStringifyImpl::WriteOptionalToJson(const void* OptionalInstnace, const FOptionalProperty* OptionalProperty)
|
|
{
|
|
if (CurrentScope)
|
|
{
|
|
CurrentScope->Apply();
|
|
}
|
|
Writer->WriteObjectStartInline();
|
|
if (const void* ValueAddress = static_cast<const void*>(OptionalProperty->GetValuePointerForReadOrReplaceIfSet(OptionalInstnace)))
|
|
{
|
|
Writer->WriteIdentifierPrefix(UE_JSON_OPTIONAL_VALUE_KEY_TCHAR);
|
|
WriteValueToJson(ValueAddress, nullptr, OptionalProperty->GetValueProperty());
|
|
}
|
|
Writer->WriteObjectEnd();
|
|
}
|
|
|
|
void FJsonStringifyImpl::WritePropertyBagDescToJson(const UPropertyBag* PropertyBag)
|
|
{
|
|
const FArrayProperty* PropertyDescsProperty = CastFieldChecked<FArrayProperty>(
|
|
PropertyBag->GetClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(UPropertyBag, PropertyDescs)));
|
|
|
|
Writer->WriteIdentifierPrefix(UE_JSON_PROPERTYBAG_TCHAR);
|
|
Writer->WriteObjectStartInline();
|
|
Writer->WriteIdentifierPrefix(UE_JSON_PROPERTYDESCS_TCHAR);
|
|
WriteArrayToJson(
|
|
PropertyDescsProperty->ContainerPtrToValuePtr<void>(PropertyBag),
|
|
PropertyDescsProperty
|
|
);
|
|
Writer->WriteObjectEnd();
|
|
}
|
|
|
|
TArray<uint8> FJsonStringifyImpl::SerialDataToJson(const UObject* Object, int32 InitialIndentLevel)
|
|
{
|
|
FJsonStringifyArchive SerialData(Object, InitialIndentLevel, this, Versions, MemoryWriter.IsFilterEditorOnly());
|
|
return SerialData.ToJson();
|
|
}
|
|
|
|
#if WITH_TEXT_ARCHIVE_SUPPORT
|
|
TArray<uint8> FJsonStringifyImpl::StructuredDataToJson(const UObject* Object, int32 InitialIndentLevel)
|
|
{
|
|
FJsonStringifyStructuredArchive StructuredData(Object, InitialIndentLevel, this, Versions, MemoryWriter.IsFilterEditorOnly());
|
|
return StructuredData.ToJson();
|
|
}
|
|
#endif // WITH_TEXT_ARCHIVE_SUPPORT
|
|
|
|
void FJsonStringifyImpl::WritePackageSummary()
|
|
{
|
|
Writer->WriteIdentifierPrefix(UE_JSON_PACKAGE_SUMMARY_KEY_TCHAR);
|
|
Writer->WriteObjectStartInline();
|
|
|
|
#if WITH_TEXT_ARCHIVE_SUPPORT
|
|
if (Versions.Num() != 0)
|
|
{
|
|
// filter dupes, preserving order of first encounter - we need some stable order:
|
|
TSet<FGuid> Encountered;
|
|
Versions.SetNum(Algo::StableRemoveIf(Versions,
|
|
[&Encountered](const FCustomVersion& V)
|
|
{
|
|
if (Encountered.Contains(V.Key))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Encountered.Add(V.Key);
|
|
return false;
|
|
}
|
|
}));
|
|
|
|
Writer->WriteIdentifierPrefix(UE_JSON_CUSTOM_VERSIONS_KEY_TCHAR);
|
|
FJsonStringifyStructuredArchive::WriteCustomVersionValueInline(Versions, Writer->GetIndentLevel(), MemoryWriter);
|
|
Writer->HACK_SetPreviousTokenWrittenSquareClose(); // the above will write the custom version as an array of tuples, track that write
|
|
}
|
|
#endif // WITH_TEXT_ARCHIVE_SUPPORT
|
|
|
|
// Note that there is no automatic compression here, if you add information to the package summary
|
|
// you should update ShouldWritePackageSummary
|
|
Writer->WriteObjectEnd();
|
|
}
|
|
|
|
bool FJsonStringifyImpl::IsDeltaEncoding() const
|
|
{
|
|
return !EnumHasAnyFlags(WriteOptions.Flags, EJsonStringifyFlags::DisableDeltaEncoding);
|
|
}
|
|
|
|
bool FJsonStringifyImpl::ShouldWritePackageSummary() const
|
|
{
|
|
return
|
|
#if WITH_TEXT_ARCHIVE_SUPPORT
|
|
Versions.Num() > 0;
|
|
#else
|
|
false;
|
|
#endif
|
|
}
|
|
|
|
FJsonStringifyImpl::FPendingScope::FPendingScope(FJsonStringifyImpl* To, const TFunction<void()>& Prefix)
|
|
: Owner(To)
|
|
, Outer(To->CurrentScope)
|
|
, PendingPrefix(Prefix)
|
|
{
|
|
To->CurrentScope = this;
|
|
}
|
|
|
|
FJsonStringifyImpl::FPendingScope::FPendingScope(FJsonStringifyImpl* To, const TFunction<void()>& Prefix, const TFunction<void()>& Postfix)
|
|
: Owner(To)
|
|
, Outer(To->CurrentScope)
|
|
, PendingPrefix(Prefix)
|
|
, PendingPostfix({Postfix})
|
|
{
|
|
To->CurrentScope = this;
|
|
}
|
|
|
|
FJsonStringifyImpl::FPendingScope::~FPendingScope()
|
|
{
|
|
if (bHasBeenApplied && PendingPostfix)
|
|
{
|
|
(*PendingPostfix)();
|
|
}
|
|
|
|
// scope left, if we've applied the prefix then we need to apply the postfix:
|
|
Owner->CurrentScope = Outer;
|
|
}
|
|
|
|
void FJsonStringifyImpl::FPendingScope::Apply()
|
|
{
|
|
// writer has decided to apply the scope, write all pending prefixes:
|
|
if (bHasBeenApplied)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bHasBeenApplied = true;
|
|
if (Outer)
|
|
{
|
|
Outer->Apply();
|
|
}
|
|
PendingPrefix();
|
|
}
|
|
|
|
}
|