// NextGenium 2026. Asset Usage Audit. #pragma once #include "CoreMinimal.h" #include "AssetUsageAuditTypes.h" #include "Containers/BitArray.h" class FAssetUsageGraph; class IAssetRegistry; struct FLevelUsageResolveOptions { /** Levels to analyse. Empty means every /Script/Engine.World package in the graph. */ TArray LevelPackages; /** * Call ULevel::ScanLevelAssets before traversing each level. * * The external-actor dependency gatherer only reports packages the registry has already * scanned. Skipping this can silently yield an empty external-actor set on a level nobody * has opened this session - which looks exactly like a level that genuinely has no actors. */ bool bScanLevelAssetsFirst = true; /** * Additionally seed the BFS from ULevel::GetExternalActorsPaths / GetExternalObjectsPaths. * * Belt and braces: the gatherer normally puts these edges in the graph already. Keeping the * explicit seed means a stale or partially-scanned registry degrades to a slower correct * answer instead of a fast wrong one, and it lets us tag ExternalActor provenance precisely. */ bool bSeedExternalPackages = true; /** * Follow a reference into another map's contents. * * Off by default, and that default is load-bearing. A map does not only reference its own * sublevels: anything it can reach may name an unrelated map, and on this project it does. * WP_Main -> BP_FirstPersonGameMode -> PDA_MenuSystemConfig -> L_MainLevel drags in the whole * of L_MainLevel, which measured at 9994 of WP_Main's 18136 rows - two thirds of the answer * was another level's content. * * With this off, a foreign map is still reported as referenced, but its contents are attributed * to that map alone. Genuine sublevels and Level Instances are unaffected: they are reached * from the level package itself or from one of its external actor packages, and that crossing * is always allowed. See ShouldCrossIntoLevel in the .cpp. */ bool bTraverseIntoOtherLevels = false; /** Record a human-readable route for each asset. Costs one int32 array per BFS. */ bool bRecordRoutes = true; /** Route strings longer than this many hops are elided in the middle. */ int32 MaxRouteHops = 12; /** Optional progress sink, called once per level with (LevelIndex, TotalLevels). */ TFunction OnLevelProgress; /** Return true to abort the sweep between levels. */ TFunction ShouldAbort; }; struct FLevelUsageStats { int32 LevelsScanned = 0; int32 AssetsReachable = 0; int32 ExternalPackagesSeeded = 0; int32 RedirectorsResolved = 0; /** * Foreign maps referenced but deliberately not expanded into. * * A non-zero value here is the amount of another level's content that would otherwise have * been attributed to this one. Worth surfacing: on WP_Main it was two thirds of the report. */ int32 ForeignLevelsNotExpanded = 0; double ScanLevelAssetsSeconds = 0.0; double TraversalSeconds = 0.0; double TotalSeconds() const { return ScanLevelAssetsSeconds + TraversalSeconds; } }; /** * Which levels reach which assets. * * Reachability is one TBitArray per level over dense package indices. At this project's scale * that is 1207 levels x 80608 bits, roughly 12 MB - cheap enough to hold both query directions * ("assets on this level" and "levels using this asset") without ever re-walking the graph. */ struct ASSETUSAGEAUDITCORE_API FLevelUsageResult { /** Level package names, parallel to LevelReachability. */ TArray LevelPackageNames; /** LevelReachability[L][A] - level L reaches asset A. */ TArray> LevelReachability; /** Union across every level. The primary input to the UsedOnLevel verdict. */ TBitArray<> ReachableFromAnyLevel; /** Accumulated provenance flags per asset index. */ TArray Provenance; /** Incoming edge counts per asset index, split by the Hard property. */ TArray HardReferenceCounts; TArray SoftReferenceCounts; /** Route string per asset index, from the first level that reached it. Empty when not recorded. */ TMap Routes; FLevelUsageStats Stats; /** Levels that reach the given asset index. */ TArray GetLevelsForAsset(int32 AssetIndex) const; bool IsReachableFromAnyLevel(int32 AssetIndex) const { return ReachableFromAnyLevel.IsValidIndex(AssetIndex) && ReachableFromAnyLevel[AssetIndex]; } }; /** * Forward traversal from level packages to everything they pull in. * * Direction matters. Going forward from the map is correct; going backward from an external actor * package to its owning map is not, because PackageDependencyData.cpp:57-96 deliberately strips * the UsedInGame flag off that reverse import by naming convention, so the AssetManager will not * drag a whole map in when something references one actor. Reconstructing ownership from the path * instead is possible but fragile - Content Bundles inject /CB// and External Data Layers * inject /EDL// between the folder and the level path. Forward traversal sidesteps all of it. */ class ASSETUSAGEAUDITCORE_API FLevelUsageResolver { public: FLevelUsageResolver(const FAssetUsageGraph& InGraph, IAssetRegistry& InAssetRegistry); FLevelUsageResult Resolve(const FLevelUsageResolveOptions& Options); private: /** BFS from one level. Marks OutReachable and accumulates provenance and counts. */ void TraverseLevel(int32 LevelIndex, const FLevelUsageResolveOptions& Options, TBitArray<>& OutReachable, FLevelUsageResult& InOutResult, TArray& ScratchPredecessor, TArray& ScratchQueue); /** Dense indices of the __ExternalActors__ / __ExternalObjects__ packages owned by a level. */ void GatherExternalPackageSeeds(int32 LevelIndex, TArray& OutSeeds, FLevelUsageResult& InOutResult) const; FString BuildRouteString(int32 LevelIndex, int32 AssetIndex, const TArray& Predecessor, int32 MaxHops) const; const FAssetUsageGraph& Graph; IAssetRegistry& AssetRegistry; };