Files
UnrealEngine/Engine/Source/Runtime/UniversalObjectLocator/Private/UniversalObjectLocator.cpp
2025-05-18 13:04:45 +08:00

709 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "UniversalObjectLocator.h"
#include "DirectPathObjectLocator.h"
#include "UniversalObjectLocatorInitializeParams.h"
#include "UniversalObjectLocatorInitializeResult.h"
#include "UniversalObjectLocatorStringParams.h"
#include "UniversalObjectLocatorStringUtils.h"
#include "UniversalObjectLocatorRegistry.h"
#define LOCTEXT_NAMESPACE "UOL"
namespace UE::UniversalObjectLocator
{
static constexpr FStringView MagicLeadingString = TEXTVIEW("uobj://");
const FFragmentType* FindBestFragmentType(const UObject* Object, UObject* Context);
} // namespace UE::UniversalObjectLocator
bool operator==(const FUniversalObjectLocator& A, const FUniversalObjectLocator& B)
{
return A.Fragments == B.Fragments;
}
bool operator!=(const FUniversalObjectLocator& A, const FUniversalObjectLocator& B)
{
return A.Fragments != B.Fragments;
}
uint32 GetTypeHash(const FUniversalObjectLocator& Locator)
{
return GetTypeHash(Locator.Fragments);
}
FUniversalObjectLocator::FUniversalObjectLocator()
{
}
FUniversalObjectLocator::FUniversalObjectLocator(UObject* Object, UObject* Context, UObject* StopAtContext)
{
Reset(Object, Context, StopAtContext);
}
UE::UniversalObjectLocator::FResolveResult FUniversalObjectLocator::Resolve(const FResolveParams& Params) const
{
using namespace UE::UniversalObjectLocator;
if (UE::IsSavingPackage(nullptr) || IsGarbageCollecting())
{
return FResolveResult();
}
// Check for invalid combinations of flags
check(!EnumHasAllFlags(Params.Flags, ELocatorResolveFlags::Load | ELocatorResolveFlags::Unload));
// Cannot have WillWait without Async
check(!EnumHasAllFlags(Params.Flags, ELocatorResolveFlags::WillWait) || EnumHasAllFlags(Params.Flags, ELocatorResolveFlags::Async));
if (EnumHasAnyFlags(Params.Flags, ELocatorResolveFlags::Async))
{
return ResolveAsyncImpl(Params);
}
else
{
return ResolveSyncImpl(Params);
}
}
UE::UniversalObjectLocator::FResolveResult FUniversalObjectLocator::ResolveSyncImpl(const FResolveParams& Params) const
{
using namespace UE::UniversalObjectLocator;
check(!EnumHasAnyFlags(Params.Flags, ELocatorResolveFlags::Async));
FResolveResult EmptyResult;
if (Fragments.Num() == 0)
{
return EmptyResult;
}
if (Fragments.Num() == 1)
{
return Fragments[0].Resolve(Params);
}
bool bLoadedIndirectly = false;
FResolveResult LastResult;
UObject* CurrentContext = Params.Context;
const int32 Num = Fragments.Num();
for (int32 Index = 0; Index < Num; ++Index)
{
const FUniversalObjectLocatorFragment& Fragment = Fragments[Index];
const bool bLastFragment = Index == (Num-1);
// Only unload the last one
FResolveParams RelativeParams(CurrentContext, Params.Flags);
if (!bLastFragment)
{
RelativeParams.Flags &= ~ELocatorResolveFlags::Unload;
}
LastResult = Fragment.Resolve(RelativeParams);
if (EnumHasAnyFlags(RelativeParams.Flags, ELocatorResolveFlags::Unload))
{
return LastResult;
}
FResolveResultData ResultData = LastResult.SyncGet();
if (ResultData.Object == nullptr)
{
// If anything fails to resolve, nothing resolves
return EmptyResult;
}
CurrentContext = ResultData.Object;
// If the last one was implicitly loaded or created, the final result should report that
if (bLastFragment)
{
ResultData.Flags.bWasLoadedIndirectly = bLoadedIndirectly;
}
else
{
bLoadedIndirectly |= ResultData.Flags.bWasLoaded;
}
}
return LastResult;
}
UE::UniversalObjectLocator::FResolveResult FUniversalObjectLocator::ResolveAsyncImpl(const FResolveParams& Params) const
{
using namespace UE::UniversalObjectLocator;
check(EnumHasAnyFlags(Params.Flags, ELocatorResolveFlags::Async));
if (Fragments.Num() == 0)
{
return FResolveResult();
}
struct FState : TSharedFromThis<FState>
{
FState(const TArray<FUniversalObjectLocatorFragment>& InFragments, ELocatorResolveFlags InInputResolveFlags)
: FragmentsCopy(InFragments)
, CurrentIndex(-1)
, InputResolveFlags(InInputResolveFlags)
{}
void ProcessNext(FResolveResultData LastResult)
{
const int32 Index = ++CurrentIndex;
const bool bFinished = Index == FragmentsCopy.Num();
const bool bLastFragment = Index == (FragmentsCopy.Num()-1);
UObject* Context = LastResult.Object;
// If the previous one was loaded or created, any subsequent operations are loaded indirectly
if (!bLastFragment)
{
bLoadedIndirectly = LastResult.Flags.bWasLoaded;
}
const bool bCannotContinue = Context == nullptr && Index != 0;
if (bFinished || bCannotContinue)
{
if (AsyncResult.IsSet())
{
LastResult.Flags.bWasLoadedIndirectly = bLoadedIndirectly;
AsyncResult->SetValue(LastResult);
}
else
{
FinalResult = FResolveResult(LastResult);
}
return;
}
// Try and resolve this one
FResolveParams RelativeParams(Context, InputResolveFlags);
// Only unload the last one
if (!bLastFragment)
{
RelativeParams.Flags &= ~ELocatorResolveFlags::Unload;
}
FResolveResult Result = FragmentsCopy[Index].Resolve(RelativeParams);
// If we don't need to wait, call the next one immediately
if (!Result.NeedsWait())
{
ProcessNext(Result.SyncGet());
return;
}
// We need to wait for something to complete...
// If the currently held FinalResult is not async, we need to make it so
if (!FinalResult.IsAsync())
{
check(!AsyncResult.IsSet());
AsyncResult.Emplace();
FinalResult = FResolveResult(AsyncResult->GetFuture());
}
// Set the continuation
Result.AsyncGet(
[State = AsShared()](const FResolveResultData& InValue)
{
State->ProcessNext(InValue);
}
);
}
TArray<FUniversalObjectLocatorFragment> FragmentsCopy;
TOptional<
TPromise<FResolveResultData>
> AsyncResult;
FResolveResult FinalResult;
int32 CurrentIndex;
ELocatorResolveFlags InputResolveFlags;
bool bLoadedIndirectly = false;
};
TSharedPtr<FState> SharedState = MakeShared<FState>(this->Fragments, Params.Flags);
SharedState->ProcessNext(nullptr);
return MoveTemp(SharedState->FinalResult);
}
UObject* FUniversalObjectLocator::SyncFind(UObject* Context) const
{
return Resolve(FResolveParams::SyncFind(Context)).SyncGet().Object;
}
UObject* FUniversalObjectLocator::SyncLoad(UObject* Context) const
{
return Resolve(FResolveParams::SyncLoad(Context)).SyncGet().Object;
}
void FUniversalObjectLocator::SyncUnload(UObject* Context) const
{
Resolve(FResolveParams::SyncUnload(Context)).SyncGet();
}
UE::UniversalObjectLocator::FResolveResult FUniversalObjectLocator::AsyncFind(UObject* Context) const
{
return Resolve(FResolveParams::AsyncFind(Context));
}
UE::UniversalObjectLocator::FResolveResult FUniversalObjectLocator::AsyncLoad(UObject* Context) const
{
return Resolve(FResolveParams::AsyncLoad(Context));
}
UE::UniversalObjectLocator::FResolveResult FUniversalObjectLocator::AsyncUnload(UObject* Context) const
{
return Resolve(FResolveParams::AsyncUnload(Context));
}
void FUniversalObjectLocator::Reset()
{
Fragments.Empty();
}
void FUniversalObjectLocator::Reset(UObject* InObject, UObject* Context, UObject* StopAtContext)
{
Fragments.Reset();
if (!InObject || !AddFragment(InObject, Context, StopAtContext))
{
// Failed to create the locator
Fragments.Empty();
}
}
void FUniversalObjectLocator::AddFragment(FUniversalObjectLocatorFragment&& InFragment)
{
Fragments.Emplace(MoveTemp(InFragment));
}
const UE::UniversalObjectLocator::FFragmentType* FUniversalObjectLocator::GetLastFragmentType() const
{
return Fragments.Num() != 0 ? Fragments.Last().GetFragmentType() : nullptr;
}
UE::UniversalObjectLocator::FFragmentTypeHandle FUniversalObjectLocator::GetLastFragmentTypeHandle() const
{
return Fragments.Num() != 0 ? Fragments.Last().GetFragmentTypeHandle() : UE::UniversalObjectLocator::FFragmentTypeHandle();
}
FUniversalObjectLocatorFragment* FUniversalObjectLocator::GetLastFragment()
{
return Fragments.Num() != 0 ? &Fragments.Last() : nullptr;
}
const FUniversalObjectLocatorFragment* FUniversalObjectLocator::GetLastFragment() const
{
return Fragments.Num() != 0 ? &Fragments.Last() : nullptr;
}
bool FUniversalObjectLocator::ForEachFragment(TFunctionRef<bool(int32, int32, const FUniversalObjectLocatorFragment&)> InFunction) const
{
for (int32 FragmentIndex = 0, NumFragments = Fragments.Num(); FragmentIndex < NumFragments; ++FragmentIndex)
{
if(!InFunction(FragmentIndex, NumFragments, Fragments[FragmentIndex]))
{
return false;
}
}
return true;
}
UE::UniversalObjectLocator::EFragmentTypeFlags FUniversalObjectLocator::GetDefaultFlags() const
{
using namespace UE::UniversalObjectLocator;
EFragmentTypeFlags Flags = EFragmentTypeFlags::None;
for (const FUniversalObjectLocatorFragment& Fragment : Fragments)
{
if (const FFragmentType* FragmentTypePtr = Fragment.GetFragmentType())
{
EnumAddFlags(Flags, FragmentTypePtr->Flags);
}
}
return Flags;
}
void FUniversalObjectLocator::ToString(FStringBuilderBase& OutString) const
{
using namespace UE::UniversalObjectLocator;
// Universal Object Locators currently write to strings of the following form:
// uobj://fragment-type-id=payload-string!...!fragment-type-id-n=payload-string-n
TStringBuilder<128> PayloadScratchSpace;
OutString += MagicLeadingString;
int32 NumPrintedTypes = 0;
int32 NumPrintedPayloads = 0;
// Print the fragment types as the path
for (int32 Index = 0; Index < Fragments.Num(); ++Index)
{
const FUniversalObjectLocatorFragment& Fragment = Fragments[Index];
if (const FFragmentType* FragmentTypePtr = Fragment.GetFragmentType())
{
if (NumPrintedTypes != 0)
{
OutString += '/';
}
FragmentTypePtr->FragmentTypeID.AppendString(OutString);
++NumPrintedTypes;
}
}
if (NumPrintedTypes == 0)
{
OutString += TEXT("none");
return;
}
OutString += TEXT("?");
// Print the payloads as a query string
for (int32 Index = 0; Index < Fragments.Num(); ++Index)
{
const FUniversalObjectLocatorFragment& Fragment = Fragments[Index];
const FFragmentType* FragmentTypePtr = Fragment.GetFragmentType();
const UStruct* FragmentStruct = FragmentTypePtr ? FragmentTypePtr->GetStruct() : nullptr;
if (FragmentStruct)
{
if (NumPrintedPayloads != 0)
{
OutString += '&';
}
PayloadScratchSpace.Reset();
FragmentTypePtr->ToString(Fragment.GetPayload(), PayloadScratchSpace);
if (PayloadScratchSpace.Len() != 0)
{
OutString.Appendf(TEXT("payload%i="), Index);
OutString.Append(PayloadScratchSpace.ToView());
}
++NumPrintedPayloads;
}
}
}
UE::UniversalObjectLocator::FParseStringResult FUniversalObjectLocator::TryParseString(FStringView InString, const FParseStringParams& InParams)
{
using namespace UE::UniversalObjectLocator;
FParseStringResult Result;
if (!InString.StartsWith(MagicLeadingString, ESearchCase::IgnoreCase))
{
return Result.Failure(UE_UOL_PARSE_ERROR(InParams, LOCTEXT("Error_MissingScheme", "String does not start with uobj://")));
}
InString = Result.Progress(InString, MagicLeadingString.Len());
if (InString.Len() == 0)
{
return Result.Failure(UE_UOL_PARSE_ERROR(InParams, LOCTEXT("Error_EmptyPath", "Path is empty.")));
}
if (InString.Compare(TEXTVIEW("none"), ESearchCase::IgnoreCase) == 0)
{
Reset();
return Result.Success();
}
FUniversalObjectLocator Tmp;
// First parse the fragment types
bool bFinishedParsingFragments = false;
while (InString.Len() != 0 && !bFinishedParsingFragments)
{
// Find the next / or ? character
const int32 NextDelimiter = UE::String::FindFirstOfAnyChar(InString, TEXTVIEW("/?#"));
FStringView ThisFragment = InString;
if (NextDelimiter != INDEX_NONE)
{
// If the delimiter is a ? this is the last fragment, but there might be
// some additional payload string to parse
bFinishedParsingFragments = (InString[NextDelimiter] == '?');
// Trim the fragment string to the delimiter that we found
// and move the remaining string to after the delimiter
ThisFragment = InString.Left(NextDelimiter);
// Now trim the rest of the string to remove this fragment.
// If the delimiter is a # then we have nothing more to parse
if (InString[NextDelimiter] == '#')
{
// Indicate that we finished parsing at the #, but reset the string
// so that we stop parsing anything else
Result.NumCharsParsed += NextDelimiter;
InString.Reset();
}
else
{
// Progress past the delimiter
InString = Result.Progress(InString, NextDelimiter + 1);
}
// Empty string - just move on to the next one
if (ThisFragment.Len() == 0)
{
continue;
}
}
else
{
// No delimiter found, so the rest of the string is parsed as a fragment type
Result.NumCharsParsed += InString.Len();
InString.Reset();
}
FUniversalObjectLocatorFragment& NewFragment = Tmp.Fragments.Emplace_GetRef();
FParseStringResult TypeResult = NewFragment.TryParseFragmentType(ThisFragment, InParams);
Result.NumCharsParsed += TypeResult.NumCharsParsed;
if (!TypeResult)
{
return Result.Failure(
UE_UOL_PARSE_ERROR(
InParams,
FText::Format(
LOCTEXT("Error_InvalidFragmentType", "Fragment Type specifier is invalid: {0}."),
FText::FromStringView(ThisFragment)
)
)
);
}
}
// Next parse the fragment payloads
bool bFinishedParsingPayloads = false;
while (InString.Len() != 0 && !bFinishedParsingPayloads)
{
// Find the next & or # character
const int32 NextDelimiter = UE::String::FindFirstOfAnyChar(InString, TEXTVIEW("&#"));
FStringView ThisPayload = InString;
if (NextDelimiter != INDEX_NONE)
{
// If the delimiter is a # this is the last payload, so we finish parsing
// after this one
bFinishedParsingFragments = (InString[NextDelimiter] == '#');
// Trim the fragment string to the delimiter that we found
// and move the remaining string to after the delimiter
ThisPayload = InString.Left(NextDelimiter);
// Progress past the delimiter
InString = Result.Progress(InString, NextDelimiter + 1);
// Empty string - just move on to the next one
if (ThisPayload.Len() == 0)
{
continue;
}
}
else
{
// No delimiter - parse the remaining string as a payload and finish parsing
Result.NumCharsParsed += InString.Len();
InString.Reset();
}
static constexpr FStringView PayloadString = TEXTVIEW("payload");
// Try and parse a fragment index - any other query string key=value pairs will be skipped
const int32 EqualsDelimiter = UE::String::FindFirstChar(ThisPayload, '=');
if (EqualsDelimiter != INDEX_NONE && ThisPayload.StartsWith(PayloadString, ESearchCase::IgnoreCase) && ThisPayload.Len() > 8)
{
int32 NumChars = 0;
uint32 ParsedIndex = 0;
// Parse an integer from the position after the 'payload' string
if (!ParseUnsignedInteger(ThisPayload.RightChop(PayloadString.Len()), ParsedIndex, &NumChars) || ParsedIndex > static_cast<uint32>(std::numeric_limits<int32>::max()))
{
// Invalid int
Result.NumCharsParsed += ThisPayload.Len();
continue;
}
else if (ParsedIndex >= static_cast<uint32>(Tmp.Fragments.Num()))
{
// Invalid index
Result.NumCharsParsed += ThisPayload.Len();
continue;
}
// Handle non-sensical payload strings like payload0123another=
if (PayloadString.Len() + NumChars != EqualsDelimiter)
{
Result.NumCharsParsed += ThisPayload.Len();
continue;
}
// Skip over the index and =
ThisPayload = Result.Progress(ThisPayload, PayloadString.Len() + NumChars + 1);
if (ThisPayload.Len() == 0)
{
// Leave the fragment as default if there is an empty string
continue;
}
const int32 FragmentIndex = static_cast<int32>(ParsedIndex);
if (!Tmp.Fragments.IsValidIndex(FragmentIndex))
{
// Invalid fragment index for this payload - skip it
continue;
}
FUniversalObjectLocatorFragment& Fragment = Tmp.Fragments[FragmentIndex];
FParseStringResult PayloadResult = Fragment.TryParseFragmentPayload(ThisPayload, InParams);
Result.NumCharsParsed += PayloadResult.NumCharsParsed;
if (!PayloadResult)
{
const FFragmentType* Type = Fragment.GetFragmentType();
return Result.Failure(
UE_UOL_PARSE_ERROR(InParams,
FText::Format(
LOCTEXT("Error_InvalidPayload", "Payload is invalid for fragment type {0}: {1}."),
FText::FromName(Type ? Type->FragmentTypeID : NAME_None),
FText::FromStringView(ThisPayload)
)
)
);
}
}
}
*this = MoveTemp(Tmp);
return Result.Success();
}
FUniversalObjectLocator FUniversalObjectLocator::FromString(FStringView InString, const FParseStringParams& InParams)
{
FUniversalObjectLocator Locator;
Locator.TryParseString(InString, InParams);
return Locator;
}
bool FUniversalObjectLocator::AddFragment(const UObject* Object, UObject* Context, UObject* StopAtContext)
{
using namespace UE::UniversalObjectLocator;
const FFragmentType* FragmentType = FindBestFragmentType(Object, Context);
if (!FragmentType)
{
return false;
}
// Initialize the payload
FUniversalObjectLocatorFragment NewLocator(*FragmentType);
FInitializeResult Result = FragmentType->InitializePayload(NewLocator.GetPayload(), FInitializeParams { Object, Context });
if (Result.Type == ELocatorType::Undefined)
{
return false;
}
// If the initialization needs to be relative to a different context, add a fragment for NewContext as well
if (Result.Type == ELocatorType::Relative && Result.RelativeToContext != Context)
{
if (ensureMsgf(Result.RelativeToContext, TEXT("Payload initialization reported a relative locator but did not specify what it is relative to.")))
{
if (StopAtContext != Result.RelativeToContext)
{
AddFragment(Result.RelativeToContext, nullptr, StopAtContext);
}
}
}
// Now add ours onto the tail
Fragments.Emplace(MoveTemp(NewLocator));
return true;
}
bool FUniversalObjectLocator::SerializeFromMismatchedTag(const FPropertyTag& Tag, FStructuredArchive::FSlot Slot)
{
static const FName NAME_SoftObjectPath = "SoftObjectPath";
if (Tag.Type == NAME_SoftObjectProperty)
{
FSoftObjectPtr OldProperty;
Slot << OldProperty;
Fragments.Reset(1);
Fragments.Emplace(TUniversalObjectLocatorFragment<FDirectPathObjectLocator>(OldProperty.ToSoftObjectPath()));
return true;
}
else if (Tag.GetType().IsStruct(NAME_SoftObjectPath))
{
FSoftObjectPath OldPath;
Slot << OldPath;
Fragments.Reset(1);
Fragments.Emplace(TUniversalObjectLocatorFragment<FDirectPathObjectLocator>(MoveTemp(OldPath)));
return true;
}
return false;
}
bool FUniversalObjectLocator::ExportTextItem(FString& ValueStr, const FUniversalObjectLocator& DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) const
{
TStringBuilder<128> String;
ToString(String);
ValueStr.AppendChar('(');
ValueStr.Append(String.ToString(), String.Len());
ValueStr.AppendChar(')');
return true;
}
bool FUniversalObjectLocator::ImportTextItem(const TCHAR*& Buffer, int32 PortFlags, UObject* Parent, FOutputDevice* ErrorText, FArchive* InSerializingArchive)
{
using namespace UE::UniversalObjectLocator;
if (Buffer && *Buffer == '(')
{
const TCHAR* BufferEnd = FCString::Strchr(Buffer, ')');
if (Buffer != BufferEnd && (BufferEnd - Buffer) < std::numeric_limits<int32>::max())
{
FStringView View(Buffer + 1, int32(BufferEnd - Buffer) - 1);
if (TryParseString(View, FParseStringParams()))
{
// ImportText parsing requires that we increment the buffer ptr beyond the closing ')' on success
Buffer = BufferEnd + 1;
return true;
}
}
}
// Try and parse this as a soft object path
FSoftObjectPath Path;
if (Path.ImportTextItem(Buffer, PortFlags, Parent, ErrorText, InSerializingArchive))
{
UObject* Object = Path.ResolveObject();
Reset(Object);
return true;
}
return false;
}
#undef LOCTEXT_NAMESPACE