// 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 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(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(nullptr, *FixupObjectPath.ToString()); } } #endif while (UObjectRedirector* Redirector = Cast(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(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(nullptr, *FixupObjectPath.ToString()); } } #endif while (UObjectRedirector* Redirector = Cast(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(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(Path, Tag, Slot); if (Slot.GetUnderlyingArchive().IsLoading()) { SetPath(MoveTemp(Path)); PostLoadPath(&Slot.GetUnderlyingArchive()); } return bReturn; } UClass* FSoftClassPath::ResolveClass() const { return Cast(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 FSoftObjectPath::PIEPackageNames;