// 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> PackageObjectPtrs; for (const FObjectKey& LoadedObjectKey : ObjectsLoaded) { if (UObject* LoadedObject = LoadedObjectKey.ResolveObjectPtr()) { UPackage* Package = LoadedObject->GetOutermost(); TArray 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 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 ObjectsLoaded; TArray 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()->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("DataAsset")); RegisterAssetIndexer(UDataTable::StaticClass(), MakeUnique()); RegisterAssetIndexer(UCurveTable::StaticClass(), MakeUnique()); RegisterAssetIndexer(UBlueprint::StaticClass(), MakeUnique()); RegisterAssetIndexer(UWidgetBlueprint::StaticClass(), MakeUnique()); RegisterAssetIndexer(UDialogueWave::StaticClass(), MakeUnique()); RegisterAssetIndexer(UWorld::StaticClass(), MakeUnique()); RegisterAssetIndexer(AActor::StaticClass(), MakeUnique()); RegisterAssetIndexer(USoundCue::StaticClass(), MakeUnique()); RegisterAssetIndexer(UMaterial::StaticClass(), MakeUnique("Material")); RegisterAssetIndexer(UMaterialFunction::StaticClass(), MakeUnique("MaterialFunction")); RegisterAssetIndexer(UMaterialParameterCollection::StaticClass(), MakeUnique("MaterialParameterCollection")); RegisterAssetIndexer(UMaterialInstance::StaticClass(), MakeUnique("MaterialInstance")); RegisterSearchProvider(TEXT("AssetRegistry"), MakeUnique()); 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()->bEnableIntegrityChecks; FileInfoDatabase.bEnableIntegrityChecks = GetDefault()->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()->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("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 TempAssetData; AssetRegistry.GetAllAssets(TempAssetData, true); for (const FAssetData& Data : TempAssetData) { OnAssetAdded(Data); } } } void FAssetSearchManager::StopScanningAssets() { if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr("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&& Indexer) { FScopeLock Lock(&AssetSearchManagerCS); Indexers.Add(AssetClass->GetFName(), MoveTemp(Indexer)); } void FAssetSearchManager::RegisterSearchProvider(FName SearchProviderName, TUniquePtr&& 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(); 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(); 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 AllAssets; IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked("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(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 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 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(FAssetSearchManager::AssetSearchIndexVersionTag); const FString SavedIndexHash = InAssetData.GetTagValueRef(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(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()->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 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 NestedIndexedTypes; const UClass* IndexableClass = InAssetClass; while (IndexableClass) { if (const TUniquePtr* 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()->bDisableDDC; if (bAllowPut) { FTCHARToUTF8 IndexedJsonUTF8(*IndexedJson); TArrayView 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& 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(); 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 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 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 RedirectorsWithBrokenMetadata; TGuardValue 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(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 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 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& Provider = SearchProviderEntry.Value; Provider->Search(SearchQuery); } } void FAssetSearchManager::AsyncMainThreadTask(TFunction 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 Operation; while (GT_Tasks.Dequeue(Operation) && MaxGameThreadTasksPerTick > 0) { Operation(); MaxGameThreadTasksPerTick--; } } } #undef LOCTEXT_NAMESPACE