Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AssetMutation.h
2026-03-08 01:47:15 -05:00

244 lines
7.7 KiB
C++

#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UObject/SavePackage.h"
#include "Misc/PackageName.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "MCPHandlers_AssetMutation.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="delete_asset"))
class UMCPHandler_DeleteAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Package path of the asset to delete"))
FString AssetPath;
UPROPERTY(meta=(Optional, Description="If true, skip reference check and force delete"))
bool Force = false;
virtual FString GetDescription() const override
{
return TEXT("Delete a .uasset after verifying no references. "
"Use force=true to skip the reference check.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
// Check if asset file exists on disk
FString PackageFilename = FPackageName::LongPackageNameToFilename(
AssetPath, FPackageName::GetAssetPackageExtension());
PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename);
if (!IFileManager::Get().FileExists(*PackageFilename))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename));
}
// Check references
IAssetRegistry& Registry = *IAssetRegistry::Get();
TArray<FName> Referencers;
Registry.GetReferencers(FName(*AssetPath), Referencers);
// Filter out self-references
Referencers.RemoveAll([this](const FName& Ref) {
return Ref.ToString() == AssetPath;
});
if ((Referencers.Num() > 0) && !Force)
{
// Classify references as "live" (loaded in memory) vs "stale" (only on disk)
TArray<TSharedPtr<FJsonValue>> LiveRefs;
TArray<TSharedPtr<FJsonValue>> StaleRefs;
for (const FName& Ref : Referencers)
{
FString RefStr = Ref.ToString();
UPackage* RefPackage = FindPackage(nullptr, *RefStr);
if (RefPackage)
{
LiveRefs.Add(MakeShared<FJsonValueString>(RefStr));
}
else
{
StaleRefs.Add(MakeShared<FJsonValueString>(RefStr));
}
}
MCPUtils::MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first."));
Result->SetStringField(TEXT("assetPath"), AssetPath);
Result->SetNumberField(TEXT("referencerCount"), Referencers.Num());
Result->SetNumberField(TEXT("liveReferencerCount"), LiveRefs.Num());
Result->SetArrayField(TEXT("liveReferencers"), LiveRefs);
Result->SetNumberField(TEXT("staleReferencerCount"), StaleRefs.Num());
Result->SetArrayField(TEXT("staleReferencers"), StaleRefs);
Result->SetStringField(TEXT("suggestion"),
StaleRefs.Num() > 0
? TEXT("Some references may be stale. Consider force=true to skip the reference check, or use change_variable_type to migrate references first.")
: TEXT("All references are live. Migrate with change_variable_type or replace_function_calls before deleting."));
return;
}
// Force delete: unload the package from memory first
TArray<TSharedPtr<FJsonValue>> RefWarnings;
if (Force)
{
// Collect reference warnings when force-deleting with existing references
for (const FName& Ref : Referencers)
{
RefWarnings.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("Warning: '%s' still references this asset"), *Ref.ToString())));
}
UPackage* Package = FindPackage(nullptr, *AssetPath);
if (Package)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Force-unloading package '%s' from memory"), *AssetPath);
// Collect all objects in this package
TArray<UObject*> ObjectsInPackage;
GetObjectsWithPackage(Package, ObjectsInPackage);
// Clear flags and remove from root to allow GC
for (UObject* Obj : ObjectsInPackage)
{
if (Obj)
{
Obj->ClearFlags(RF_Standalone | RF_Public);
Obj->RemoveFromRoot();
}
}
Package->ClearFlags(RF_Standalone | RF_Public);
Package->RemoveFromRoot();
// Reset loaders to release file handles
ResetLoaders(Package);
// Force garbage collection to free the objects
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting asset '%s' (%s)%s"),
*AssetPath, *PackageFilename, Force ? TEXT(" [FORCE]") : TEXT(""));
// Delete the file on disk
bool bDeleted = IFileManager::Get().Delete(*PackageFilename, false, true);
if (bDeleted)
{
// Trigger an asset registry rescan so it notices the deletion
TArray<FString> PathsToScan;
int32 LastSlash;
if (AssetPath.FindLastChar(TEXT('/'), LastSlash))
{
PathsToScan.Add(AssetPath.Left(LastSlash));
}
if (PathsToScan.Num() > 0)
{
Registry.ScanPathsSynchronous(PathsToScan, true);
}
}
Result->SetStringField(TEXT("filename"), PackageFilename);
if (!bDeleted)
{
MCPUtils::MakeErrorJson(Result, TEXT("Failed to delete file from disk"));
}
if (RefWarnings.Num() > 0)
{
Result->SetArrayField(TEXT("warnings"), RefWarnings);
}
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="rename_asset"))
class UMCPHandler_RenameAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Current package path of the asset"))
FString AssetPath;
UPROPERTY(meta=(Description="New package path or new asset name"))
FString NewPath;
virtual FString GetDescription() const override
{
return TEXT("Rename or move an asset with reference fixup.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming asset '%s' -> '%s'"), *AssetPath, *NewPath);
// Use FAssetToolsModule to perform the rename with reference fixup
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
// Build the source/dest arrays
TArray<FAssetRenameData> RenameData;
// We need to load the asset to get the object
MCPAssets<UObject> Assets;
if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return;
UObject* AssetObj = Assets.Object();
// Parse new path into package path and asset name
FString NewPackagePath, NewAssetName;
int32 LastSlash;
if (NewPath.FindLastChar(TEXT('/'), LastSlash))
{
NewPackagePath = NewPath.Left(LastSlash);
NewAssetName = NewPath.Mid(LastSlash + 1);
}
else
{
// If no slash, assume same directory with new name
FString OldPackagePath;
if (AssetPath.FindLastChar(TEXT('/'), LastSlash))
{
OldPackagePath = AssetPath.Left(LastSlash);
}
NewPackagePath = OldPackagePath;
NewAssetName = NewPath;
}
FAssetRenameData RenameEntry(AssetObj, NewPackagePath, NewAssetName);
RenameData.Add(RenameEntry);
bool bSuccess = AssetTools.RenameAssets(RenameData);
if (bSuccess)
{
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Rename %s"), bSuccess ? TEXT("succeeded") : TEXT("failed"));
Result->SetStringField(TEXT("newPackagePath"), NewPackagePath);
Result->SetStringField(TEXT("newAssetName"), NewAssetName);
if (!bSuccess)
{
MCPUtils::MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist."));
}
}
};