// Copyright Epic Games, Inc. All Rights Reserved. #include "ReferenceInfoUtils.h" #include "HAL/FileManager.h" #include "Misc/Paths.h" #include "Misc/OutputDeviceFile.h" #include "HAL/IConsoleManager.h" #include "Model.h" #include "Modules/ModuleManager.h" #include "Serialization/ArchiveUObject.h" #include "UObject/Class.h" #include "UObject/UObjectIterator.h" #include "UObject/Package.h" #include "Engine/Level.h" #include "GameFramework/Actor.h" #include "Materials/MaterialInterface.h" #include "AssetRegistry/AssetData.h" #include "Editor/UnrealEdEngine.h" #include "ThumbnailRendering/ThumbnailManager.h" #include "Editor.h" #include "UnrealEdGlobals.h" #include "AssetRegistry/ARFilter.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Engine/Selection.h" void ExecuteReferenceInfo(const TArray& Args, UWorld* InWorld ) { bool bShowDefault = true; bool bShowScript = true; int32 Depth = 0; for (int32 ArgIdx = 0; ArgIdx < Args.Num(); ++ArgIdx) { if (FCString::Stristr(*Args[ArgIdx], TEXT("nodefault"))) { bShowDefault = false; } if (FCString::Stristr(*Args[ArgIdx], TEXT("noscript"))) { bShowScript = false; } FParse::Value(*Args[ArgIdx], TEXT("DEPTH="), Depth); } ReferenceInfoUtils::GenerateOutput(InWorld, Depth, bShowDefault, bShowScript); } static FAutoConsoleCommandWithWorldAndArgs ActorReferenceInfoCVar( TEXT("ReferenceInfo"), TEXT("Outputs reference info for selected actors to a log file. Syntax is: ReferenceInfo [-depth=] [-nodefault] [-noscript]"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(ExecuteReferenceInfo) ); namespace ReferenceInfoUtils { typedef TMap< TObjectPtr, TArray> > ObjectReferenceGraph; typedef TMap ReferenceTreeMap; typedef TMap ObjectNameMap; /** * Data container to hold information about what is referencing a given set of assets. */ struct FReferencedAssets { /** The object that holding a reference to the set of assets */ UObject* Referencer; /** The set of assets that are being referenced */ TArray AssetList; /** Default ctor */ FReferencedAssets() : Referencer(NULL) { } /** Sets the name of the referencer */ FReferencedAssets(UObject* InReferencer) : Referencer(InReferencer) { } /** Serializer **/ friend FArchive& operator<<(FArchive& Ar, FReferencedAssets& Asset) { return Ar << Asset.Referencer << Asset.AssetList; } }; /** * This archive searches objects for assets. It determines the set of assets by whether they support thumbnails or not. Possibly, not best but it displays everything as thumbnails */ class FFindAssetsArchive : public FArchiveUObject { /** The root object that was used to being serialization for this archive */ UObject* StartObject; /** The object currently being serialized */ UObject* CurrentObject; /** The array to add any found assets too */ TArray& AssetList; /** Set when the global asset list is updated. Used to prevent the reference graph from being polluted by calls to the public version of BuildAssetList */ ObjectReferenceGraph* CurrentReferenceGraph; /** If false, ignore all assets referenced only through script */ bool bIncludeScriptRefs; /** If false, ignore all assets referenced only through archetype/class default objects */ bool bIncludeDefaultRefs; /** Maximum depth to recursively serialize objects; 0 indicates no limit to recursion */ const int32 MaxRecursionDepth; /** Current recursion depth */ int32 CurrentDepth; /** * Manually serializes the class and archetype for the specified object so that assets which are referenced through the object's class/archetype can be differentiated */ void HandleReferencedObject(UObject* Obj) { if (CurrentReferenceGraph != NULL) { // Here we allow recursion if the current depth is less-than-equal (as opposed to less-than) because the archetype and class are treated as transparent objects // Serialization of the class and object are controlled by the "show class refs" and "show default refs" buttons if (MaxRecursionDepth == 0 || CurrentDepth < MaxRecursionDepth) { // Now change the current reference list to the one for this object if (bIncludeDefaultRefs == true) { auto* ReferencedAssets = GetAssetList(Obj); // See the comment for the bIncludeScriptRefs block UObject* ObjectArc = Obj->GetArchetype(); ReferencedAssets->AddUnique(ObjectArc); UObject* PreviousObject = CurrentObject; CurrentObject = ObjectArc; if (ObjectArc->HasAnyMarks(OBJECTMARK_TagExp)) { // Temporarily disable serialization of the class, as we need to specially handle that as well bool bSkipClassSerialization = ArIgnoreClassRef; ArIgnoreClassRef = true; ObjectArc->UnMark(OBJECTMARK_TagExp); ObjectArc->Serialize(*this); ArIgnoreClassRef = bSkipClassSerialization; } CurrentObject = PreviousObject; } if (bIncludeScriptRefs == true) { auto* ReferencedAssets = GetAssetList(Obj); // We want to see assets referenced by this object's class, but classes don't have associated thumbnail rendering info // So we'll need to serialize the class manually in order to get the object references encountered through the class to fall // under the appropriate tree item // Serializing the class will result in serializing the class default object; but we need to do this manually (for the same reason // that we do it for the class), so temporarily prevent the CDO from being serialized by this archive UClass* ObjectClass = Obj->GetClass(); ReferencedAssets->AddUnique(ObjectClass); UObject* PreviousObject = CurrentObject; CurrentObject = ObjectClass; if (ObjectClass->HasAnyMarks(OBJECTMARK_TagExp)) { ObjectClass->UnMark(OBJECTMARK_TagExp); ObjectClass->Serialize(*this); } CurrentObject = PreviousObject; } } } } /** * Retrieves the referenced assets list for the specified object */ TArray>* GetAssetList(UObject* Referencer) { check(Referencer); auto* ReferencedAssetList = CurrentReferenceGraph->Find(ObjectPtrWrap(Referencer)); if (ReferencedAssetList == NULL) { // add a new entry for the specified object ReferencedAssetList = &CurrentReferenceGraph->Add(ObjectPtrWrap(Referencer), TArray>{}); } return ReferencedAssetList; } public: /** * Functor that starts the serialization process */ FFindAssetsArchive(UObject* Search, TArray& OutAssetList, ObjectReferenceGraph* ReferenceGraph = NULL, int32 MaxRecursion = 0, bool bIncludeClasses = true, bool bIncludeDefaults = false) : StartObject(Search) , AssetList(OutAssetList) , CurrentReferenceGraph(ReferenceGraph) , bIncludeScriptRefs(bIncludeClasses) , bIncludeDefaultRefs(bIncludeDefaults) , MaxRecursionDepth(MaxRecursion) , CurrentDepth(0) { ArIsObjectReferenceCollector = true; ArIgnoreClassRef = !bIncludeScriptRefs; CurrentObject = StartObject; *this << StartObject; } /** * Adds the object reference to the asset list if it supports thumbnails * Recursively searches through its references for more assets */ FArchive& operator<<(UObject*& Obj) { // Don't check null references or objects already visited if (Obj != NULL && Obj->HasAnyMarks(OBJECTMARK_TagExp) && // Ff we wish to filter out assets referenced through script, we need to ignore all class objects, not just the UObject::Class reference (!ArIgnoreClassRef || (Cast(Obj) == NULL))) { // Clear the search flag so we don't revisit objects Obj->UnMark(OBJECTMARK_TagExp); if (Obj->IsA(UField::StaticClass())) { // Skip all of the other stuff because the serialization of UFields will quickly overflow our stack given the number of temporary variables we create in the below code Obj->Serialize(*this); } else { // Only report this object reference if it supports thumbnail display this eliminates all of the random objects like functions, properties, etc. const bool bCDO = Obj->HasAnyFlags(RF_ClassDefaultObject); const bool bIsContent = GUnrealEd->GetThumbnailManager()->GetRenderingInfo(Obj) != NULL; const bool bIncludeAnyway = (Obj->GetOuter() == CurrentObject) && (Cast(CurrentObject) == NULL); const bool bShouldReportAsset = !bCDO && (bIsContent || bIncludeAnyway); // Remember which object we were serializing UObject* PreviousObject = CurrentObject; if (bShouldReportAsset) { CurrentObject = Obj; // Add this object to the list to display AssetList.Add(CurrentObject); if (CurrentReferenceGraph != NULL) { auto* CurrentObjectAssets = GetAssetList(PreviousObject); check(CurrentObjectAssets); // Add this object to the list of objects referenced by the object currently being serialized CurrentObjectAssets->Add(CurrentObject); HandleReferencedObject(CurrentObject); } } else if (Obj == StartObject) { HandleReferencedObject(Obj); } if (MaxRecursionDepth == 0 || CurrentDepth < MaxRecursionDepth) { CurrentDepth++; // Now recursively search this object for more references Obj->Serialize(*this); CurrentDepth--; } // Restore the previous object that was being serialized CurrentObject = PreviousObject; } } return *this; } }; /** This is a list of classes that should be ignored when building the asset list as they are always loaded and therefore not pertinent */ TArray IgnoreClasses; /** This is a list of packages that should be ignored when building the asset list as they are always loaded and therefore not pertinent */ TArray IgnorePackages; /** Holds the list of assets that are being referenced by the current selection */ TArray Referencers; /** The object graph for the assets referenced by the currently selected actors */ ObjectReferenceGraph ReferenceGraph; /** Caches the names of the objects referenced by the currently selected actors */ ObjectNameMap ObjectNameCache; /** * Checks an object to see if it should be included for asset searching */ bool ShouldSearchForAssets(const UObject* Object, const TArray& ClassesToIgnore, const TArray& PackagesToIgnore, bool bIncludeDefaults = false) { bool bShouldSearch = true; // Package name transition if (Object->HasAnyFlags(RF_ClassDefaultObject) && (Object->GetOutermost()->GetFName() == NAME_CoreUObject || Object->GetOutermost()->GetFName() == GLongCoreUObjectPackageName)) { // Ignore all class default objects for classes which are declared in Core bShouldSearch = false; } // Check to see if we should ignore a class for (int32 Index = 0; Index < ClassesToIgnore.Num(); Index++) { // Bail if we are on the ignore list if (Object->IsA(ClassesToIgnore[Index])) { bShouldSearch = false; break; } } if (bShouldSearch) { // Check to see if we should ignore it due to package for (int32 Index = 0; Index < PackagesToIgnore.Num(); Index++) { // If this object belongs to this package, bail if (Object->IsIn(PackagesToIgnore[Index])) { bShouldSearch = false; break; } } } if (bShouldSearch && !bIncludeDefaults && Object->IsTemplate()) { // If this object is an archetype and we don't want to see assets referenced by defaults, don't include this object bShouldSearch = false; } return bShouldSearch; } /** * Builds a list of assets to display from the currently selected actors. * NOTE: It ignores assets that are there because they are always loaded * such as default materials, textures, etc. */ void BuildAssetList(UWorld* InWorld, int32 Depth, bool bShowDefault, bool bShowScript) { // Clear the old list Referencers.Empty(); ReferenceGraph.Empty(); ObjectNameCache.Empty(); TArray BspMats; // Search all BSP surfaces for ones that are selected and add their // Materials to a temp list for (int32 Index = 0; Index < InWorld->GetModel()->Surfs.Num(); Index++) { // Only add materials that are selected if (InWorld->GetModel()->Surfs[Index].PolyFlags & PF_Selected) { // No point showing the default material if (InWorld->GetModel()->Surfs[Index].Material != NULL) { BspMats.AddUnique(InWorld->GetModel()->Surfs[Index].Material); } } } // If any BSP surfaces are selected if (BspMats.Num() > 0) { FReferencedAssets& Referencer = Referencers.Emplace_GetRef(InWorld->GetModel()); // Now copy the array Referencer.AssetList = BspMats; ReferenceGraph.Add(ObjectPtrWrap(InWorld->GetModel()), ObjectPtrWrap(BspMats)); } // This is the maximum depth to use when searching for references const int32 MaxRecursionDepth = Depth; USelection* ActorSelection = GEditor->GetSelectedActors(); // Mark all objects so we don't get into an endless recursion for (FThreadSafeObjectIterator It; It; ++It) { // Skip the level, world, and any packages that should be ignored if (ShouldSearchForAssets(*It, IgnoreClasses, IgnorePackages, bShowDefault)) { It->Mark(OBJECTMARK_TagExp); } else { It->UnMark(OBJECTMARK_TagExp); } } TArray SelectedActors; // Get the list of currently selected actors ActorSelection->GetSelectedObjects(SelectedActors); // Build the list of assets from the set of selected actors for (int32 Index = 0; Index < SelectedActors.Num(); Index++) { // Set the flag for the selected item, as it could have actually been cleared by an // earlier selected object, which would result in a crash later SelectedActors[Index]->Mark(OBJECTMARK_TagExp); // Create a new entry for this actor FReferencedAssets& Referencer = Referencers.Emplace_GetRef(SelectedActors[Index]); // Add to the list of referenced assets FFindAssetsArchive(SelectedActors[Index], Referencer.AssetList, &ReferenceGraph, MaxRecursionDepth, bShowScript, bShowDefault); } // Rebuild the name cache for (int32 RefIndex = 0; RefIndex < Referencers.Num(); RefIndex++) { FReferencedAssets& Referencer = Referencers[RefIndex]; if (!ObjectNameCache.Contains(Referencer.Referencer)) { ObjectNameCache.Add(Referencer.Referencer, *Referencer.Referencer->GetName()); } for (int32 AssetIndex = 0; AssetIndex < Referencer.AssetList.Num(); AssetIndex++) { if (!ObjectNameCache.Contains(Referencer.AssetList[AssetIndex])) { ObjectNameCache.Add(Referencer.AssetList[AssetIndex], *Referencer.AssetList[AssetIndex]->GetName()); } } } } /** * Helper method... returns the name of a referenced object */ const TCHAR* GetObjectNameFromCache(UObject* Obj) { FString* CachedObjectName = ObjectNameCache.Find(Obj); if (CachedObjectName == NULL) { CachedObjectName = &ObjectNameCache.Add(Obj, *Obj->GetName()); } return **CachedObjectName; } /** * Outputs a single item for the details list */ void OutputDetailsItem(FOutputDeviceFile& FileAr, FString AssetId, UObject* ReferencedObject, FString& ItemString) { FString ObjName = FString::Printf(TEXT("%s (%s)"), *ItemString, *AssetId); FString Underline; int32 ObjNameLen = ObjName.Len(); for (int32 i = 0; i < ObjNameLen; ++i) { Underline += TEXT("-"); } // Create a string for the resource size. const SIZE_T ReferencedObjectResourceSize = ReferencedObject->GetResourceSizeBytes(EResourceSizeMode::EstimatedTotal); FString ResourceSizeString; if ( ReferencedObjectResourceSize > 0 ) { ResourceSizeString = FString::Printf( TEXT("%.2f"), ((float)ReferencedObjectResourceSize)/1024.f ); } FString ObjectPathName; if ( ReferencedObject->GetOuter() != NULL ) { ObjectPathName = ReferencedObject->GetOuter()->GetPathName(); } // Add this referenced asset's information to the list. FileAr.Log(TEXT("")); FileAr.Log(*ObjName); FileAr.Log(*Underline); FileAr.Logf(TEXT("Grouping: %s"), *ObjectPathName); FileAr.Logf(TEXT("Class: %s"), GetObjectNameFromCache(ReferencedObject->GetClass())); FileAr.Logf(TEXT("Size: %s"), *ResourceSizeString); FileAr.Logf(TEXT("Info: %s"), *ReferencedObject->GetDesc()); } /** * Recursively transverses the reference tree */ void OutputReferencedAssets(FOutputDeviceFile& FileAr, int32 CurrentDepth, FString ParentId, UObject* BaseObject, const TArray* AssetList) { check(AssetList); const FString ScriptItemString = NSLOCTEXT("UnrealEd", "Script", "Script").ToString(); const FString DefaultsItemString = NSLOCTEXT("UnrealEd", "Defaults", "Defaults").ToString(); for (int32 AssetIndex = 0; AssetIndex < AssetList->Num(); AssetIndex++) { UObject* ReferencedObject = (*AssetList)[AssetIndex]; check(ReferencedObject); // get the list of assets this object is referencing auto* ReferencedAssets = ReferenceGraph.Find(ObjectPtrWrap(ReferencedObject)); // add a new tree item for this referenced asset FString ItemString; if (ReferencedObject == BaseObject->GetClass()) { ItemString = *ScriptItemString; if (ReferencedAssets == NULL || ReferencedAssets->Num() == 0) { // special case for the "Script" node - don't add it if it doesn't have any children continue; } } else if (ReferencedObject == BaseObject->GetArchetype()) { ItemString = *DefaultsItemString; if (ReferencedAssets == NULL || ReferencedAssets->Num() == 0) { // special case for the "Defaults" node - don't add it if it doesn't have any children continue; } } else { if (CurrentDepth > 0) { ItemString = ReferencedObject->GetPathName(); } else { ItemString = GetObjectNameFromCache(ReferencedObject); } } FString AssetId = FString::Printf(TEXT("%s.%d"), *ParentId, AssetIndex); if (CurrentDepth > 0) { FString TabStr; for (int32 i = 0; i < CurrentDepth; ++i) { TabStr += TEXT("\t"); } FileAr.Logf(TEXT("%s(%s) %s"), *TabStr, *AssetId, *ItemString); } else { OutputDetailsItem(FileAr, AssetId, ReferencedObject, ItemString); } if (ReferencedAssets != NULL) { // If this object is referencing other objects, output those objects OutputReferencedAssets(FileAr, (CurrentDepth == 0)? 0: CurrentDepth + 1, AssetId, ReferencedObject, &ObjectPtrDecay(*ReferencedAssets)); } } } /** * Outputs the tree view */ void OutputTree(FOutputDeviceFile& FileAr) { FileAr.Logf(TEXT("*******************")); FileAr.Logf(TEXT("* Reference Graph *")); FileAr.Logf(TEXT("*******************")); FileAr.Logf(TEXT("")); for (int32 ReferenceIndex = 0; ReferenceIndex < Referencers.Num(); ReferenceIndex++) { // add an item at the root level for the selected actor FReferencedAssets& Asset = Referencers[ReferenceIndex]; FString Id = FString::Printf(TEXT("%d"), ReferenceIndex); FileAr.Logf(TEXT("(%s) %s"), *Id, GetObjectNameFromCache(Asset.Referencer)); auto* ReferencedAssets = ReferenceGraph.Find(ObjectPtrWrap(Asset.Referencer)); if (ReferencedAssets) { OutputReferencedAssets(FileAr, 1, Id, Asset.Referencer, &ObjectPtrDecay(*ReferencedAssets)); } } } /** * Outputs the details list */ void OutputDetails(FOutputDeviceFile& FileAr) { FileAr.Logf(LINE_TERMINATOR); FileAr.Logf(TEXT("*********************")); FileAr.Logf(TEXT("* Reference Details *")); FileAr.Logf(TEXT("*********************")); for (int32 ReferenceIndex = 0; ReferenceIndex < Referencers.Num(); ReferenceIndex++) { // add an item at the root level for the selected actor FReferencedAssets& Asset = Referencers[ReferenceIndex]; FString Id = FString::Printf(TEXT("%d"), ReferenceIndex); FString ItemName = GetObjectNameFromCache(Asset.Referencer); OutputDetailsItem(FileAr, Id, Asset.Referencer, ItemName); auto* ReferencedAssets = ReferenceGraph.Find(ObjectPtrWrap(Asset.Referencer)); if (ReferencedAssets) { OutputReferencedAssets(FileAr, 0, Id, Asset.Referencer, &ObjectPtrDecay(*ReferencedAssets)); } } } void GenerateOutput(UWorld* InWorld, int32 Depth, bool bShowDefault, bool bShowScript) { ELogTimes::Type PrintLogTimes = GPrintLogTimes; // Create log file const FString PathName = *(FPaths::ProjectLogDir() + TEXT("RefInfo/")); IFileManager::Get().MakeDirectory(*PathName); const FString Filename = FString::Printf(TEXT("Output-%s.txt"), *FDateTime::Now().ToString(TEXT("%m.%d-%H.%M.%S"))); const FString FilenameFull = PathName + Filename; FOutputDeviceFile FileAr(*FilenameFull); FileAr.SetSuppressEventTag(true); GPrintLogTimes = ELogTimes::None; // Set up our ignore lists IgnoreClasses.Empty(); IgnorePackages.Empty(); IgnoreClasses.Add(ULevel::StaticClass()); IgnoreClasses.Add(UWorld::StaticClass()); // Load the asset registry module FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); TArray AssetData; FARFilter Filter; Filter.PackagePaths.Add(FName(TEXT("/Engine/EngineResources"))); Filter.PackagePaths.Add(FName(TEXT("/Engine/EngineFonts"))); Filter.PackagePaths.Add(FName(TEXT("/Engine/EngineMaterials"))); Filter.PackagePaths.Add(FName(TEXT("/Engine/EditorResources"))); Filter.PackagePaths.Add(FName(TEXT("/Engine/EditorMaterials"))); AssetRegistryModule.Get().GetAssets(Filter, AssetData); for (int32 AssetIdx = 0; AssetIdx < AssetData.Num(); ++AssetIdx) { IgnorePackages.Add(FindObject(NULL, *AssetData[AssetIdx].PackageName.ToString(), true)); } IgnorePackages.Add(GetTransientPackage()); // Bug? At this point IgnorePackages often has a handful of null entries, which, completely throws off the filtering process IgnorePackages.Remove(nullptr); // Generate reference info BuildAssetList(InWorld, Depth, bShowDefault, bShowScript); // Output reference info OutputTree(FileAr); OutputDetails(FileAr); FileAr.TearDown(); GPrintLogTimes = PrintLogTimes; // Display "completed" popup FString AbsPath = FilenameFull; FPaths::ConvertRelativePathToFull(AbsPath); FFormatNamedArguments Args; Args.Add( TEXT("AbsolutePath"), FText::FromString( AbsPath ) ); FNotificationInfo Info( FText::Format( NSLOCTEXT("UnrealEd", "ReferenceInfoSavedNotification", "Reference info was successfully saved to: {AbsolutePath}"), Args ) ); Info.ExpireDuration = 3.0f; FSlateNotificationManager::Get().AddNotification(Info); } }