feat: asset usage audit plugin
Editor tool for the LA and 3D departments: which assets each level uses, which are used nowhere, and export of a chosen set. Three modules. AssetUsageAuditCore holds the whole analysis and links no UI and no editor-only asset pipeline, so it stays runnable from a commandlet; AssetUsageAuditEditor holds the Slate panel and everything that needs UnrealEd or AssetTools; AssetUsageAuditTests holds 152 specs. Load-bearing decisions, each of which produces a wrong answer if undone: - Dependency queries are always Package + NoRequirements, never Hard. The map-to-external-actor edges the OFPA gatherer emits carry Game|Build without Hard, so a Hard query drops all 16117 external actor packages in this project. There is deliberately no Hard constant in the code. - Crossing into another map is allowed only from a level or its external actor package. Without that rule WP_Main reported 18136 assets, of which 9994 belonged to L_MainLevel, reached through the GameMode. - The verdict has five states, never a bool. The registry cannot see FMOD events, DataTable rows or string-built paths; those are Unknown, and the tool never proposes a deletion. - Copying .uasset files does not preserve references - they are stored as full package paths. Only the Migrate layout produces something Unreal can open; the others write a manifest so the graph can be rebuilt. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// NextGenium 2026. Asset Usage Audit.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class IAssetRegistry;
|
||||
|
||||
/**
|
||||
* Everything a set of packages needs in order to be usable somewhere else.
|
||||
*
|
||||
* Exists because "export the ticked meshes" without this produces meshes that open pink: the
|
||||
* package file of a StaticMesh holds no materials and no textures, only references to them. The
|
||||
* same applies to a Niagara system and its sprites, and to a Blueprint and everything it spawns.
|
||||
*
|
||||
* Deliberately a separate step rather than something FAssetUsageExporter does internally. The
|
||||
* exporter stays a dumb file copier that needs no Asset Registry and is testable without one, and
|
||||
* the caller keeps the chance to show "40 ticked, 380 will be written" before anything is copied -
|
||||
* which matters, because that ratio surprises people.
|
||||
*
|
||||
* This is NOT IAssetTools::MigratePackages. Migrate walks the same closure but also decides where
|
||||
* files go, prompts, and cannot be told to stop at a folder boundary. Here the traversal is bounded
|
||||
* by the same exclusion list the audit uses, and engine content is left out by default because the
|
||||
* destination project already has it.
|
||||
*/
|
||||
namespace AssetDependencyClosure
|
||||
{
|
||||
struct FOptions
|
||||
{
|
||||
/**
|
||||
* Package prefixes not to descend into, in either /Game/... or Content/... form.
|
||||
* Normally the audit's own exclusion list, so an export cannot pull in content the report
|
||||
* deliberately ignores.
|
||||
*/
|
||||
TArray<FString> ExcludePackagePaths;
|
||||
|
||||
/**
|
||||
* Include /Engine and /Temp packages in the result.
|
||||
*
|
||||
* Off by default. A mesh using DefaultMaterial genuinely depends on /Engine content, but
|
||||
* copying it into the export folder is almost never what someone wants - the destination
|
||||
* project ships the same file, and overwriting it there is worse than useless.
|
||||
*/
|
||||
bool bIncludeEnginePackages = false;
|
||||
|
||||
/**
|
||||
* Stop after this many hops from a seed. Zero means no limit.
|
||||
*
|
||||
* A limit is a blunt instrument and changes the answer rather than just shortening it, so it
|
||||
* is off by default. It exists for the "just the materials, not the whole graph" case.
|
||||
*/
|
||||
int32 MaxDepth = 0;
|
||||
|
||||
/** Called as (Visited, Queued). Return false to stop; the partial result is still returned. */
|
||||
TFunction<bool(int32, int32)> OnProgress;
|
||||
};
|
||||
|
||||
// Exported: Summarise is defined out of line, so the Editor module cannot link without this.
|
||||
struct ASSETUSAGEAUDITCORE_API FStats
|
||||
{
|
||||
/** Seeds that were valid package names to begin with. */
|
||||
int32 SeedCount = 0;
|
||||
|
||||
/** Size of the returned array, seeds included. */
|
||||
int32 TotalCount = 0;
|
||||
|
||||
/** Reached but dropped, per reason. Kept apart so a surprising result can be explained. */
|
||||
int32 SkippedScript = 0;
|
||||
int32 SkippedEngine = 0;
|
||||
int32 SkippedExcluded = 0;
|
||||
|
||||
/**
|
||||
* Maps reached through an ordinary asset rather than through a level, and therefore not
|
||||
* followed - nor included.
|
||||
*
|
||||
* The same rule FLevelUsageResolver applies, and for the same measured reason: on WP_Main
|
||||
* the chain BP_FirstPersonGameMode -> PDA_MenuSystemConfig -> L_MainLevel drags in 9994
|
||||
* packages belonging to a different map. Without this the export dialog would quote an
|
||||
* honest number for a wrong set.
|
||||
*
|
||||
* Excluded rather than merely not expanded, unlike in the audit. The exporter expands any
|
||||
* .umap it is handed into its One File Per Actor packages, so including the map file would
|
||||
* pull the foreign level's contents back in through the exporter instead of the closure.
|
||||
*/
|
||||
int32 ForeignLevelsSkipped = 0;
|
||||
|
||||
/** Deepest hop count actually reached. Tells you whether MaxDepth did anything. */
|
||||
int32 DeepestHop = 0;
|
||||
|
||||
bool bStoppedEarly = false;
|
||||
|
||||
FString Summarise() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Seeds plus everything they depend on, transitively.
|
||||
*
|
||||
* Seeds come first and in their original order, so a caller can still tell what was asked for.
|
||||
* The traversal uses AssetUsageAudit::MakeTraversalQuery() - package category, no requirements -
|
||||
* for the same reason the audit does: a Hard-only query silently drops every soft edge, which
|
||||
* includes every One File Per Actor package and every TSoftObjectPtr a Blueprint resolves.
|
||||
*/
|
||||
/** One ticked asset and everything it pulls in, the seed first. */
|
||||
struct FSeedClosure
|
||||
{
|
||||
FName Seed;
|
||||
TArray<FName> Packages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Closure per seed, rather than one closure over all of them.
|
||||
*
|
||||
* Needed by the folder-per-asset layout: a texture shared by forty meshes has to appear in all
|
||||
* forty folders, and the flat Gather deliberately returns it once. Costs one traversal per
|
||||
* seed, so it is the slower call by construction - use Gather when a single list will do.
|
||||
*/
|
||||
ASSETUSAGEAUDITCORE_API TArray<FSeedClosure> GatherPerSeed(
|
||||
IAssetRegistry& Registry,
|
||||
const TArray<FName>& Seeds,
|
||||
const FOptions& Options,
|
||||
FStats& OutStats);
|
||||
|
||||
ASSETUSAGEAUDITCORE_API TArray<FName> Gather(
|
||||
IAssetRegistry& Registry,
|
||||
const TArray<FName>& Seeds,
|
||||
const FOptions& Options,
|
||||
FStats& OutStats);
|
||||
}
|
||||
Reference in New Issue
Block a user