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

1239 lines
35 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetSearchManager.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "DerivedDataCacheInterface.h"
#include "IAssetSearchModule.h"
#include "Misc/Paths.h"
#include "HAL/RunnableThread.h"
#include "Misc/App.h"
#include "StudioTelemetry.h"
#include "AnalyticsEventAttribute.h"
#include "Misc/PackageName.h"
#include "WidgetBlueprint.h"
#include "Engine/DataTable.h"
#include "Engine/DataAsset.h"
#include "SearchSerializer.h"
#include "Sound/DialogueWave.h"
#include "Settings/SearchProjectSettings.h"
#include "Settings/SearchUserSettings.h"
#include "Sound/SoundCue.h"
#include "Misc/ScopedSlowTask.h"
#include "Editor.h"
#include "UObject/AssetRegistryTagsContext.h"
#include "UObject/ObjectSaveContext.h"
#include "FileHelpers.h"
#include "Misc/MessageDialog.h"
#include "Engine/CurveTable.h"
#include "Materials/Material.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialParameterCollection.h"
#include "Materials/MaterialInstance.h"
#include "Hash/CityHash.h"
#include "Indexers/GenericObjectIndexer.h"
#include "Indexers/DataTableIndexer.h"
#include "Indexers/BlueprintIndexer.h"
#include "Indexers/WidgetBlueprintIndexer.h"
#include "Indexers/CurveTableIndexer.h"
#include "Indexers/DialogueWaveIndexer.h"
#include "Indexers/LevelIndexer.h"
#include "Indexers/ActorIndexer.h"
#include "Indexers/SoundCueIndexer.h"
#include "Indexers/MaterialExpressionIndexer.h"
#include "Providers/AssetRegistrySearchProvider.h"
#define LOCTEXT_NAMESPACE "FAssetSearchManager"
static bool bForceEnableSearch = false;
FAutoConsoleVariableRef CVarDisableUniversalSearch(
TEXT("Search.ForceEnable"),
bForceEnableSearch,
TEXT("Enable universal search")
);
static bool bTryIndexAssetsOnLoad = false;
FAutoConsoleVariableRef CVarTryIndexAssetsOnLoad(
TEXT("Search.TryIndexAssetsOnLoad"),
bTryIndexAssetsOnLoad,
TEXT("Tries to index assets on load.")
);
static bool bTryToGCDuringMissingIndexing = false;
FAutoConsoleVariableRef CVarTryToGCDuringMissingIndexing(
TEXT("Search.TryToGCDuringMissingIndexing"),
bTryToGCDuringMissingIndexing,
TEXT("Tries to GC occasionally while indexing missing items.")
);
//DEFINE_LOG_CATEGORY_STATIC(LogAssetSearch, Log, All);
class FUnloadPackageScope
{
public:
FUnloadPackageScope()
{
if (bTryToGCDuringMissingIndexing)
{
FCoreUObjectDelegates::OnAssetLoaded.AddRaw(this, &FUnloadPackageScope::OnAssetLoaded);
}
}
~FUnloadPackageScope()
{
if (bTryToGCDuringMissingIndexing)
{
FCoreUObjectDelegates::OnAssetLoaded.RemoveAll(this);
TryUnload(true);
}
}
int32 TryUnload(bool bResetTrackedObjects)
{
if (!bTryToGCDuringMissingIndexing)
{
return 0;
}
TArray<TWeakObjectPtr<UObject>> PackageObjectPtrs;
for (const FObjectKey& LoadedObjectKey : ObjectsLoaded)
{
if (UObject* LoadedObject = LoadedObjectKey.ResolveObjectPtr())
{
UPackage* Package = LoadedObject->GetOutermost();
TArray<UObject*> PackageObjects;
GetObjectsWithOuter(Package, PackageObjects, false);
for (UObject* PackageObject : PackageObjects)
{
PackageObject->ClearFlags(RF_Standalone);
PackageObjectPtrs.Add(PackageObject);
}
}
}
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
int32 NumRemoved = 0;
for (int32 LoadedAssetIndex = 0; LoadedAssetIndex < PackageObjectPtrs.Num(); LoadedAssetIndex++)
{
TWeakObjectPtr<UObject> PackageObjectPtr = PackageObjectPtrs[LoadedAssetIndex];
if (UObject* LoadedObject = PackageObjectPtr.Get())
{
//FReferencerInformationList ReferencesIncludingUndo;
//bool bReferencedInMemoryOrUndoStack = IsReferenced(LoadedObject, GARBAGE_COLLECTION_KEEPFLAGS, EInternalObjectFlags_GarbageCollectionKeepFlags, true, &ReferencesIncludingUndo);
LoadedObject->SetFlags(RF_Standalone);
}
else
{
NumRemoved++;
}
}
if (bResetTrackedObjects)
{
ObjectsLoaded.Reset();
}
else
{
for (int32 ObjectIndex = 0; ObjectIndex < ObjectsLoaded.Num(); ObjectIndex++)
{
if (!ObjectsLoaded[ObjectIndex].ResolveObjectPtr())
{
ObjectsLoaded.RemoveAt(ObjectIndex);
ObjectIndex--;
}
}
}
return NumRemoved;
}
int32 GetObjectsLoaded() const
{
return ObjectsLoaded.Num();
}
private:
void OnAssetLoaded(UObject* InObject)
{
ObjectsLoaded.Add(FObjectKey(InObject));
}
private:
TArray<FObjectKey> ObjectsLoaded;
TArray<UClass*> ClassFilters;
};
const FName FAssetSearchManager::AssetSearchIndexVersionTag(TEXT("ASI_Version"));
const FName FAssetSearchManager::AssetSearchIndexHashTag(TEXT("ASI_Hash"));
const FName FAssetSearchManager::AssetSearchIndexDataTag(TEXT("ASI_Data"));
FAssetSearchManager::FAssetSearchManager()
{
PendingDatabaseUpdates = 0;
IsAssetUpToDateCount = 0;
ActiveDownloads = 0;
DownloadQueueCount = 0;
TotalSearchRecords = 0;
LastRecordCountUpdateSeconds = 0;
IntermediateStorage = GetDefault<USearchProjectSettings>()->IntermediateStorage;
RunThread = false;
}
FAssetSearchManager::~FAssetSearchManager()
{
RunThread = false;
if (DatabaseThread)
{
DatabaseThread->WaitForCompletion();
}
StopScanningAssets();
UPackage::PackageSavedWithContextEvent.RemoveAll(this);
FCoreUObjectDelegates::OnAssetLoaded.RemoveAll(this);
UObject::FAssetRegistryTag::OnGetExtraObjectTagsWithContext.RemoveAll(this);
FTSTicker::GetCoreTicker().RemoveTicker(TickerHandle);
}
void FAssetSearchManager::Start()
{
RegisterAssetIndexer(UDataAsset::StaticClass(), MakeUnique<FGenericObjectIndexer>("DataAsset"));
RegisterAssetIndexer(UDataTable::StaticClass(), MakeUnique<FDataTableIndexer>());
RegisterAssetIndexer(UCurveTable::StaticClass(), MakeUnique<FCurveTableIndexer>());
RegisterAssetIndexer(UBlueprint::StaticClass(), MakeUnique<FBlueprintIndexer>());
RegisterAssetIndexer(UWidgetBlueprint::StaticClass(), MakeUnique<FWidgetBlueprintIndexer>());
RegisterAssetIndexer(UDialogueWave::StaticClass(), MakeUnique<FDialogueWaveIndexer>());
RegisterAssetIndexer(UWorld::StaticClass(), MakeUnique<FLevelIndexer>());
RegisterAssetIndexer(AActor::StaticClass(), MakeUnique<FActorIndexer>());
RegisterAssetIndexer(USoundCue::StaticClass(), MakeUnique<FSoundCueIndexer>());
RegisterAssetIndexer(UMaterial::StaticClass(), MakeUnique<FMaterialExpressionIndexer>("Material"));
RegisterAssetIndexer(UMaterialFunction::StaticClass(), MakeUnique<FMaterialExpressionIndexer>("MaterialFunction"));
RegisterAssetIndexer(UMaterialParameterCollection::StaticClass(), MakeUnique<FGenericObjectIndexer>("MaterialParameterCollection"));
RegisterAssetIndexer(UMaterialInstance::StaticClass(), MakeUnique<FGenericObjectIndexer>("MaterialInstance"));
RegisterSearchProvider(TEXT("AssetRegistry"), MakeUnique<FAssetRegistrySearchProvider>());
if (IntermediateStorage == ESearchIntermediateStorage::DerivedDataCache)
{
UPackage::PackageSavedWithContextEvent.AddRaw(this, &FAssetSearchManager::HandlePackageSaved);
}
FCoreUObjectDelegates::OnAssetLoaded.AddRaw(this, &FAssetSearchManager::OnAssetLoaded);
TickerHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FAssetSearchManager::Tick_GameThread), 0);
SearchDatabase.bEnableIntegrityChecks = GetDefault<USearchUserSettings>()->bEnableIntegrityChecks;
FileInfoDatabase.bEnableIntegrityChecks = GetDefault<USearchUserSettings>()->bEnableIntegrityChecks;
RunThread = true;
DatabaseThread = FRunnableThread::Create(this, TEXT("UniversalSearch"), 0, TPri_BelowNormal);
if (IntermediateStorage == ESearchIntermediateStorage::AssetTagData)
{
UObject::FAssetRegistryTag::OnGetExtraObjectTagsWithContext.AddRaw(this, &FAssetSearchManager::HandleOnGetExtraObjectTags);
}
}
void FAssetSearchManager::UpdateScanningAssets()
{
bool bTargetState = GetDefault<USearchUserSettings>()->bEnableSearch;
if (GIsBuildMachine || FApp::IsUnattended())
{
bTargetState = false;
}
if (bForceEnableSearch)
{
bTargetState = true;
}
if (bTargetState != bStarted)
{
bStarted = bTargetState;
if (bTargetState)
{
StartScanningAssets();
}
else
{
StopScanningAssets();
}
}
}
void FAssetSearchManager::StartScanningAssets()
{
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
if (AssetRegistry.IsLoadingAssets())
{
AssetRegistry.OnFilesLoaded().AddRaw(this, &FAssetSearchManager::OnAssetScanFinished);
}
else
{
AssetRegistry.OnAssetAdded().AddRaw(this, &FAssetSearchManager::OnAssetAdded);
AssetRegistry.OnAssetRemoved().AddRaw(this, &FAssetSearchManager::OnAssetRemoved);
TArray<FAssetData> TempAssetData;
AssetRegistry.GetAllAssets(TempAssetData, true);
for (const FAssetData& Data : TempAssetData)
{
OnAssetAdded(Data);
}
}
}
void FAssetSearchManager::StopScanningAssets()
{
if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr<FAssetRegistryModule>("AssetRegistry"))
{
IAssetRegistry* AssetRegistry = AssetRegistryModule->TryGet();
if (AssetRegistry)
{
AssetRegistry->OnAssetAdded().RemoveAll(this);
AssetRegistry->OnAssetRemoved().RemoveAll(this);
AssetRegistry->OnFilesLoaded().RemoveAll(this);
}
}
ProcessAssetQueue.Reset();
AssetNeedingReindexing.Reset();
}
void FAssetSearchManager::TryConnectToDatabase()
{
if (!bDatabaseOpen)
{
check(!IsInGameThread());
if ((FPlatformTime::Seconds() - LastConnectionAttempt) > 30)
{
LastConnectionAttempt = FPlatformTime::Seconds();
const FString SessionPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Search")));
if (!FileInfoDatabase.IsValid())
{
FScopeLock ScopedLock(&FileInfoDatabaseCS);
if (!FileInfoDatabase.Open(SessionPath))
{
return;
}
}
if (!SearchDatabase.IsValid())
{
FScopeLock ScopedLock(&SearchDatabaseCS);
if (!SearchDatabase.Open(SessionPath))
{
return;
}
}
bDatabaseOpen = true;
}
}
}
FSearchStats FAssetSearchManager::GetStats() const
{
FSearchStats Stats;
Stats.Scanning = ProcessAssetQueue.Num();
Stats.Processing = IsAssetUpToDateCount + DownloadQueueCount + ActiveDownloads;
Stats.Updating = PendingDatabaseUpdates;
Stats.TotalRecords = TotalSearchRecords;
Stats.AssetsMissingIndex = AssetNeedingReindexing.Num();
return Stats;
}
void FAssetSearchManager::RegisterAssetIndexer(const UClass* AssetClass, TUniquePtr<IAssetIndexer>&& Indexer)
{
FScopeLock Lock(&AssetSearchManagerCS);
Indexers.Add(AssetClass->GetFName(), MoveTemp(Indexer));
}
void FAssetSearchManager::RegisterSearchProvider(FName SearchProviderName, TUniquePtr<ISearchProvider>&& InSearchProvider)
{
FScopeLock Lock(&AssetSearchManagerCS);
SearchProviders.Add(SearchProviderName, MoveTemp(InSearchProvider));
}
void FAssetSearchManager::OnAssetAdded(const FAssetData& InAssetData)
{
FScopeLock Lock(&AssetSearchManagerCS);
static const FString EngineContentPathWithSlash = FPackageName::FilenameToLongPackageName(FPaths::EngineContentDir());
static const FString DeveloperPathWithSlash = FPackageName::FilenameToLongPackageName(FPaths::GameDevelopersDir());
static const FString UsersDeveloperPathWithSlash = FPackageName::FilenameToLongPackageName(FPaths::GameUserDeveloperDir());
const FString PackageName = InAssetData.PackageName.ToString();
const FString PackageMountPoint = FPackageName::GetPackageMountPoint(PackageName).ToString();
FString PackageFilename;
FPackageName::TryConvertLongPackageNameToFilename(PackageName, PackageFilename);
// Don't index things in the engine directory if we're storing data in asset tags, since we don't want to mutate the engine data.
if (IntermediateStorage == ESearchIntermediateStorage::AssetTagData)
{
if (PackageName.StartsWith(EngineContentPathWithSlash) ||
FPaths::IsUnderDirectory(PackageFilename, FPaths::EnginePluginsDir()))
{
return;
}
}
// Don't process stuff in the other developer folders.
if (PackageName.StartsWith(DeveloperPathWithSlash))
{
if (!PackageName.StartsWith(UsersDeveloperPathWithSlash))
{
return;
}
}
// Ignore it if the package is in one of the project ignored paths.
const USearchProjectSettings* ProjectSettings = GetDefault<USearchProjectSettings>();
for (const FDirectoryPath& IgnoredPath : ProjectSettings->IgnoredPaths)
{
if (PackageName.StartsWith(IgnoredPath.Path))
{
return;
}
}
// Ignore it if the package is in one of the user ignored paths.
const USearchUserSettings* UserSettings = GetDefault<USearchUserSettings>();
for (const FDirectoryPath& IgnoredPath : UserSettings->IgnoredPaths)
{
if (PackageName.StartsWith(IgnoredPath.Path))
{
return;
}
}
// Don't index redirectors, just act like they don't exist.
if (InAssetData.IsRedirector())
{
return;
}
FAssetOperation Operation;
Operation.Asset = InAssetData;
ProcessAssetQueue.Add(Operation);
}
void FAssetSearchManager::OnAssetRemoved(const FAssetData& InAssetData)
{
FScopeLock Lock(&AssetSearchManagerCS);
FAssetOperation Operation;
Operation.Asset = InAssetData;
Operation.bRemoval = true;
ProcessAssetQueue.Add(Operation);
}
void FAssetSearchManager::OnAssetScanFinished()
{
FScopeLock Lock(&AssetSearchManagerCS);
TArray<FAssetData> AllAssets;
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
AssetRegistry.OnFilesLoaded().RemoveAll(this);
AssetRegistry.OnAssetAdded().AddRaw(this, &FAssetSearchManager::OnAssetAdded);
AssetRegistry.OnAssetRemoved().AddRaw(this, &FAssetSearchManager::OnAssetRemoved);
AssetRegistry.GetAllAssets(AllAssets, true);
for (const FAssetData& Data : AllAssets)
{
OnAssetAdded(Data);
}
PendingDatabaseUpdates++;
UpdateOperations.Enqueue([this, AssetsAvailable = MoveTemp(AllAssets)]() mutable {
FScopeLock ScopedLock(&SearchDatabaseCS);
//UE_LOG(LogAssetSearch, Log, TEXT(""));
SearchDatabase.RemoveAssetsNotInThisSet(AssetsAvailable);
PendingDatabaseUpdates--;
});
}
uint64 FAssetSearchManager::GetTextHash(FStringView PackageRelativeExportPath) const
{
if (PackageRelativeExportPath.Len() == 0)
{
return 0;
}
return CityHash64(reinterpret_cast<const char*>(PackageRelativeExportPath.GetData() + 1), (PackageRelativeExportPath.Len() - 1) * sizeof(TCHAR));
}
void FAssetSearchManager::HandleOnGetExtraObjectTags(FAssetRegistryTagsContext Context)
{
if (!Context.IsFullUpdate())
{
return;
}
const UObject* Object = Context.GetObject();
const ITargetPlatform* TargetPlatform = Context.GetTargetPlatform();
if (GIsEditor && !Object->GetOutermost()->HasAnyPackageFlags(PKG_ForDiffing) && !IsRunningCookCommandlet())
{
if (!GEditor->IsAutosaving())
{
if (!TargetPlatform || TargetPlatform->HasEditorOnlyData())
{
FScopeLock Lock(&AssetSearchManagerCS);
FAssetData InAssetData(Object, FAssetData::ECreationFlags::SkipAssetRegistryTagsGathering);
FString IndexedJson;
const bool bWasIndexed = IndexAsset(InAssetData, Object, IndexedJson);
if (bWasIndexed && !IndexedJson.IsEmpty())
{
const FString IndexVersion = GetBaseIndexKey(Object->GetClass());
const FString IndexHash = LexToString(GetTextHash(IndexedJson));
Context.AddTag(UObject::FAssetRegistryTag(FAssetSearchManager::AssetSearchIndexVersionTag, IndexVersion, UObject::FAssetRegistryTag::TT_Hidden));
Context.AddTag(UObject::FAssetRegistryTag(FAssetSearchManager::AssetSearchIndexHashTag, IndexHash, UObject::FAssetRegistryTag::TT_Hidden));
Context.AddTag(UObject::FAssetRegistryTag(FAssetSearchManager::AssetSearchIndexDataTag, IndexedJson, UObject::FAssetRegistryTag::TT_Hidden));
if (!IndexedJson.IsEmpty())
{
AddOrUpdateAsset(InAssetData, IndexedJson, IndexHash);
}
}
}
}
}
}
void FAssetSearchManager::HandlePackageSaved(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext ObjectSaveContext)
{
check(IntermediateStorage == ESearchIntermediateStorage::DerivedDataCache);
// Only execute if this is a user save
if (ObjectSaveContext.IsProceduralSave())
{
return;
}
if (GIsEditor && !IsRunningCommandlet())
{
FScopeLock Lock(&AssetSearchManagerCS);
TArray<UObject*> Objects;
const bool bIncludeNestedObjects = false;
GetObjectsWithPackage(Package, Objects, bIncludeNestedObjects);
for (UObject* Entry : Objects)
{
RequestIndexAsset_DDC(Entry);
}
}
}
void FAssetSearchManager::OnAssetLoaded(UObject* InObject)
{
if (bTryIndexAssetsOnLoad)
{
switch (IntermediateStorage)
{
case ESearchIntermediateStorage::DerivedDataCache:
RequestIndexAsset_DDC(InObject);
break;
case ESearchIntermediateStorage::AssetTagData:
// Don't need to do this on load, I mean, it's a little more accurate but if we're storing it
// in tag data, it's almost certainly up to date unless someone changed the serialization.
break;
}
}
}
bool FAssetSearchManager::RequestIndexAsset_DDC(const UObject* InAsset)
{
FScopeLock Lock(&AssetSearchManagerCS);
check(IntermediateStorage == ESearchIntermediateStorage::DerivedDataCache);
if (GEditor == nullptr || GEditor->IsAutosaving())
{
return false;
}
if (IsAssetIndexable(InAsset))
{
TWeakObjectPtr<const UObject> AssetWeakPtr = InAsset;
FAssetData AssetData(InAsset);
return AsyncGetDerivedDataKey(AssetData, [this, AssetData, AssetWeakPtr](bool bSuccess, FString InDDCKey) {
if (!bSuccess)
{
return;
}
UpdateOperations.Enqueue([this, AssetData, AssetWeakPtr, InDDCKey]() {
FScopeLock ScopedLock(&SearchDatabaseCS);
if (!SearchDatabase.IsAssetUpToDate(AssetData, InDDCKey))
{
AsyncMainThreadTask([this, AssetWeakPtr]() {
StoreIndexForAsset_DDC(AssetWeakPtr.Get());
});
}
});
});
}
return false;
}
bool FAssetSearchManager::IsAssetIndexable(const UObject* InAsset) const
{
if (InAsset && InAsset->IsAsset())
{
// If it's not a permanent package, and one we just loaded for diffing, don't index it.
UPackage* Package = InAsset->GetOutermost();
if (Package->HasAnyPackageFlags(/*LOAD_ForDiff | */LOAD_PackageForPIE | LOAD_ForFileDiff))
{
return false;
}
if (InAsset->HasAnyFlags(RF_Transient))
{
return false;
}
return true;
}
return false;
}
bool FAssetSearchManager::TryLoadIndexForAsset(const FAssetData& InAssetData)
{
switch (IntermediateStorage)
{
case ESearchIntermediateStorage::DerivedDataCache:
return TryLoadIndexForAsset_DDC(InAssetData);
case ESearchIntermediateStorage::AssetTagData:
return TryLoadIndexForAsset_Tags(InAssetData);
default:
ensure(false);
return false;
}
}
bool FAssetSearchManager::TryLoadIndexForAsset_Tags(const FAssetData& InAssetData)
{
FScopeLock Lock(&AssetSearchManagerCS);
check(IntermediateStorage == ESearchIntermediateStorage::AssetTagData);
IsAssetUpToDateCount++;
FString CurrentIndexerNameAndVersion = GetBaseIndexKey(InAssetData.GetClass());
if (!CurrentIndexerNameAndVersion.IsEmpty())
{
const FString SavedIndexerNameAndVersion = InAssetData.GetTagValueRef<FString>(FAssetSearchManager::AssetSearchIndexVersionTag);
const FString SavedIndexHash = InAssetData.GetTagValueRef<FString>(FAssetSearchManager::AssetSearchIndexHashTag);
if (!SavedIndexerNameAndVersion.IsEmpty())
{
if (SavedIndexerNameAndVersion != CurrentIndexerNameAndVersion)
{
// Log need to update/missing...etc.
AssetNeedingReindexing.Add(InAssetData);
}
UpdateOperations.Enqueue([this, InAssetData, SavedIndexHash]() {
FScopeLock ScopedLock(&SearchDatabaseCS);
if (!SearchDatabase.IsAssetUpToDate(InAssetData, SavedIndexHash))
{
AsyncMainThreadTask([this, InAssetData, SavedIndexHash]() {
const FString IndexedJson = InAssetData.GetTagValueRef<FString>(FAssetSearchManager::AssetSearchIndexDataTag);
IsAssetUpToDateCount--;
if (!IndexedJson.IsEmpty())
{
AddOrUpdateAsset(InAssetData, IndexedJson, SavedIndexHash);
}
});
}
else
{
IsAssetUpToDateCount--;
}
});
return true;
}
else
{
AssetNeedingReindexing.Add(InAssetData);
}
}
IsAssetUpToDateCount--;
return false;
}
bool FAssetSearchManager::TryLoadIndexForAsset_DDC(const FAssetData& InAssetData)
{
FScopeLock Lock(&AssetSearchManagerCS);
check(IntermediateStorage == ESearchIntermediateStorage::DerivedDataCache);
bool bAllowFetch = !GetDefault<USearchProjectSettings>()->bDisableDDC;
if (!bAllowFetch)
{
return false;
}
const bool bSuccess = AsyncGetDerivedDataKey(InAssetData, [this, InAssetData](bool bSuccess, FString InDDCKey) {
if (!bSuccess)
{
IsAssetUpToDateCount--;
return;
}
FeedOperations.Enqueue([this, InAssetData, InDDCKey]() {
FScopeLock ScopedLock(&SearchDatabaseCS);
if (!SearchDatabase.IsAssetUpToDate(InAssetData, InDDCKey))
{
AsyncRequestDownload(InAssetData, InDDCKey);
}
IsAssetUpToDateCount--;
});
});
if (bSuccess)
{
IsAssetUpToDateCount++;
}
return bSuccess;
}
void FAssetSearchManager::AsyncRequestDownload(const FAssetData& InAssetData, const FString& InDDCKey)
{
FScopeLock Lock(&AssetSearchManagerCS);
DownloadQueueCount++;
FAssetDDCRequest DDCRequest;
DDCRequest.AssetData = InAssetData;
DDCRequest.DDCKey = InDDCKey;
DownloadQueue.Enqueue(DDCRequest);
}
bool FAssetSearchManager::AsyncGetDerivedDataKey(const FAssetData& InAssetData, TFunction<void(bool, FString)> DDCKeyCallback)
{
FScopeLock Lock(&AssetSearchManagerCS);
check(IntermediateStorage == ESearchIntermediateStorage::DerivedDataCache);
const FString AssetPath = InAssetData.PackagePath.ToString();
if (AssetPath.Contains(FPackagePath::GetExternalActorsFolderName()) || AssetPath.Contains(FPackagePath::GetExternalObjectsFolderName()))
{
return false;
}
FString IndexersNamesAndVersions = GetIndexerVersion(InAssetData.GetClass());
// If the indexer names and versions is empty, then we know it's not possible to index this type of thing.
if (IndexersNamesAndVersions.IsEmpty())
{
return false;
}
UpdateOperations.Enqueue([this, InAssetData, IndexersNamesAndVersions, DDCKeyCallback]() {
FAssetFileInfo FileInfo;
{
FScopeLock ScopedLock(&FileInfoDatabaseCS);
FileInfoDatabase.AddOrUpdateFileInfo(InAssetData, FileInfo);
}
if (FileInfo.Hash.IsValid())
{
// The universal key for content is:
// AssetSearch_V{SerializerVersion}_{IndexersNamesAndVersions}_{ObjectPathHash}_{FileOnDiskHash}
const FString ObjectPathString = InAssetData.GetObjectPathString();
FSHAHash ObjectPathHash;
FSHA1::HashBuffer(*ObjectPathString, ObjectPathString.Len() * sizeof(FString::ElementType), ObjectPathHash.Hash);
TStringBuilder<512> DDCKey;
DDCKey.Append(TEXT("AssetSearch_V"));
DDCKey.Append(LexToString(FSearchSerializer::GetVersion()));
DDCKey.Append(TEXT("_"));
DDCKey.Append(IndexersNamesAndVersions);
DDCKey.Append(TEXT("_"));
DDCKey.Append(ObjectPathHash.ToString());
DDCKey.Append(TEXT("_"));
DDCKey.Append(LexToString(FileInfo.Hash));
const FString DDCKeyString = DDCKey.ToString();
DDCKeyCallback(true, DDCKeyString);
}
else
{
UE_LOG(LogAssetSearch, Warning, TEXT("%s unable to hash file."), *InAssetData.PackageName.ToString());
DDCKeyCallback(false, TEXT(""));
}
});
return true;
}
bool FAssetSearchManager::HasIndexerForClass(const UClass* InAssetClass) const
{
const UClass* IndexableClass = InAssetClass;
while (IndexableClass)
{
if (Indexers.Contains(IndexableClass->GetFName()))
{
return true;
}
IndexableClass = IndexableClass->GetSuperClass();
}
return false;
}
FString FAssetSearchManager::GetBaseIndexKey(const UClass* InAssetClass) const
{
TStringBuilder<256> VersionString;
const FString AssetIndexerVersions = GetIndexerVersion(InAssetClass);
if (!AssetIndexerVersions.IsEmpty())
{
VersionString.Append(TEXT("AssetSearch_V"));
VersionString.Append(LexToString(FSearchSerializer::GetVersion()));
VersionString.Append(TEXT("_"));
VersionString.Append(AssetIndexerVersions);
}
return VersionString.ToString();
}
FString FAssetSearchManager::GetIndexerVersion(const UClass* InAssetClass) const
{
TStringBuilder<256> VersionString;
TArray<UClass*> NestedIndexedTypes;
const UClass* IndexableClass = InAssetClass;
while (IndexableClass)
{
if (const TUniquePtr<IAssetIndexer>* IndexerPtr = Indexers.Find(IndexableClass->GetFName()))
{
IAssetIndexer* Indexer = IndexerPtr->Get();
VersionString.Append(Indexer->GetName());
VersionString.Append(TEXT("_"));
VersionString.Append(LexToString(Indexer->GetVersion()));
Indexer->GetNestedAssetTypes(NestedIndexedTypes);
}
IndexableClass = IndexableClass->GetSuperClass();
}
for (UClass* NestedIndexedType : NestedIndexedTypes)
{
VersionString.Append(GetIndexerVersion(NestedIndexedType));
}
return VersionString.ToString();
}
bool FAssetSearchManager::IndexAsset(const FAssetData& InAssetData, const UObject* InAsset, FString& OutIndexedJson) const
{
FScopeLock Lock(&AssetSearchManagerCS);
if (IsAssetIndexable(InAsset) && HasIndexerForClass(InAsset->GetClass()))
{
FSearchSerializer Serializer(InAssetData, &OutIndexedJson);
return Serializer.IndexAsset(InAsset, Indexers);
}
return false;
}
void FAssetSearchManager::StoreIndexForAsset(const UObject* InAsset)
{
switch (IntermediateStorage)
{
case ESearchIntermediateStorage::DerivedDataCache:
return StoreIndexForAsset_DDC(InAsset);
case ESearchIntermediateStorage::AssetTagData:
return StoreIndexForAsset_Tags(InAsset);
default:
ensure(false);
}
}
void FAssetSearchManager::StoreIndexForAsset_Tags(const UObject* InAsset)
{
FScopeLock Lock(&AssetSearchManagerCS);
check(IntermediateStorage == ESearchIntermediateStorage::AssetTagData);
// StoreIndexForAsset doesn't really do much of anything when it's stored in tag data, all we can really
// do is mark the package dirty so that it can be saved. There's really nothing else that need be done
// to upgrade it, since the index data was already grabbed from the tag data upon discovery.
InAsset->MarkPackageDirty();
}
void FAssetSearchManager::StoreIndexForAsset_DDC(const UObject* InAsset)
{
FScopeLock Lock(&AssetSearchManagerCS);
check(IntermediateStorage == ESearchIntermediateStorage::DerivedDataCache);
FString IndexedJson;
FAssetData InAssetData(InAsset, FAssetData::ECreationFlags::SkipAssetRegistryTagsGathering);
const bool bWasIndexed = IndexAsset(InAssetData, InAsset, IndexedJson);
if (bWasIndexed && !IndexedJson.IsEmpty())
{
AsyncGetDerivedDataKey(InAssetData, [this, InAssetData, IndexedJson](bool bSuccess, FString InDDCKey) {
if (!bSuccess)
{
return;
}
AsyncMainThreadTask([this, InAssetData, IndexedJson, InDDCKey]() {
check(IsInGameThread());
bool bAllowPut = !GetDefault<USearchProjectSettings>()->bDisableDDC;
if (bAllowPut)
{
FTCHARToUTF8 IndexedJsonUTF8(*IndexedJson);
TArrayView<const uint8> IndexedJsonUTF8View((const uint8*)IndexedJsonUTF8.Get(), IndexedJsonUTF8.Length() * sizeof(UTF8CHAR));
GetDerivedDataCacheRef().Put(*InDDCKey, IndexedJsonUTF8View, InAssetData.GetObjectPathString(), false);
}
AddOrUpdateAsset(InAssetData, IndexedJson, InDDCKey);
});
});
}
}
void FAssetSearchManager::LoadDDCContentIntoDatabase(const FAssetData& InAsset, const TArray<uint8>& Content, const FString& DerivedDataKey)
{
FUTF8ToTCHAR WByteBuffer((const ANSICHAR*)Content.GetData(), Content.Num());
FString IndexedJson = FString::ConstructFromPtrSize(WByteBuffer.Get(), WByteBuffer.Length());
AddOrUpdateAsset(InAsset, IndexedJson, DerivedDataKey);
}
void FAssetSearchManager::AddOrUpdateAsset(const FAssetData& InAssetData, const FString& IndexedJson, const FString& DerivedDataKey)
{
FScopeLock Lock(&AssetSearchManagerCS);
PendingDatabaseUpdates++;
UpdateOperations.Enqueue([this, InAssetData, IndexedJson, DerivedDataKey]() {
FScopeLock ScopedLock(&SearchDatabaseCS);
SearchDatabase.AddOrUpdateAsset(InAssetData, IndexedJson, DerivedDataKey);
PendingDatabaseUpdates--;
});
}
bool FAssetSearchManager::Tick_GameThread(float DeltaTime)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FAssetSearchManager_Tick);
FScopeLock Lock(&AssetSearchManagerCS);
check(IsInGameThread());
UpdateScanningAssets();
ProcessGameThreadTasks();
const USearchUserSettings* UserSettings = GetDefault<USearchUserSettings>();
const FSearchPerformance& PerformanceLimits = bForceEnableSearch ? UserSettings->DefaultOptions : UserSettings->GetPerformanceOptions();
int32 ScanLimit = PerformanceLimits.AssetScanRate;
while (ProcessAssetQueue.Num() > 0 && ScanLimit > 0)
{
FAssetOperation Operation = ProcessAssetQueue.Pop(EAllowShrinking::No);
FAssetData Asset = Operation.Asset;
if (Operation.bRemoval)
{
PendingDatabaseUpdates++;
UpdateOperations.Enqueue([this, Asset]() {
FScopeLock ScopedLock(&SearchDatabaseCS);
SearchDatabase.RemoveAsset(Asset);
PendingDatabaseUpdates--;
});
}
else
{
TryLoadIndexForAsset(Asset);
}
ScanLimit--;
}
while (!DownloadQueue.IsEmpty() && ActiveDownloads < PerformanceLimits.ParallelDownloads)
{
FAssetDDCRequest DDCRequest;
bool bSuccess = DownloadQueue.Dequeue(DDCRequest);
check(bSuccess);
DownloadQueueCount--;
ActiveDownloads++;
DDCRequest.DDCHandle = GetDerivedDataCacheRef().GetAsynchronous(*DDCRequest.DDCKey, DDCRequest.AssetData.GetObjectPathString());
ProcessDDCQueue.Enqueue(DDCRequest);
}
int32 MaxQueueProcesses = 1000;
int32 DownloadProcessLimit = PerformanceLimits.DownloadProcessRate;
while (!ProcessDDCQueue.IsEmpty() && DownloadProcessLimit > 0 && MaxQueueProcesses > 0)
{
const FAssetDDCRequest* PendingRequest = ProcessDDCQueue.Peek();
if (GetDerivedDataCacheRef().PollAsynchronousCompletion(PendingRequest->DDCHandle))
{
bool bDataWasBuilt;
TArray<uint8> OutContent;
bool bGetSuccessful = GetDerivedDataCacheRef().GetAsynchronousResults(PendingRequest->DDCHandle, OutContent, &bDataWasBuilt);
if (bGetSuccessful)
{
LoadDDCContentIntoDatabase(PendingRequest->AssetData, OutContent, PendingRequest->DDCKey);
DownloadProcessLimit--;
}
else if (UserSettings->bShowAssetsNeedingIndexing)
{
AssetNeedingReindexing.Add(PendingRequest->AssetData);
}
ProcessDDCQueue.Pop();
ActiveDownloads--;
MaxQueueProcesses--;
continue;
}
break;
}
if (bDatabaseOpen && ((FPlatformTime::Seconds() - LastRecordCountUpdateSeconds) > 30))
{
LastRecordCountUpdateSeconds = FPlatformTime::Seconds();
ImmediateOperations.Enqueue([this]() {
FScopeLock ScopedLock(&SearchDatabaseCS);
TotalSearchRecords = SearchDatabase.GetTotalSearchRecords();
});
}
return true;
}
uint32 FAssetSearchManager::Run()
{
Tick_DatabaseOperationThread();
return 0;
}
void FAssetSearchManager::Tick_DatabaseOperationThread()
{
while (RunThread)
{
if (!bDatabaseOpen)
{
TryConnectToDatabase();
FPlatformProcess::Sleep(1);
continue;
}
TFunction<void()> Operation;
if (ImmediateOperations.Dequeue(Operation) || FeedOperations.Dequeue(Operation) || UpdateOperations.Dequeue(Operation))
{
Operation();
}
else
{
FPlatformProcess::Sleep(0.1);
}
}
}
void FAssetSearchManager::ForceIndexOnAssetsMissingIndex()
{
FScopeLock Lock(&AssetSearchManagerCS);
{
FScopedSlowTask IndexingTask(AssetNeedingReindexing.Num(), LOCTEXT("ForceIndexOnAssetsMissingIndex", "Indexing Assets"));
IndexingTask.MakeDialog(true);
int32 RemovedCount = 0;
TArray<FAssetData> RedirectorsWithBrokenMetadata;
TGuardValue<bool> GuardResetTesting(GIsAutomationTesting, true);
const int32 OnePercentChunk = (int32)(AssetNeedingReindexing.Num() / 100.0);
int32 ChunkProgress = 0;
FUnloadPackageScope UnloadScope;
for (const FAssetData& Asset : AssetNeedingReindexing)
{
if (IndexingTask.ShouldCancel())
{
break;
}
if (RemovedCount > ChunkProgress)
{
ChunkProgress += OnePercentChunk;
IndexingTask.EnterProgressFrame(OnePercentChunk, FText::Format(LOCTEXT("ForceIndexOnAssetsMissingIndexFormat", "Indexing Asset ({0} of {1})"), RemovedCount + 1, AssetNeedingReindexing.Num()));
}
if (UObject* AssetToIndex = Asset.GetAsset())
{
// This object's metadata incorrectly labled it as something other than a redirector. We need to resave it
// to stop it from appearing as something it's not.
if (UObjectRedirector* Redirector = Cast<UObjectRedirector>(AssetToIndex))
{
RedirectorsWithBrokenMetadata.Add(Asset);
RemovedCount++;
continue;
}
if (!bTryIndexAssetsOnLoad)
{
StoreIndexForAsset(AssetToIndex);
}
}
if (UnloadScope.GetObjectsLoaded() > 2000)
{
UnloadScope.TryUnload(true);
}
RemovedCount++;
}
if (RedirectorsWithBrokenMetadata.Num() > 0)
{
EAppReturnType::Type ResaveRedirectors = FMessageDialog::Open(EAppMsgType::YesNo,
LOCTEXT("ResaveRedirectors", "We found some redirectors that didn't have the correct asset metadata identifying them as redirectors. Would you like to resave them, so that they stop appearing as missing asset indexes?"));
if (ResaveRedirectors == EAppReturnType::Yes)
{
TArray<UPackage*> PackagesToSave;
for (const FAssetData& BrokenAsset : RedirectorsWithBrokenMetadata)
{
if (UObject* Redirector = BrokenAsset.GetAsset())
{
PackagesToSave.Add(Redirector->GetOutermost());
}
}
FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, /*bCheckDirty*/false, /*bPromptToSave*/false);
}
}
AssetNeedingReindexing.RemoveAtSwap(0, RemovedCount);
}
if (IntermediateStorage == ESearchIntermediateStorage::AssetTagData)
{
const bool bPromptUserToSave = true;
const bool bSaveMapPackages = true;
const bool bSaveContentPackages = true;
const bool bFastSave = false;
const bool bNotifyNoPackagesSaved = false;
const bool bCanBeDeclined = false;
FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, bNotifyNoPackagesSaved, bCanBeDeclined);
}
}
void FAssetSearchManager::Search(FSearchQueryPtr SearchQuery)
{
check(IsInGameThread());
FScopeLock Lock(&AssetSearchManagerCS);
if (FStudioTelemetry::IsAvailable())
{
FStudioTelemetry::Get().RecordEvent(TEXT("AssetSearch"), { FAnalyticsEventAttribute(TEXT("QueryString"), SearchQuery->QueryText)} );
}
ImmediateOperations.Enqueue([this, SearchQuery]() {
TArray<FSearchRecord> Results;
{
FScopeLock ScopedLock(&SearchDatabaseCS);
// This short circuits the search if it's no longer important to fulfill
if (SearchQuery->IsQueryStillImportant())
{
SearchDatabase.EnumerateSearchResults(SearchQuery->QueryText, [&Results](FSearchRecord&& InResult) {
Results.Add(MoveTemp(InResult));
return true;
});
}
}
AsyncMainThreadTask([ResultsFwd = MoveTemp(Results), SearchQuery]() mutable {
if (FSearchQuery::ResultsCallbackFunction ResultsCallback = SearchQuery->GetResultsCallback())
{
ResultsCallback(MoveTemp(ResultsFwd));
}
});
});
for (auto& SearchProviderEntry : SearchProviders)
{
TUniquePtr<ISearchProvider>& Provider = SearchProviderEntry.Value;
Provider->Search(SearchQuery);
}
}
void FAssetSearchManager::AsyncMainThreadTask(TFunction<void()> Task)
{
FScopeLock Lock(&AssetSearchManagerCS);
GT_Tasks.Enqueue(Task);
}
void FAssetSearchManager::ProcessGameThreadTasks()
{
FScopeLock Lock(&AssetSearchManagerCS);
if (!GT_Tasks.IsEmpty())
{
if (GIsSavingPackage)
{
// If we're saving packages just give up, the call in Tick_GameThread will do this later.
return;
}
int MaxGameThreadTasksPerTick = 1000;
TFunction<void()> Operation;
while (GT_Tasks.Dequeue(Operation) && MaxGameThreadTasksPerTick > 0)
{
Operation();
MaxGameThreadTasksPerTick--;
}
}
}
#undef LOCTEXT_NAMESPACE