// 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& 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()) { // Set to null any pointer to an external asset References.Add(ObjRef); } return *this; } TArray& 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& 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 Roots, TConstArrayView DisallowList, bool bFilterEditorOnly, TArray& OutRoots, TArray& OutExports) { // find everything roots references that is within roots and put it into // OutExports, unless it is in the disallow list: TSet DisallowSet; DisallowSet.Append(DisallowList); for (const UObject* Obj : DisallowList) { // const hacks and GetObjects expressiveness junk TArray DisallowedSubObjects; GetObjectsWithOuter(Obj, DisallowedSubObjects, true, RF_Transient); for (UObject* DisallowedObj : DisallowedSubObjects) { DisallowSet.Add(DisallowedObj); } } TSet AllowedRoots; AllowedRoots.Append(Roots); for (const UObject* Obj : Roots) { if (Obj && !Obj->IsA()) { OutRoots.Add(Obj); } } TArray PendingRefs; Algo::CopyIf(Roots, PendingRefs, [](const UObject* Obj) { return IsValid(Obj); }); TSet RefsProcessed; RefsProcessed.Append(PendingRefs); TArray ScratchRefs; // just keeping this allocation alive across iterations while (PendingRefs.Num()) { const UObject* Iter = PendingRefs.Pop(); if (AllowedRoots.Contains(Iter) && Iter->IsA()) { // add all immediate roots of a package, if filter 'editor only' objects // exclude any object that are of an editor only native type: TArray 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(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 Roots, const FJsonStringifyOptions& Options) : WriteOptions(Options) , MemoryWriter(Result) , Writer(FJsonWriter::Create(&MemoryWriter)) , CurrentObject(nullptr) { const bool bFilterEditorOnly = (Options.Flags & EJsonStringifyFlags::FilterEditorOnlyData) != EJsonStringifyFlags::Default; TArray Exports; GatherExports(Roots, {}, bFilterEditorOnly, RootObjects, Exports); ObjectsToExport = TSet(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& ResultTyped = reinterpret_cast&>(Result); return FUtf8String(MoveTemp(ResultTyped)); } void FJsonStringifyImpl::WriteObjectAsJsonToWriter(const UObject* OwningObject, const UObject* InObject, TSharedRef WriterToUse) { if (CurrentScope) { CurrentScope->Apply(); } if (InObject && InObject->IsIn(OwningObject) && ObjectsToExport.Contains(InObject) && !ObjectsExported.Contains(InObject)) { ObjectsExported.Add(InObject); TSharedRef 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 WriterToUse) { WriterToUse->WriteValueInline(FUtf8StringView(WriteFieldReference(Value))); } void FJsonStringifyImpl::WriteObjectAsJsonToArchive(const UObject* OwningObject, const UObject* InObject, FArchive* ArchiveToUse, int32 InitialIndentLevel) { TSharedRef 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 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 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 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(Object)) { if (const void* SCD = const_cast(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 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(Container, Idx); const void* ArchetypeValue = Property->ContainerPtrToValuePtr(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(Container, Index), DefaultContainer ? Property->ContainerPtrToValuePtr(DefaultContainer, Index) : nullptr, Property); } } else { WriteValueToJson( Property->ContainerPtrToValuePtr(Container), DefaultContainer ? Property->ContainerPtrToValuePtr(DefaultContainer) : nullptr, Property); } } void FJsonStringifyImpl::WriteValueToJson(const void* Value, const void* DefaultValue, const FProperty* Property) { bool bHandledAsAggregate = true; if (const FStructProperty* StructProperty = CastField(Property)) { // write any tagged data for the struct - but be sure to delta serialize WriteStructToJson(Value, DefaultValue, StructProperty->Struct, StructProperty->Struct); } else if (CastField(Property) || CastField(Property)) { const UObject* Object = *(UObject**)Value; WriteObjectAsJsonToWriter(CurrentObject, Object, Writer); } // containers: else if (const FArrayProperty* ArrayProperty = CastField(Property)) { WriteArrayToJson(Value, ArrayProperty); } else if (const FSetProperty* SetProperty = CastField(Property)) { WriteSetToJson(Value, SetProperty); } else if (const FMapProperty* MapProperty = CastField(Property)) { WriteMapToJson(Value, MapProperty); } else if (const FOptionalProperty* OptionalProperty = CastField(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(Property)) { bool bValue = BoolProp->GetPropertyValue(Value); Writer->WriteValueInline(bValue); } else if (const FByteProperty* ByteProperty = CastField(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(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(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(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(StructInstance) : nullptr; const FInstancedStruct* DefaultInstanceTyped = DefaultStruct == FInstancedStruct::StaticStruct() ? static_cast(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(InstancedStructType); const bool bWriteStructType = InstancedStructType && !PropertyBag; TUniquePtr FallbackStruct; if(InstancedStructType) { StructInstance = StructInstanceTyped->GetMemory(); Struct = InstancedStructType; if(DefaultInstanceTyped) { DefaultInstance = DefaultInstanceTyped->GetMemory(); DefaultStruct = DefaultInstancedStructType; } else { // fall back to default constructed struct FallbackStruct = MakeUnique(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 PropertyIt(Struct); PropertyIt; ++PropertyIt) { FProperty* Property = *PropertyIt; WriteIdentifierAndValueToJson( StructInstance, Property->IsInContainer(const_cast(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(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( 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(PropertyBag), PropertyDescsProperty ); Writer->WriteObjectEnd(); } TArray FJsonStringifyImpl::SerialDataToJson(const UObject* Object, int32 InitialIndentLevel) { FJsonStringifyArchive SerialData(Object, InitialIndentLevel, this, Versions, MemoryWriter.IsFilterEditorOnly()); return SerialData.ToJson(); } #if WITH_TEXT_ARCHIVE_SUPPORT TArray 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 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& Prefix) : Owner(To) , Outer(To->CurrentScope) , PendingPrefix(Prefix) { To->CurrentScope = this; } FJsonStringifyImpl::FPendingScope::FPendingScope(FJsonStringifyImpl* To, const TFunction& Prefix, const TFunction& 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(); } }