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

737 lines
23 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "UObject/SoftObjectPath.h"
#include "UObject/PropertyPortFlags.h"
#include "UObject/UnrealType.h"
#include "UObject/ObjectRedirector.h"
#include "Misc/PackageName.h"
#include "Misc/StringBuilder.h"
#include "UObject/LinkerLoad.h"
#include "UObject/UObjectThreadContext.h"
#include "UObject/CoreRedirects.h"
#include "Misc/RedirectCollector.h"
FSoftObjectPath::FSoftObjectPath(const UObject* InObject)
{
if (InObject)
{
SetPath(InObject->GetPathName());
}
}
FString FSoftObjectPath::ToString() const
{
// Most of the time there is no sub path so we can do a single string allocation
if (SubPathString.IsEmpty())
{
return GetAssetPathString();
}
TCHAR Buffer[FName::StringBufferSize];
FStringView AssetPathString;
if (AssetPathName.IsValid())
{
AssetPathString = FStringView(Buffer, AssetPathName.ToString(Buffer));
}
FString FullPathString;
// Preallocate to correct size and then append strings
FullPathString.Reserve(AssetPathString.Len() + SubPathString.Len() + 1);
FullPathString += AssetPathString;
FullPathString += ':';
FullPathString += SubPathString;
return FullPathString;
}
void FSoftObjectPath::ToString(FStringBuilderBase& Builder) const
{
if (!AssetPathName.IsNone())
{
Builder << AssetPathName;
}
if (SubPathString.Len() > 0)
{
Builder << ':' << SubPathString;
}
}
/** Helper function that adds info about the object currently being serialized when triggering an ensure about invalid soft object path */
static FString GetObjectBeingSerializedForSoftObjectPath()
{
FString Result;
FUObjectSerializeContext* SerializeContext = FUObjectThreadContext::Get().GetSerializeContext();
if (SerializeContext && SerializeContext->SerializedObject)
{
Result = FString::Printf(TEXT(" while serializing %s"), *SerializeContext->SerializedObject->GetFullName());
}
return Result;
}
void FSoftObjectPath::SetPath(FWideStringView Path)
{
if (Path.IsEmpty() || Path.Equals(TEXT("None"), ESearchCase::CaseSensitive))
{
// Empty path, just empty the pathname.
Reset();
}
else if (ensureMsgf(!FPackageName::IsShortPackageName(Path), TEXT("Cannot create SoftObjectPath with short package name '%.*s'%s! You must pass in fully qualified package names"), Path.Len(), Path.GetData(), *GetObjectBeingSerializedForSoftObjectPath()))
{
if (Path[0] != '/')
{
// Possibly an ExportText path. Trim the ClassName.
Path = FPackageName::ExportTextPathToObjectPath(Path);
}
int32 ColonIndex;
if (Path.FindChar(':', ColonIndex))
{
// Has a subobject, split on that then create a name from the temporary path
AssetPathName = FName(Path.Left(ColonIndex));
SubPathString = Path.Mid(ColonIndex + 1);
}
else
{
// No Subobject
AssetPathName = FName(Path);
SubPathString.Empty();
}
}
}
void FSoftObjectPath::SetPath(FAnsiStringView Path)
{
TStringBuilder<256> Wide;
Wide << Path;
SetPath(Wide);
}
void FSoftObjectPath::SetPath(FName PathName)
{
if (PathName.IsNone())
{
Reset();
}
else
{
TCHAR Buffer[FName::StringBufferSize];
FStringView Path(Buffer, PathName.ToString(Buffer));
if (ensureMsgf(!FPackageName::IsShortPackageName(Path), TEXT("Cannot create SoftObjectPath with short package name '%s'%s! You must pass in fully qualified package names"), Path.GetData(), *GetObjectBeingSerializedForSoftObjectPath()))
{
if (Path[0] != '/')
{
// Possibly an ExportText path. Trim the ClassName.
Path = FPackageName::ExportTextPathToObjectPath(Path);
}
int32 ColonIndex;
if (Path.FindChar(':', ColonIndex))
{
// Has a subobject, split on that then create a name from the temporary path
AssetPathName = FName(Path.Left(ColonIndex));
SubPathString = Path.Mid(ColonIndex + 1);
}
else
{
// No Subobject
AssetPathName = Path.GetData() == Buffer ? PathName : FName(Path);
SubPathString.Empty();
}
}
}
}
#if WITH_EDITOR
extern bool* GReportSoftObjectPathRedirects;
#endif
bool FSoftObjectPath::PreSavePath(bool* bReportSoftObjectPathRedirects)
{
#if WITH_EDITOR
if (IsNull())
{
return false;
}
FName FoundRedirection = GRedirectCollector.GetAssetPathRedirection(AssetPathName);
if (FoundRedirection != NAME_None)
{
if (AssetPathName != FoundRedirection && bReportSoftObjectPathRedirects)
{
*bReportSoftObjectPathRedirects = true;
}
AssetPathName = FoundRedirection;
return true;
}
if (FixupCoreRedirects())
{
return true;
}
#endif // WITH_EDITOR
return false;
}
void FSoftObjectPath::PostLoadPath(FArchive* InArchive) const
{
#if WITH_EDITOR
GRedirectCollector.OnSoftObjectPathLoaded(*this, InArchive);
#endif // WITH_EDITOR
}
bool FSoftObjectPath::Serialize(FArchive& Ar)
{
// Archivers will call back into SerializePath for the various fixups
Ar << *this;
return true;
}
bool FSoftObjectPath::Serialize(FStructuredArchive::FSlot Slot)
{
// Archivers will call back into SerializePath for the various fixups
Slot << *this;
return true;
}
void FSoftObjectPath::SerializePath(FArchive& Ar)
{
bool bSerializeInternals = true;
#if WITH_EDITOR
if (Ar.IsSaving())
{
PreSavePath(false ? GReportSoftObjectPathRedirects : nullptr);
}
// Only read serialization options in editor as it is a bit slow
FName PackageName, PropertyName;
ESoftObjectPathCollectType CollectType = ESoftObjectPathCollectType::AlwaysCollect;
ESoftObjectPathSerializeType SerializeType = ESoftObjectPathSerializeType::AlwaysSerialize;
FSoftObjectPathThreadContext& ThreadContext = FSoftObjectPathThreadContext::Get();
ThreadContext.GetSerializationOptions(PackageName, PropertyName, CollectType, SerializeType, &Ar);
if (SerializeType == ESoftObjectPathSerializeType::NeverSerialize)
{
bSerializeInternals = false;
}
else if (SerializeType == ESoftObjectPathSerializeType::SkipSerializeIfArchiveHasSize)
{
bSerializeInternals = Ar.IsObjectReferenceCollector() || Ar.Tell() < 0;
}
#endif // WITH_EDITOR
if (bSerializeInternals)
{
if (Ar.IsLoading() && Ar.UE4Ver() < VER_UE4_ADDED_SOFT_OBJECT_PATH)
{
FString Path;
Ar << Path;
if (Ar.UE4Ver() < VER_UE4_KEEP_ONLY_PACKAGE_NAMES_IN_STRING_ASSET_REFERENCES_MAP)
{
Path = FPackageName::GetNormalizedObjectPath(Path);
}
SetPath(MoveTemp(Path));
}
else
{
Ar << AssetPathName;
Ar << SubPathString;
}
}
#if WITH_EDITOR
if (Ar.IsLoading())
{
if (Ar.IsPersistent())
{
PostLoadPath(&Ar);
// If we think it's going to work, we try to do the pre-save fixup now. This is important because it helps with blueprint CDO save determinism with redirectors
// It's important that the entire CDO hierarchy gets fixed up before an instance in a map gets saved otherwise the delta serialization will save too much
// If the asset registry hasn't fully loaded this won't necessarily work, but it won't do any harm
// This will never work in -game builds or on initial load so don't try
if (GIsEditor && !GIsInitialLoad)
{
PreSavePath(nullptr);
}
}
if (Ar.GetPortFlags() & PPF_DuplicateForPIE)
{
// Remap unique ID if necessary
// only for fixing up cross-level references, inter-level references handled in FDuplicateDataReader
FixupForPIE();
}
}
#endif // WITH_EDITOR
}
bool FSoftObjectPath::operator==(FSoftObjectPath const& Other) const
{
return AssetPathName == Other.AssetPathName && SubPathString == Other.SubPathString;
}
bool FSoftObjectPath::ExportTextItem(FString& ValueStr, FSoftObjectPath const& DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) const
{
if (0 != (PortFlags & EPropertyPortFlags::PPF_ExportCpp))
{
return false;
}
if (!IsNull())
{
// Fixup any redirectors
FSoftObjectPath Temp = *this;
Temp.PreSavePath();
ValueStr += Temp.ToString();
}
else
{
ValueStr += TEXT("None");
}
return true;
}
bool FSoftObjectPath::ImportTextItem(const TCHAR*& Buffer, int32 PortFlags, UObject* Parent, FOutputDevice* ErrorText, FArchive* InSerializingArchive)
{
TStringBuilder<256> ImportedPath;
const TCHAR* NewBuffer = FPropertyHelpers::ReadToken(Buffer, /* out */ ImportedPath, /* dotted names */ true);
if (!NewBuffer)
{
return false;
}
Buffer = NewBuffer;
if (ImportedPath == TEXT("None"_SV))
{
Reset();
}
else
{
if (*Buffer == TCHAR('\''))
{
// A ' token likely means we're looking at a path string in the form "Texture2d'/Game/UI/HUD/Actions/Barrel'" and we need to read and append the path part
// We have to skip over the first ' as FPropertyHelpers::ReadToken doesn't read single-quoted strings correctly, but does read a path correctly
Buffer++; // Skip the leading '
ImportedPath.Reset();
NewBuffer = FPropertyHelpers::ReadToken(Buffer, /* out */ ImportedPath, /* dotted names */ true);
if (!NewBuffer)
{
return false;
}
Buffer = NewBuffer;
if (*Buffer++ != TCHAR('\''))
{
return false;
}
}
SetPath(ImportedPath);
}
#if WITH_EDITOR
if (Parent && IsEditorOnlyObject(Parent))
{
// We're probably reading config for an editor only object, we need to mark this reference as editor only
FSoftObjectPathSerializationScope SerializationScope(NAME_None, NAME_None, ESoftObjectPathCollectType::EditorOnlyCollect, ESoftObjectPathSerializeType::AlwaysSerialize);
PostLoadPath(InSerializingArchive);
}
else
#endif
{
// Consider this a load, so Config string references get cooked
PostLoadPath(InSerializingArchive);
}
return true;
}
/**
* Serializes from mismatched tag.
*
* @template_param TypePolicy The policy should provide two things:
* - GetTypeName() method that returns registered name for this property type,
* - typedef Type, which is a C++ type to serialize if property matched type name.
* @param Tag Property tag to match type.
* @param Ar Archive to serialize from.
*/
template <class TypePolicy>
bool SerializeFromMismatchedTagTemplate(FString& Output, const FPropertyTag& Tag, FStructuredArchive::FSlot Slot)
{
if (Tag.Type == TypePolicy::GetTypeName())
{
typename TypePolicy::Type* ObjPtr = nullptr;
Slot << ObjPtr;
if (ObjPtr)
{
Output = ObjPtr->GetPathName();
}
else
{
Output = FString();
}
return true;
}
else if (Tag.Type == NAME_StrProperty)
{
FString String;
Slot << String;
Output = String;
return true;
}
return false;
}
bool FSoftObjectPath::SerializeFromMismatchedTag(struct FPropertyTag const& Tag, FStructuredArchive::FSlot Slot)
{
struct UObjectTypePolicy
{
typedef UObject Type;
static const FName FORCEINLINE GetTypeName() { return NAME_ObjectProperty; }
};
FString Path = ToString();
bool bReturn = SerializeFromMismatchedTagTemplate<UObjectTypePolicy>(Path, Tag, Slot);
if (Slot.GetUnderlyingArchive().IsLoading())
{
SetPath(MoveTemp(Path));
PostLoadPath(&Slot.GetUnderlyingArchive());
}
return bReturn;
}
UObject* FSoftObjectPath::TryLoad(FUObjectSerializeContext* InLoadContext) const
{
UObject* LoadedObject = nullptr;
if (!IsNull())
{
if (IsSubobject())
{
// For subobjects, it's not safe to call LoadObject directly, so we want to load the parent object and then resolve again
FSoftObjectPath TopLevelPath = FSoftObjectPath(AssetPathName, FString());
UObject* TopLevelObject = TopLevelPath.TryLoad(InLoadContext);
// This probably loaded the top-level object, so re-resolve ourselves
return ResolveObject();
}
FString PathString = ToString();
#if WITH_EDITOR
if (GPlayInEditorID != INDEX_NONE)
{
// If we are in PIE and this hasn't already been fixed up, we need to fixup at resolution time. We cannot modify the path as it may be somewhere like a blueprint CDO
FSoftObjectPath FixupObjectPath = *this;
if (FixupObjectPath.FixupForPIE())
{
PathString = FixupObjectPath.ToString();
}
}
#endif
LoadedObject = StaticLoadObject(UObject::StaticClass(), nullptr, *PathString, nullptr, LOAD_None, nullptr, true);
#if WITH_EDITOR
// Look at core redirects if we didn't find the object
if (!LoadedObject)
{
FSoftObjectPath FixupObjectPath = *this;
if (FixupObjectPath.FixupCoreRedirects())
{
LoadedObject = LoadObject<UObject>(nullptr, *FixupObjectPath.ToString());
}
}
#endif
while (UObjectRedirector* Redirector = Cast<UObjectRedirector>(LoadedObject))
{
LoadedObject = Redirector->DestinationObject;
}
}
return LoadedObject;
}
UObject* FSoftObjectPath::ResolveObject() const
{
// Don't try to resolve if we're saving a package because StaticFindObject can't be used here
// and we usually don't want to force references to weak pointers while saving.
if (IsNull() || GIsSavingPackage)
{
return nullptr;
}
#if WITH_EDITOR
if (GPlayInEditorID != INDEX_NONE)
{
// If we are in PIE and this hasn't already been fixed up, we need to fixup at resolution time. We cannot modify the path as it may be somewhere like a blueprint CDO
FSoftObjectPath FixupObjectPath = *this;
if (FixupObjectPath.FixupForPIE())
{
return FixupObjectPath.ResolveObjectInternal();
}
}
#endif
return ResolveObjectInternal();
}
UObject* FSoftObjectPath::ResolveObjectInternal() const
{
if (SubPathString.IsEmpty())
{
TCHAR PathString[FName::StringBufferSize];
AssetPathName.ToString(PathString);
return ResolveObjectInternal(PathString);
}
else
{
return ResolveObjectInternal(*ToString());
}
}
UObject* FSoftObjectPath::ResolveObjectInternal(const TCHAR* PathString) const
{
UObject* FoundObject = FindObject<UObject>(nullptr, PathString);
#if WITH_EDITOR
// Look at core redirects if we didn't find the object
if (!FoundObject)
{
FSoftObjectPath FixupObjectPath = *this;
if (FixupObjectPath.FixupCoreRedirects())
{
FoundObject = FindObject<UObject>(nullptr, *FixupObjectPath.ToString());
}
}
#endif
while (UObjectRedirector* Redirector = Cast<UObjectRedirector>(FoundObject))
{
FoundObject = Redirector->DestinationObject;
}
return FoundObject;
}
FSoftObjectPath FSoftObjectPath::GetOrCreateIDForObject(const class UObject* Object)
{
check(Object);
return FSoftObjectPath(Object);
}
void FSoftObjectPath::AddPIEPackageName(FName NewPIEPackageName)
{
PIEPackageNames.Add(NewPIEPackageName);
}
void FSoftObjectPath::ClearPIEPackageNames()
{
PIEPackageNames.Empty();
}
bool FSoftObjectPath::FixupForPIE(int32 PIEInstance)
{
#if WITH_EDITOR
if (PIEInstance != INDEX_NONE && !IsNull())
{
const FString Path = ToString();
// Determine if this reference has already been fixed up for PIE
const FString ShortPackageOuterAndName = FPackageName::GetLongPackageAssetName(Path);
if (!ShortPackageOuterAndName.StartsWith(PLAYWORLD_PACKAGE_PREFIX))
{
// Name of the ULevel subobject of UWorld, set in InitializeNewWorld
const bool bIsChildOfLevel = SubPathString.StartsWith(TEXT("PersistentLevel."));
FString PIEPath = FString::Printf(TEXT("%s/%s_%d_%s"), *FPackageName::GetLongPackagePath(Path), PLAYWORLD_PACKAGE_PREFIX, PIEInstance, *ShortPackageOuterAndName);
const FName PIEPackage = (!bIsChildOfLevel ? FName(*FPackageName::ObjectPathToPackageName(PIEPath)) : NAME_None);
// Duplicate if this an already registered PIE package or this looks like a level subobject reference
if (bIsChildOfLevel || PIEPackageNames.Contains(PIEPackage))
{
// Need to prepend PIE prefix, as we're in PIE and this refers to an object in a PIE package
SetPath(MoveTemp(PIEPath));
return true;
}
}
}
#endif
return false;
}
bool FSoftObjectPath::FixupForPIE()
{
return FixupForPIE(GPlayInEditorID);
}
bool FSoftObjectPath::FixupCoreRedirects()
{
FString OldString = ToString();
FCoreRedirectObjectName OldName = FCoreRedirectObjectName(OldString);
// Always try the object redirect, this will pick up any package redirects as well
// For things that look like native objects, try all types as we don't know which it would be
const bool bIsNative = OldString.StartsWith(TEXT("/Script/"));
FCoreRedirectObjectName NewName = FCoreRedirects::GetRedirectedName(bIsNative ? ECoreRedirectFlags::Type_AllMask : ECoreRedirectFlags::Type_Object, OldName);
if (OldName != NewName)
{
// Only do the fixup if the old object isn't in memory, this avoids false positives
UObject* FoundOldObject = FindObjectSafe<UObject>(nullptr, *OldString);
if (!FoundOldObject)
{
SetPath(NewName.ToString());
return true;
}
}
return false;
}
bool FSoftClassPath::SerializeFromMismatchedTag(struct FPropertyTag const& Tag, FStructuredArchive::FSlot Slot)
{
struct UClassTypePolicy
{
typedef UClass Type;
// Class property shares the same tag id as Object property
static const FName FORCEINLINE GetTypeName() { return NAME_ObjectProperty; }
};
FString Path = ToString();
bool bReturn = SerializeFromMismatchedTagTemplate<UClassTypePolicy>(Path, Tag, Slot);
if (Slot.GetUnderlyingArchive().IsLoading())
{
SetPath(MoveTemp(Path));
PostLoadPath(&Slot.GetUnderlyingArchive());
}
return bReturn;
}
UClass* FSoftClassPath::ResolveClass() const
{
return Cast<UClass>(ResolveObject());
}
FSoftClassPath FSoftClassPath::GetOrCreateIDForClass(const UClass* InClass)
{
check(InClass);
return FSoftClassPath(InClass);
}
bool FSoftObjectPathThreadContext::GetSerializationOptions(FName& OutPackageName, FName& OutPropertyName, ESoftObjectPathCollectType& OutCollectType, ESoftObjectPathSerializeType& OutSerializeType, FArchive* Archive) const
{
FName CurrentPackageName, CurrentPropertyName;
ESoftObjectPathCollectType CurrentCollectType = ESoftObjectPathCollectType::AlwaysCollect;
ESoftObjectPathSerializeType CurrentSerializeType = ESoftObjectPathSerializeType::AlwaysSerialize;
bool bFoundAnything = false;
if (OptionStack.Num() > 0)
{
// Go from the top of the stack down
for (int32 i = OptionStack.Num() - 1; i >= 0; i--)
{
const FSerializationOptions& Options = OptionStack[i];
// Find first valid package/property names. They may not necessarily match
if (Options.PackageName != NAME_None && CurrentPackageName == NAME_None)
{
CurrentPackageName = Options.PackageName;
}
if (Options.PropertyName != NAME_None && CurrentPropertyName == NAME_None)
{
CurrentPropertyName = Options.PropertyName;
}
// Restrict based on lowest/most restrictive collect type
if (Options.CollectType < CurrentCollectType)
{
CurrentCollectType = Options.CollectType;
}
if (Options.SerializeType < CurrentSerializeType)
{
CurrentSerializeType = Options.SerializeType;
}
}
bFoundAnything = true;
}
// Check UObject serialize context as a backup
FUObjectSerializeContext* LoadContext = Archive ? Archive->GetSerializeContext() : nullptr;
if (LoadContext && LoadContext->SerializedObject)
{
FLinkerLoad* Linker = LoadContext->SerializedObject->GetLinker();
if (Linker)
{
if (CurrentPackageName == NAME_None)
{
CurrentPackageName = FName(*FPackageName::FilenameToLongPackageName(Linker->Filename));
}
if (Archive == nullptr)
{
// Use archive from linker if it wasn't passed in
Archive = Linker;
}
bFoundAnything = true;
}
}
// Check archive for property/editor only info, this works for any serialize if passed in
if (Archive)
{
FProperty* CurrentProperty = Archive->GetSerializedProperty();
if (CurrentProperty && CurrentPropertyName == NAME_None)
{
CurrentPropertyName = CurrentProperty->GetFName();
}
bool bEditorOnly = false;
#if WITH_EDITOR
bEditorOnly = Archive->IsEditorOnlyPropertyOnTheStack();
static FName UntrackedName = TEXT("Untracked");
if (CurrentProperty && CurrentProperty->HasMetaData(UntrackedName))
{
// Property has the Untracked metadata, so set to never collect references
CurrentCollectType = ESoftObjectPathCollectType::NeverCollect;
}
#endif
// If we were always collect before and not overridden by stack options, set to editor only
if (bEditorOnly && CurrentCollectType == ESoftObjectPathCollectType::AlwaysCollect)
{
CurrentCollectType = ESoftObjectPathCollectType::EditorOnlyCollect;
}
bFoundAnything = true;
}
if (bFoundAnything)
{
OutPackageName = CurrentPackageName;
OutPropertyName = CurrentPropertyName;
OutCollectType = CurrentCollectType;
OutSerializeType = CurrentSerializeType;
return true;
}
return bFoundAnything;
}
FThreadSafeCounter FSoftObjectPath::CurrentTag(1);
TSet<FName> FSoftObjectPath::PIEPackageNames;