// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #include "Libraries/DirectiveUtilEditorAssetAuditLibrary.h" #include "AssetRegistry/DirectiveUtilAssetRegistry.h" #include "AssetRegistry/DirectiveUtilDependencyCycleFinder.h" #include "AssetRegistry/ARFilter.h" #include "AssetRegistry/IAssetRegistry.h" #include "Engine/AssetManager.h" #include "HAL/FileManager.h" #include "Misc/PackageName.h" #include "UObject/ObjectRedirector.h" namespace { using namespace UE::AssetRegistry; bool IsPackageInsideAuditPath(const FName PackageName, const FName Path) { const FString Package = PackageName.ToString(); FString Parent = Path.ToString(); Parent.RemoveFromEnd(TEXT("/")); return Package == Parent || Package.StartsWith(Parent + TEXT("/")); } bool ContainsPathSegment(const FName PackagePath, const FString& Segment) { const FString Path = TEXT("/") + PackagePath.ToString().TrimChar(TEXT('/')) + TEXT("/"); return Path.Contains(TEXT("/") + Segment + TEXT("/")); } bool IsExcluded(const FAssetData& Asset, const FDirectiveUtilAssetAuditOptions& Options) { if (Asset.AssetClassPath == UObjectRedirector::StaticClass()->GetClassPathName()) { return true; } if (Options.bUseDefaultPathExclusions && (ContainsPathSegment(Asset.PackagePath, TEXT("Developers")) || ContainsPathSegment(Asset.PackagePath, TEXT("__ExternalActors__")) || ContainsPathSegment(Asset.PackagePath, TEXT("__ExternalObjects__")) || ContainsPathSegment(Asset.PackagePath, TEXT("Test")) || ContainsPathSegment(Asset.PackagePath, TEXT("Tests")))) { return true; } for (const FName Path : Options.ExcludedPackagePaths) { if (IsPackageInsideAuditPath(Asset.PackageName, Path)) { return true; } } return false; } TArray GetScannedAssets(IAssetRegistry& AssetRegistry, const FDirectiveUtilAssetAuditOptions& Options) { DirectiveUtilitiesEditor::EnsureAssetRegistryScan(AssetRegistry); FARFilter Filter; Filter.bRecursivePaths = true; if (Options.PackagePaths.IsEmpty()) { Filter.PackagePaths.Add(TEXT("/Game")); } else { Filter.PackagePaths.Append(Options.PackagePaths); } TArray Assets; AssetRegistry.GetAssets(Filter, Assets); Assets.RemoveAll([&Options](const FAssetData& Asset) { return !Asset.IsValid() || IsExcluded(Asset, Options); }); Assets.Sort([](const FAssetData& Left, const FAssetData& Right) { return Left.GetSoftObjectPath().LexicalLess(Right.GetSoftObjectPath()); }); return Assets; } bool IsPrimaryAsset(const FAssetData& Asset) { const UAssetManager* AssetManager = UAssetManager::GetIfInitialized(); return AssetManager && AssetManager->GetPrimaryAssetIdForData(Asset).IsValid(); } EDirectiveUtilAssetDependencyType GetDependencyType(const FAssetDependency& Dependency) { if (EnumHasAnyFlags(Dependency.Category, EDependencyCategory::SearchableName)) { return EDirectiveUtilAssetDependencyType::SearchableName; } if (EnumHasAnyFlags(Dependency.Category, EDependencyCategory::Manage)) { return EnumHasAnyFlags(Dependency.Properties, EDependencyProperty::Direct) ? EDirectiveUtilAssetDependencyType::DirectManagement : EDirectiveUtilAssetDependencyType::IndirectManagement; } return EnumHasAnyFlags(Dependency.Properties, EDependencyProperty::Hard) ? EDirectiveUtilAssetDependencyType::HardPackage : EDirectiveUtilAssetDependencyType::SoftPackage; } bool PackageExists(IAssetRegistry& AssetRegistry, const FName PackageName) { if (PackageName.IsNone()) { return true; } const FString Package = PackageName.ToString(); if (Package.StartsWith(TEXT("/Script/")) || Package.StartsWith(TEXT("/Memory/")) || Package.StartsWith(TEXT("/Temp/"))) { return true; } if (FindPackage(nullptr, *Package)) { return true; } TArray PackageAssets; return AssetRegistry.GetAssetsByPackageName(PackageName, PackageAssets, true) && !PackageAssets.IsEmpty(); } class FAssetAuditScan { public: FAssetAuditScan(IAssetRegistry& InAssetRegistry, const FDirectiveUtilAssetAuditOptions& InOptions) : AssetRegistry(InAssetRegistry) , bIncludePrimaryAssets(InOptions.bIncludePrimaryAssets) , Assets(GetScannedAssets(InAssetRegistry, InOptions)) { } const TArray& GetAssets() const { return Assets; } const TArray& GetDependencies(const FName PackageName) { if (const TArray* Existing = Dependencies.Find(PackageName)) { return *Existing; } TArray& Result = Dependencies.Add(PackageName); AssetRegistry.GetDependencies(FAssetIdentifier(PackageName), Result, EDependencyCategory::All); return Result; } const TArray& GetReferencers(const FName PackageName) { if (const TArray* Existing = Referencers.Find(PackageName)) { return *Existing; } TArray& Result = Referencers.Add(PackageName); AssetRegistry.GetReferencers(PackageName, Result, EDependencyCategory::All); Result.Remove(PackageName); Result.Sort(FNameLexicalLess()); return Result; } bool IncludesPrimaryAssets() const { return bIncludePrimaryAssets; } IAssetRegistry& GetAssetRegistry() const { return AssetRegistry; } private: IAssetRegistry& AssetRegistry; bool bIncludePrimaryAssets; TArray Assets; TMap> Dependencies; TMap> Referencers; }; FString EscapeCsv(const FString& Value) { FString Escaped = Value.Replace(TEXT("\""), TEXT("\"\"")); return FString::Printf(TEXT("\"%s\""), *Escaped); } FString DependencyTypeToString(const EDirectiveUtilAssetDependencyType Type) { switch (Type) { case EDirectiveUtilAssetDependencyType::HardPackage: return TEXT("Hard Package"); case EDirectiveUtilAssetDependencyType::SoftPackage: return TEXT("Soft Package"); case EDirectiveUtilAssetDependencyType::SearchableName: return TEXT("Searchable Name"); case EDirectiveUtilAssetDependencyType::DirectManagement: return TEXT("Direct Management"); case EDirectiveUtilAssetDependencyType::IndirectManagement: return TEXT("Indirect Management"); } return TEXT("Unknown"); } TArray FindUnreferencedCandidates(FAssetAuditScan& Scan) { TArray Candidates; for (const FAssetData& Asset : Scan.GetAssets()) { if ((!IsPrimaryAsset(Asset) || Scan.IncludesPrimaryAssets()) && Scan.GetReferencers(Asset.PackageName).IsEmpty()) { Candidates.Add(Asset); } } return Candidates; } TArray FindMissingReferences(FAssetAuditScan& Scan) { TArray MissingReferences; for (const FAssetData& Asset : Scan.GetAssets()) { for (const FAssetDependency& Dependency : Scan.GetDependencies(Asset.PackageName)) { if (!PackageExists(Scan.GetAssetRegistry(), Dependency.AssetId.PackageName)) { FDirectiveUtilMissingAssetReference& MissingReference = MissingReferences.AddDefaulted_GetRef(); MissingReference.ReferencingAsset = Asset; MissingReference.MissingPackage = Dependency.AssetId.PackageName; MissingReference.DependencyType = GetDependencyType(Dependency); } } } MissingReferences.Sort([](const FDirectiveUtilMissingAssetReference& Left, const FDirectiveUtilMissingAssetReference& Right) { if (Left.ReferencingAsset.PackageName != Right.ReferencingAsset.PackageName) { return Left.ReferencingAsset.PackageName.LexicalLess(Right.ReferencingAsset.PackageName); } return Left.MissingPackage.LexicalLess(Right.MissingPackage); }); return MissingReferences; } TArray FindDependencyCycles(FAssetAuditScan& Scan) { TSet ScannedPackages; for (const FAssetData& Asset : Scan.GetAssets()) { ScannedPackages.Add(Asset.PackageName); } TMap> Graph; for (const FName Package : ScannedPackages) { TArray& Edges = Graph.Add(Package); for (const FAssetDependency& Dependency : Scan.GetDependencies(Package)) { if (ScannedPackages.Contains(Dependency.AssetId.PackageName)) { Edges.AddUnique(Dependency.AssetId.PackageName); } } Edges.Sort(FNameLexicalLess()); } return DirectiveUtilitiesEditor::FDependencyCycleFinder(Graph).Find(); } } TArray UDirectiveUtilEditorAssetAuditLibrary::FindUnreferencedAssetCandidates( const FDirectiveUtilAssetAuditOptions& Options) { IAssetRegistry* AssetRegistry = IAssetRegistry::Get(); if (!AssetRegistry) { return {}; } FAssetAuditScan Scan(*AssetRegistry, Options); return FindUnreferencedCandidates(Scan); } TArray UDirectiveUtilEditorAssetAuditLibrary::FindMissingAssetReferences( const FDirectiveUtilAssetAuditOptions& Options) { IAssetRegistry* AssetRegistry = IAssetRegistry::Get(); if (!AssetRegistry) { return {}; } FAssetAuditScan Scan(*AssetRegistry, Options); return FindMissingReferences(Scan); } TArray UDirectiveUtilEditorAssetAuditLibrary::FindAssetDependencyCycles( const FDirectiveUtilAssetAuditOptions& Options) { IAssetRegistry* AssetRegistry = IAssetRegistry::Get(); if (!AssetRegistry) { return {}; } FAssetAuditScan Scan(*AssetRegistry, Options); return FindDependencyCycles(Scan); } FDirectiveUtilAssetAuditReport UDirectiveUtilEditorAssetAuditLibrary::BuildAssetAuditReport( const FDirectiveUtilAssetAuditOptions& Options) { FDirectiveUtilAssetAuditReport Report; IAssetRegistry* AssetRegistry = IAssetRegistry::Get(); if (!AssetRegistry) { return Report; } FAssetAuditScan Scan(*AssetRegistry, Options); const TArray Candidates = FindUnreferencedCandidates(Scan); Report.MissingReferences = FindMissingReferences(Scan); Report.DependencyCycles = FindDependencyCycles(Scan); TSet CandidatePaths; for (const FAssetData& Candidate : Candidates) { CandidatePaths.Add(Candidate.GetSoftObjectPath()); } TMap MissingCounts; for (const FDirectiveUtilMissingAssetReference& MissingReference : Report.MissingReferences) { ++MissingCounts.FindOrAdd(MissingReference.ReferencingAsset.PackageName); } TSet CyclicPackages; for (const FDirectiveUtilAssetDependencyCycle& Cycle : Report.DependencyCycles) { CyclicPackages.Append(Cycle.Packages); } for (const FAssetData& Asset : Scan.GetAssets()) { FDirectiveUtilAssetAuditEntry& Entry = Report.Assets.AddDefaulted_GetRef(); Entry.Asset = Asset; Entry.PackageName = Asset.PackageName; Entry.PackagePath = Asset.PackagePath; Entry.AssetClass = Asset.AssetClassPath.ToString(); Entry.bPrimaryAsset = IsPrimaryAsset(Asset); Entry.DependencyCount = Scan.GetDependencies(Asset.PackageName).Num(); Entry.ReferencerCount = Scan.GetReferencers(Asset.PackageName).Num(); FString PackageFilename; if (FPackageName::DoesPackageExist(Asset.PackageName.ToString(), &PackageFilename)) { Entry.DiskSize = FMath::Max(IFileManager::Get().FileSize(*PackageFilename), 0); } if (CandidatePaths.Contains(Asset.GetSoftObjectPath())) { Entry.Findings.Add(TEXT("Unreferenced candidate")); } if (const int32* MissingCount = MissingCounts.Find(Asset.PackageName)) { Entry.Findings.Add(FString::Printf(TEXT("%d missing reference%s"), *MissingCount, *MissingCount == 1 ? TEXT("") : TEXT("s"))); } if (CyclicPackages.Contains(Asset.PackageName)) { Entry.Findings.Add(TEXT("Dependency cycle")); } } return Report; } FString UDirectiveUtilEditorAssetAuditLibrary::AssetAuditReportToCsv(const FDirectiveUtilAssetAuditReport& Report) { TArray Rows; Rows.Add(TEXT("Asset,Package,Path,Class,Disk Size,Dependencies,Referencers,Primary Asset,Findings")); for (const FDirectiveUtilAssetAuditEntry& Entry : Report.Assets) { const TArray Columns = { EscapeCsv(Entry.Asset.GetSoftObjectPath().ToString()), EscapeCsv(Entry.PackageName.ToString()), EscapeCsv(Entry.PackagePath.ToString()), EscapeCsv(Entry.AssetClass), LexToString(Entry.DiskSize), LexToString(Entry.DependencyCount), LexToString(Entry.ReferencerCount), Entry.bPrimaryAsset ? TEXT("true") : TEXT("false"), EscapeCsv(FString::Join(Entry.Findings, TEXT("; "))), }; Rows.Add(FString::Join(Columns, TEXT(","))); } Rows.Add(TEXT("")); Rows.Add(TEXT("Missing Reference,Referencing Asset,Dependency Type")); for (const FDirectiveUtilMissingAssetReference& MissingReference : Report.MissingReferences) { const TArray Columns = { EscapeCsv(MissingReference.MissingPackage.ToString()), EscapeCsv(MissingReference.ReferencingAsset.GetSoftObjectPath().ToString()), EscapeCsv(DependencyTypeToString(MissingReference.DependencyType)), }; Rows.Add(FString::Join(Columns, TEXT(","))); } Rows.Add(TEXT("")); Rows.Add(TEXT("Dependency Cycle")); for (const FDirectiveUtilAssetDependencyCycle& Cycle : Report.DependencyCycles) { TArray Packages; for (const FName Package : Cycle.Packages) { Packages.Add(Package.ToString()); } Rows.Add(EscapeCsv(FString::Join(Packages, TEXT(" -> ")))); } return FString::Join(Rows, TEXT("\n")); }