244 lines
7.7 KiB
C++
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."));
|
|
}
|
|
}
|
|
};
|