feat: asset deletion scan and delete dialog (WIP)

Adds AssetDeletionScan to Core (no UI/editor-pipeline deps, invariant holds)
and SAssetDeleteDialog to the Editor module, plus 10 spec cases.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-07 17:14:39 +07:00
parent 569ec56a19
commit fbdd0ff5e0
8 changed files with 1734 additions and 0 deletions
@@ -12,6 +12,7 @@
#include "AssetUsageExporter.h"
#include "AssetUsagePaths.h"
#include "AssetUsageReportWriter.h"
#include "SAssetDeleteDialog.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Algo/AllOf.h"
@@ -580,6 +581,33 @@ void SAssetUsageAuditPanel::Construct(const FArguments& InArgs)
.OnClicked(this, &SAssetUsageAuditPanel::OnExportFilesClicked)
.IsEnabled(this, &SAssetUsageAuditPanel::HasCheckedAssets)
]
// Delete sits last and behind a separator, deliberately. It acts on the same ticked
// set as Export, so it belongs on this bar, but it is the only irreversible action in
// the tool and must never be the button someone reaches by muscle memory.
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(8.0f, 2.0f, 2.0f, 2.0f)
[
SNew(SSeparator)
.Orientation(Orient_Vertical)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(2.0f, 0.0f)
[
SNew(SButton)
.Text(LOCTEXT("DeleteTicked", "Delete ticked assets..."))
.ToolTipText(LOCTEXT("DeleteTickedTip",
"Permanently delete the ticked assets from the project. Levels are never deleted. "
"Shows what still references them before anything is removed."))
.ButtonColorAndOpacity(FLinearColor(0.55f, 0.16f, 0.16f))
.OnClicked(this, &SAssetUsageAuditPanel::OnDeleteClicked)
.IsEnabled(this, &SAssetUsageAuditPanel::HasCheckedAssets)
]
]
];
}
@@ -2224,6 +2252,83 @@ FReply SAssetUsageAuditPanel::OnExportFilesClicked()
return FReply::Handled();
}
// --- Deleting ----------------------------------------------------------------------------------
FReply SAssetUsageAuditPanel::OnDeleteClicked()
{
TArray<FAssetUsageRow> Rows;
GetCheckedRows(Rows);
if (Rows.IsEmpty())
{
return FReply::Handled();
}
TArray<FName> Packages;
Packages.Reserve(Rows.Num());
for (const FAssetUsageRow& Row : Rows)
{
Packages.Add(Row.PackageName);
}
// Deliberately NOT widened with the dependency closure, unlike export. Export widens so that a
// mesh arrives with its materials; widening a delete the same way would destroy content nobody
// ticked, and a single shared texture would drag half the project along with it. Delete acts on
// exactly what was ticked, and never on more.
const FAssetDeleteOutcome Outcome = ShowAssetDeleteDialog(Packages);
if (Outcome.bCancelled)
{
StatusText = LOCTEXT("DeleteCancelled", "Delete cancelled - nothing was changed.");
return FReply::Handled();
}
ForgetDeletedPackages(Outcome.DeletedPackages);
StatusText = FText::Format(
LOCTEXT("DeleteFinished",
"Deleted {0} of {1} ticked asset(s); {2} were refused. Reference counts elsewhere in this result are now out of date - run the audit again."),
FText::AsNumber(Outcome.DeletedPackages.Num()),
FText::AsNumber(Outcome.Requested),
FText::AsNumber(Outcome.Refused));
return FReply::Handled();
}
void SAssetUsageAuditPanel::ForgetDeletedPackages(const TArray<FName>& DeletedPackages)
{
if (DeletedPackages.IsEmpty())
{
return;
}
const TSet<FName> Gone(DeletedPackages);
LastResult.Rows.RemoveAll([&Gone](const FAssetUsageRow& Row)
{
return Gone.Contains(Row.PackageName);
});
AllRows.RemoveAll([&Gone](const FAssetUsageRowPtr& Row)
{
return Row.IsValid() && Gone.Contains(Row->PackageName);
});
for (FName Package : DeletedPackages)
{
CheckedPackages.Remove(Package);
}
// Removing the rows is honest but incomplete: everything that referenced them still carries the
// reference counts it had before, and an asset that was UsedByAssetsOnly through a now-deleted
// Blueprint has a different verdict than the one on screen. Marking the result stale says so,
// rather than leaving figures that look freshly measured.
bResultStale = true;
RebuildFilteredRows();
}
void SAssetUsageAuditPanel::RunMigrateExport(const TArray<FAssetUsageRow>& Rows, const FAssetExportRequest& Request)
{
TArray<FName> Packages;