Files
UnrealEngine/Engine/Plugins/Editor/AssetSearch/Source/Private/Providers/AssetRegistrySearchProvider.cpp
2025-05-18 13:04:45 +08:00

381 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetRegistrySearchProvider.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Misc/TextFilterExpressionEvaluator.h"
#include "Async/Async.h"
#include "Algo/LevenshteinDistance.h"
/** Mapping of asset property tag aliases that can be used by text searches */
class FFrontendFilter_AssetPropertyTagAliases_HackCopy
{
public:
static FFrontendFilter_AssetPropertyTagAliases_HackCopy& Get()
{
static FFrontendFilter_AssetPropertyTagAliases_HackCopy Singleton;
return Singleton;
}
/** Get the source tag for the given asset data and alias, or none if there is no match */
FName GetSourceTagFromAlias(const FAssetData& InAssetData, const FName InAlias)
{
TSharedPtr<TMap<FName, FName>>& AliasToSourceTagMapping = ClassToAliasTagsMapping.FindOrAdd(InAssetData.AssetClassPath);
if (!AliasToSourceTagMapping.IsValid())
{
static const FName NAME_DisplayName(TEXT("DisplayName"));
AliasToSourceTagMapping = MakeShared<TMap<FName, FName>>();
UClass* AssetClass = InAssetData.GetClass();
if (AssetClass)
{
TMap<FName, UObject::FAssetRegistryTagMetadata> AssetTagMetaData;
AssetClass->GetDefaultObject()->GetAssetRegistryTagMetadata(AssetTagMetaData);
for (const auto& AssetTagMetaDataPair : AssetTagMetaData)
{
if (!AssetTagMetaDataPair.Value.DisplayName.IsEmpty())
{
const FName DisplayName = MakeObjectNameFromDisplayLabel(AssetTagMetaDataPair.Value.DisplayName.ToString(), NAME_None);
AliasToSourceTagMapping->Add(DisplayName, AssetTagMetaDataPair.Key);
}
}
for (const auto& KeyValuePair : InAssetData.TagsAndValues)
{
if (FProperty* Field = FindFProperty<FProperty>(AssetClass, KeyValuePair.Key))
{
if (Field->HasMetaData(NAME_DisplayName))
{
const FName DisplayName = MakeObjectNameFromDisplayLabel(Field->GetMetaData(NAME_DisplayName), NAME_None);
AliasToSourceTagMapping->Add(DisplayName, KeyValuePair.Key);
}
}
}
}
}
return AliasToSourceTagMapping.IsValid() ? AliasToSourceTagMapping->FindRef(InAlias) : NAME_None;
}
private:
/** Mapping from class name -> (alias -> source) */
TMap<FTopLevelAssetPath, TSharedPtr<TMap<FName, FName>>> ClassToAliasTagsMapping;
};
/** Expression context to test the given asset data against the current text filter */
class FFrontendFilter_TextFilterExpressionContext_HackCopy : public ITextFilterExpressionContext
{
public:
typedef TRemoveReference<const FAssetData&>::Type* FAssetFilterTypePtr;
FFrontendFilter_TextFilterExpressionContext_HackCopy()
: AssetPtr(nullptr)
, bIncludeClassName(true)
, bIncludeAssetPath(false)
, bIncludeCollectionNames(true)
, NameKeyName("Name")
, PathKeyName("Path")
, ClassKeyName("Class")
, TypeKeyName("Type")
, CollectionKeyName("Collection")
, TagKeyName("Tag")
{
}
void SetAsset(FAssetFilterTypePtr InAsset)
{
AssetPtr = InAsset;
if (bIncludeAssetPath)
{
// Get the full asset path, and also split it so we can compare each part in the filter
AssetPtr->PackageName.AppendString(AssetFullPath);
AssetFullPath.ParseIntoArray(AssetSplitPath, TEXT("/"));
AssetFullPath.ToUpperInline();
if (bIncludeClassName)
{
// Get the full export text path as people sometimes search by copying this (requires class and asset path search to be enabled in order to match)
AssetPtr->GetExportTextName(AssetExportTextName);
AssetExportTextName.ToUpperInline();
}
}
}
void ClearAsset()
{
AssetPtr = nullptr;
AssetFullPath.Reset();
AssetExportTextName.Reset();
AssetSplitPath.Reset();
AssetCollectionNames.Reset();
}
void SetIncludeClassName(const bool InIncludeClassName)
{
bIncludeClassName = InIncludeClassName;
}
bool GetIncludeClassName() const
{
return bIncludeClassName;
}
void SetIncludeAssetPath(const bool InIncludeAssetPath)
{
bIncludeAssetPath = InIncludeAssetPath;
}
bool GetIncludeAssetPath() const
{
return bIncludeAssetPath;
}
void SetIncludeCollectionNames(const bool InIncludeCollectionNames)
{
bIncludeCollectionNames = InIncludeCollectionNames;
}
bool GetIncludeCollectionNames() const
{
return bIncludeCollectionNames;
}
virtual bool TestBasicStringExpression(const FTextFilterString& InValue, const ETextFilterTextComparisonMode InTextComparisonMode) const override
{
if (InValue.CompareName(AssetPtr->AssetName, InTextComparisonMode))
{
return true;
}
if (bIncludeAssetPath)
{
if (InValue.CompareFString(AssetFullPath, InTextComparisonMode))
{
return true;
}
for (const FString& AssetPathPart : AssetSplitPath)
{
if (InValue.CompareFString(AssetPathPart, InTextComparisonMode))
{
return true;
}
}
}
if (bIncludeClassName)
{
if (InValue.CompareFString(AssetPtr->AssetClassPath.ToString(), InTextComparisonMode))
{
return true;
}
}
if (bIncludeClassName && bIncludeAssetPath)
{
// Only test this if we're searching the class name and asset path too, as the exported text contains the type and path in the string
if (InValue.CompareFString(AssetExportTextName, InTextComparisonMode))
{
return true;
}
}
if (bIncludeCollectionNames)
{
for (const FName& AssetCollectionName : AssetCollectionNames)
{
if (InValue.CompareName(AssetCollectionName, InTextComparisonMode))
{
return true;
}
}
}
return false;
}
virtual bool TestComplexExpression(const FName& InKey, const FTextFilterString& InValue, const ETextFilterComparisonOperation InComparisonOperation, const ETextFilterTextComparisonMode InTextComparisonMode) const override
{
// Special case for the asset name, as this isn't contained within the asset registry meta-data
if (InKey == NameKeyName)
{
// Names can only work with Equal or NotEqual type tests
if (InComparisonOperation != ETextFilterComparisonOperation::Equal && InComparisonOperation != ETextFilterComparisonOperation::NotEqual)
{
return false;
}
const bool bIsMatch = TextFilterUtils::TestBasicStringExpression(AssetPtr->AssetName, InValue, InTextComparisonMode);
return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bIsMatch : !bIsMatch;
}
// Special case for the asset path, as this isn't contained within the asset registry meta-data
if (InKey == PathKeyName)
{
// Paths can only work with Equal or NotEqual type tests
if (InComparisonOperation != ETextFilterComparisonOperation::Equal && InComparisonOperation != ETextFilterComparisonOperation::NotEqual)
{
return false;
}
// If the comparison mode is partial, then we only need to test the ObjectPath as that contains the other two as sub-strings
bool bIsMatch = false;
if (InTextComparisonMode == ETextFilterTextComparisonMode::Partial)
{
bIsMatch = TextFilterUtils::TestBasicStringExpression(AssetPtr->GetObjectPathString(), InValue, InTextComparisonMode);
}
else
{
bIsMatch = TextFilterUtils::TestBasicStringExpression(AssetPtr->GetObjectPathString(), InValue, InTextComparisonMode)
|| TextFilterUtils::TestBasicStringExpression(AssetPtr->PackageName, InValue, InTextComparisonMode)
|| TextFilterUtils::TestBasicStringExpression(AssetPtr->PackagePath, InValue, InTextComparisonMode);
}
return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bIsMatch : !bIsMatch;
}
// Special case for the asset type, as this isn't contained within the asset registry meta-data
if (InKey == ClassKeyName || InKey == TypeKeyName)
{
// Class names can only work with Equal or NotEqual type tests
if (InComparisonOperation != ETextFilterComparisonOperation::Equal && InComparisonOperation != ETextFilterComparisonOperation::NotEqual)
{
return false;
}
const bool bIsMatch = TextFilterUtils::TestBasicStringExpression(AssetPtr->AssetClassPath.ToString(), InValue, InTextComparisonMode);
return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bIsMatch : !bIsMatch;
}
// Special case for collections, as these aren't contained within the asset registry meta-data
if (InKey == CollectionKeyName || InKey == TagKeyName)
{
// Collections can only work with Equal or NotEqual type tests
if (InComparisonOperation != ETextFilterComparisonOperation::Equal && InComparisonOperation != ETextFilterComparisonOperation::NotEqual)
{
return false;
}
bool bFoundMatch = false;
for (const FName& AssetCollectionName : AssetCollectionNames)
{
if (TextFilterUtils::TestBasicStringExpression(AssetCollectionName, InValue, InTextComparisonMode))
{
bFoundMatch = true;
break;
}
}
return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bFoundMatch : !bFoundMatch;
}
// Generic handling for anything in the asset meta-data
{
auto GetMetaDataValue = [this, &InKey](FString& OutMetaDataValue) -> bool
{
// Check for a literal key
if (AssetPtr->GetTagValue(InKey, OutMetaDataValue))
{
return true;
}
// Check for an alias key
const FName LiteralKey = FFrontendFilter_AssetPropertyTagAliases_HackCopy::Get().GetSourceTagFromAlias(*AssetPtr, InKey);
if (!LiteralKey.IsNone() && AssetPtr->GetTagValue(LiteralKey, OutMetaDataValue))
{
return true;
}
return false;
};
FString MetaDataValue;
if (GetMetaDataValue(MetaDataValue))
{
return TextFilterUtils::TestComplexExpression(MetaDataValue, InValue, InComparisonOperation, InTextComparisonMode);
}
}
return false;
}
private:
/** Pointer to the asset we're currently filtering */
FAssetFilterTypePtr AssetPtr;
/** Full path of the current asset */
FString AssetFullPath;
/** The export text name of the current asset */
FString AssetExportTextName;
/** Split path of the current asset */
TArray<FString> AssetSplitPath;
/** Names of the collections that the current asset is in */
TArray<FName> AssetCollectionNames;
/** Are we supposed to include the class name in our basic string tests? */
bool bIncludeClassName;
/** Search inside the entire asset path? */
bool bIncludeAssetPath;
/** Search collection names? */
bool bIncludeCollectionNames;
/** Keys used by TestComplexExpression */
const FName NameKeyName;
const FName PathKeyName;
const FName ClassKeyName;
const FName TypeKeyName;
const FName CollectionKeyName;
const FName TagKeyName;
};
void FAssetRegistrySearchProvider::Search(FSearchQueryPtr SearchQuery)
{
Async(EAsyncExecution::LargeThreadPool, [SearchQuery]() mutable {
TRACE_CPUPROFILER_EVENT_SCOPE_STR("FAssetRegistrySearchProvider Async Search");
const IAssetRegistry& Registry = FAssetRegistryModule::GetRegistry();
// Start by gathering all assets.
TArray<FAssetData> Assets;
Registry.GetAllAssets(Assets, true /* bIncludeOnlyOnDiskAssets */ );
FTextFilterExpressionEvaluator TextFilterExpressionEvaluator(ETextFilterExpressionEvaluatorMode::Complex);
TextFilterExpressionEvaluator.SetFilterText(FText::FromString(SearchQuery->QueryText));
FFrontendFilter_TextFilterExpressionContext_HackCopy TextFilterExpressionContext;
TArray<FSearchRecord> SearchResults;
for (auto AssetIter = Assets.CreateIterator(); AssetIter; ++AssetIter)
{
const FAssetData& Asset = *AssetIter;
TextFilterExpressionContext.SetAsset(&Asset);
if (TextFilterExpressionEvaluator.TestTextFilter(TextFilterExpressionContext))
{
FSearchRecord Record;
Record.AssetPath = Asset.GetObjectPathString();
Record.AssetName = Asset.AssetName.ToString();
Record.AssetClass = Asset.AssetClassPath;
const float WorstCase = Record.AssetName.Len() + SearchQuery->QueryText.Len();
Record.Score = -50.0f * (1.0f - (Algo::LevenshteinDistance(Record.AssetName.ToLower(), SearchQuery->QueryText.ToLower()) / WorstCase));
SearchResults.Add(Record);
}
TextFilterExpressionContext.ClearAsset();
}
Async(EAsyncExecution::TaskGraphMainThread, [SearchQuery, SearchResults = MoveTemp(SearchResults)]() mutable {
if (FSearchQuery::ResultsCallbackFunction ResultsCallback = SearchQuery->GetResultsCallback())
{
ResultsCallback(MoveTemp(SearchResults));
}
});
});
}