EM_Task/CoreUObject/Private/UObject/FieldPath.cpp
Boshuang Zhao 5144a49c9b add
2026-02-13 16:18:33 +08:00

426 lines
15 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
FieldPath.cpp: Pointer to UObject asset, keeps extra information so that it is works even if the asset is not in memory
=============================================================================*/
#include "UObject/FieldPath.h"
#include "UObject/UnrealType.h"
#include "UObject/Package.h"
#include "UObject/UObjectArray.h"
#include "UObject/FortniteMainBranchObjectVersion.h"
#include "UObject/ReleaseObjectVersion.h"
#include "UObject/LinkerLoad.h"
#include "UObject/UObjectThreadContext.h"
#include "UObject/PropertyHelper.h"
#include "Algo/Reverse.h"
/** Helper function used for logging the path to property */
static FString PathToString(const TArray<FName>& InPath)
{
// See FFieldPath::Generate for formatting specifics
// Generate path from the outermost (last item) to the property (first item)
// Initilize with the number of delimiters we're going to add
int32 PathLength = InPath.Num() ? (InPath.Num() - 1) : 0;
for (FName PathSegment: InPath)
{
PathLength += PathSegment.GetStringLength();
}
FString Result;
if (PathLength)
{
// Allocate once and construct the path string
Result.Reserve(PathLength);
Result += InPath.Last().ToString();
if (InPath.Num() > 1)
{
// Path may be either a full path from the package to the property or just from the outer property to the inner property
// If it's a full path, the delimiter char for the next object (owner struct) will be a '.' otherwise we assume it's all subobjects
if (Result.Len() && Result[0] == '/')
{
Result += '.';
}
else
{
Result += SUBOBJECT_DELIMITER_CHAR;
}
// Add the remainder of the path (it's all subobjects from this point)
for (int32 PathIndex = InPath.Num() - 2; PathIndex >= 0; --PathIndex)
{
Result += InPath[PathIndex].ToString();
if (PathIndex > 0)
{
Result += SUBOBJECT_DELIMITER_CHAR;
}
}
}
}
return Result;
}
#if WITH_EDITORONLY_DATA
FFieldPath::FFieldPath(UField* InField, const FName& InPropertyTypeName)
{
if (InField)
{
// Must be constructed from the equivalent UField class
check(InField->GetClass()->GetFName() == InPropertyTypeName);
GenerateFromUField(InField);
}
}
#endif
void FFieldPath::Generate(FField* InField)
{
Reset();
if (InField)
{
// Add field names from the innermost to the outermost, stop at the owner struct
UStruct* Owner = InField->GetOwnerStruct();
check(Owner); // A field that has no owner is not allowed in FFieldPath
for (FFieldVariant Iter(InField); Iter.IsValid(); Iter = Iter.GetOwnerVariant())
{
UStruct* MaybeOwner = Iter.Get<UStruct>();
if (MaybeOwner == Owner)
{
break;
}
else
{
Path.Add(Iter.GetFName());
}
}
ResolvedOwner = Owner;
ResolvedField = InField;
#if WITH_EDITORONLY_DATA
FieldPathSerialNumber = Owner->FieldPathSerialNumber;
InitialFieldClass = InField->GetClass();
#endif // WITH_EDITORONLY_DATA
}
}
void FFieldPath::Generate(const TCHAR* InFieldPathString)
{
// Expected format is: FullPackageName.Subobject[:Subobject:...]:FieldName
check(InFieldPathString);
Reset();
TArray<FName> Result;
{
TCHAR NameBuffer[NAME_SIZE];
int32 NameIndex = 0;
// Construct names
while (true)
{
if (*InFieldPathString == '.' || *InFieldPathString == SUBOBJECT_DELIMITER_CHAR || *InFieldPathString == '\0')
{
NameBuffer[NameIndex] = '\0';
if (NameIndex > 0)
{
Result.Add(NameBuffer);
NameIndex = 0;
}
if (*InFieldPathString == '\0')
{
break;
}
}
else
{
NameBuffer[NameIndex++] = *InFieldPathString;
}
++InFieldPathString;
}
}
if (Result.Num() > 1)
{
// Reverse the order so that it's innermost to outermost
Algo::Reverse(Result);
}
else if (Result.Num() == 1 && Result[0] == NAME_None)
{
Result.Empty();
}
Path = MoveTemp(Result);
ResolveField();
}
UStruct* FFieldPath::ConvertFromFullPath(FLinkerLoad* InLinker)
{
// First try the StaticFindObject approach
UStruct* Owner = TryToResolveOwnerFromStruct();
if (!Owner)
{
// UClass::Serialize() unhashes the class currently being serialized so SFO will not work on it
// If possible try to find the owner through the current serialize context
if (InLinker)
{
Owner = TryToResolveOwnerFromLinker(InLinker);
}
}
// It's possible the full path points to a renamed asset
UE_CLOG(!Owner && Path.Num(), LogProperty, Verbose, TEXT("Failed resolve owner when converting from full property path \"%s\""), *PathToString(Path));
return Owner;
}
/** Helper function that checks if two paths have identical trailing sequences */
static bool HasCommonTrailingSequence(const TArray<FName>& PathA, const TArray<FName>& PathB)
{
bool bTrailingSeuenceIdentical = true;
const int32 MaxNumToCheck = FMath::Min(PathA.Num(), PathB.Num());
for (int32 PathIndex = 0; PathIndex < MaxNumToCheck; ++PathIndex)
{
if (PathA[PathA.Num() - PathIndex - 1] != PathB[PathB.Num() - PathIndex - 1])
{
bTrailingSeuenceIdentical = false;
break;
}
}
return bTrailingSeuenceIdentical;
}
UStruct* FFieldPath::TryToResolveOwnerFromLinker(FLinkerLoad* InLinker) const
{
UStruct* OwnerStruct = nullptr;
check(InLinker);
FUObjectSerializeContext* Context = InLinker->GetSerializeContext();
if (Context && Context->SerializedObject && Context->SerializedObject->IsA<UStruct>())
{
TArray<FName> StructPath;
for (UObject* Obj = Context->SerializedObject; Obj; Obj = Obj->GetOuter())
{
StructPath.Add(Obj->GetFName());
}
// Check if our Path contains StructToPath (the assumption is that Path has more elements than struct path) otherwise we know the struct can't hold our property
if (StructPath.Num() < Path.Num() && HasCommonTrailingSequence(StructPath, Path))
{
int32 OwnerPathIndex = Path.Num() - StructPath.Num();
OwnerStruct = CastChecked<UStruct>(Context->SerializedObject);
ResolvedOwner = OwnerStruct;
// Remove portion of the path responsible for storing the owner path
FFieldPath* MutableThis = const_cast<FFieldPath*>(this);
MutableThis->Path.RemoveAt(OwnerPathIndex, Path.Num() - OwnerPathIndex);
}
}
return OwnerStruct;
}
UStruct* FFieldPath::TryToResolveOwnerFromStruct(UStruct* InCurrentStruct /*= nullptr*/, EPathResolveType InResolveType /*= FFieldPath::UseStructIfOuterNotFound*/) const
{
// Resolve from the outermost to the innermost UObject
UObject* LastOuter = nullptr;
int32 LastOuterIndex = -1;
for (int32 PathIndex = Path.Num() - 1; PathIndex > 0; --PathIndex)
{
UObject* Outer = StaticFindObjectFastSafe(UObject::StaticClass(), LastOuter, Path[PathIndex]);
if (InCurrentStruct && PathIndex == (Path.Num() - 1))
{
UObject* CurrentOutermost = InCurrentStruct->GetOutermost();
if ((InResolveType == FFieldPath::UseStructIfOuterNotFound && !Outer) || // Outer is not found so try to use the provided struct Outer
(InResolveType == FFieldPath::UseStructAlways && CurrentOutermost != Outer) // Prioritize the provided struct Outer over the resolved one
)
{
Outer = CurrentOutermost;
}
}
if (!Outer)
{
break;
}
LastOuterIndex = PathIndex;
LastOuter = Outer;
}
UStruct* Owner = Cast<UStruct>(LastOuter);
if (Owner)
{
ResolvedOwner = Owner;
// Remove portion of the path responsible for storing the owner path
FFieldPath* MutableThis = const_cast<FFieldPath*>(this);
MutableThis->Path.RemoveAt(LastOuterIndex, MutableThis->Path.Num() - LastOuterIndex);
}
return Owner;
}
FField* FFieldPath::TryToResolvePath(UStruct* InCurrentStruct, FFieldPath::EPathResolveType InResolveType /*= FFieldPath::UseStructIfOuterNotFound*/) const
{
FField* Result = nullptr;
UStruct* Owner = ResolvedOwner.Get();
if (!Owner)
{
// We're probably dealing with an old path format where the Path array contained the full path to the field
Owner = TryToResolveOwnerFromStruct(InCurrentStruct, InResolveType);
}
// At this point the owner should've been fully resolved
if (Owner && Path.Num())
{
int32 PathIndex = Path.Num() - 1;
check(PathIndex <= 1);
Result = FindFProperty<FField>(Owner, Path[PathIndex]);
if (Result)
{
if (PathIndex > 0)
{
// Nested property
Result = Result->GetInnerFieldByName(Path[0]);
}
}
}
return Result;
}
FString FFieldPath::ToString() const
{
FString Result;
if (UStruct* Owner = ResolvedOwner.Get())
{
if (ResolvedField)
{
Result = ResolvedField->GetPathName();
}
else
{
Result = Owner->GetPathName();
Result += SUBOBJECT_DELIMITER_CHAR;
Result += PathToString(Path);
}
}
else
{
// Revert back to old path format where the package and UStruct owner were also specified
Result = PathToString(Path);
}
// Nativized BP support
if (Result.StartsWith(UDynamicClass::GetTempPackagePrefix(), ESearchCase::IgnoreCase))
{
Result.RemoveFromStart(UDynamicClass::GetTempPackagePrefix(), ESearchCase::IgnoreCase);
}
return Result;
}
FArchive& operator<<(FArchive& Ar, FFieldPath& InOutPropertyPath)
{
Ar.UsingCustomVersion(FFortniteMainBranchObjectVersion::GUID);
Ar.UsingCustomVersion(FReleaseObjectVersion::GUID);
if (Ar.IsSaving())
{
UStruct* Owner = InOutPropertyPath.ResolvedOwner.Get();
bool bFilterEditorOnlyProperty = false;
if (Owner && Ar.IsFilterEditorOnly())
{
FProperty* ResolvedProperty = CastField<FProperty>(InOutPropertyPath.GetTyped(FProperty::StaticClass()));
if (ResolvedProperty && ResolvedProperty->HasAnyPropertyFlags(CPF_EditorOnly))
{
bFilterEditorOnlyProperty = true;
}
}
if (!Owner || bFilterEditorOnlyProperty)
{
// If there's no owner, make sure we don't serialize potentially unresolved path from the actual field
// because if we don't save the owner, we won't be able to resolve it anyway.
// Possible scenario: the owner was GC'd and the path is no longer valid
TArray<FName> EmptyPath;
UStruct* NullOwner = nullptr;
Ar << EmptyPath;
Ar << NullOwner;
UE_CLOG(InOutPropertyPath.Path.Num(), LogProperty, Verbose, TEXT("Null owner but property path is not empty when saving \"%s\""), *PathToString(InOutPropertyPath.Path));
}
else
{
Ar << InOutPropertyPath.Path;
Ar << Owner;
}
checkf(Owner == InOutPropertyPath.ResolvedOwner.Get(), TEXT("FFieldPath owner has changed when saving, this is not allowed (Path: \"%s\", new owner: \"%s\")"),
*InOutPropertyPath.ToString(), *GetPathNameSafe(Owner));
}
else
{
Ar << InOutPropertyPath.Path;
// The old serialization format could save 'None' paths, they should be just empty
if (InOutPropertyPath.Path.Num() == 1 && InOutPropertyPath.Path[0] == NAME_None)
{
InOutPropertyPath.Path.Empty();
}
if (Ar.CustomVer(FFortniteMainBranchObjectVersion::GUID) >= FFortniteMainBranchObjectVersion::FFieldPathOwnerSerialization || Ar.CustomVer(FReleaseObjectVersion::GUID) >= FReleaseObjectVersion::FFieldPathOwnerSerialization)
{
UStruct* SerializedOwner = InOutPropertyPath.ResolvedOwner.Get();
Ar << SerializedOwner;
InOutPropertyPath.ResolvedOwner = SerializedOwner;
if (!SerializedOwner)
{
UE_CLOG(InOutPropertyPath.Path.Num(), LogProperty, Verbose, TEXT("Serialized null owner for property \"%s\""), *PathToString(InOutPropertyPath.Path));
// At this point it makes no sense to keep the remainder of the path around as it will produce unnecessary warnings
// Possible scenario: owning struct was not cooked or deleted and the asset was not loaded
InOutPropertyPath.Path.Empty();
}
}
else if (InOutPropertyPath.Path.Num())
{
// Convert from the old format: resolve the owner now and remove its path from the field path
FLinkerLoad* Linker = Cast<FLinkerLoad>(Ar.GetLinker());
UStruct* Owner = InOutPropertyPath.ConvertFromFullPath(Linker);
InOutPropertyPath.ResolvedOwner = Owner;
// This usually happens when the old path format serialized a path and then the owner struct's package got renamed or moved
// There's code to handle that in bot UClass and UAnimBlueprintGeneratedClass
UE_CLOG(!Owner, LogProperty, Verbose, TEXT("Failed to resolve property owner from Path \"%s\""), *PathToString(InOutPropertyPath.Path));
}
else
{
InOutPropertyPath.ResolvedOwner = nullptr;
}
if (!Ar.IsObjectReferenceCollector())
{
InOutPropertyPath.ClearCachedField();
}
}
return Ar;
}
#if WITH_EDITORONLY_DATA
void FFieldPath::GenerateFromUField(UField* InField)
{
Reset();
for (UObject* Obj = InField; Obj; Obj = Obj->GetOuter())
{
UStruct* MaybeOwner = Cast<UStruct>(Obj);
if (MaybeOwner)
{
ResolvedOwner = MaybeOwner;
break;
}
else
{
Path.Add(Obj->GetFName());
}
}
}
bool FFieldPath::IsFieldPathSerialNumberIdentical(UStruct* InStruct) const
{
return FieldPathSerialNumber == InStruct->FieldPathSerialNumber;
}
int32 FFieldPath::GetFieldPathSerialNumber(UStruct* InStruct) const
{
return InStruct->FieldPathSerialNumber;
}
#endif // WITH_EDITORONLY_DATA