Put handlers in headers files (MCP)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
#include "MCPHandlers_DiffBlueprints.h"
|
||||||
|
#include "MCPHandlers_Interfaces.h"
|
||||||
|
#include "MCPHandlers_Mutation.h"
|
||||||
|
#include "MCPHandlers_PinMutation.h"
|
||||||
|
#include "MCPHandlers_AssetMutation.h"
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
#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->SetBoolField(TEXT("success"), bDeleted);
|
||||||
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
||||||
|
Result->SetStringField(TEXT("filename"), PackageFilename);
|
||||||
|
Result->SetBoolField(TEXT("forced"), Force);
|
||||||
|
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
|
||||||
|
FAssetData* FoundAsset = UMCPAssetFinder::FindAnyAsset(AssetPath);
|
||||||
|
if (!FoundAsset)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset '%s' not found. Checked Blueprints, Materials, Material Instances, and Material Functions."), *AssetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* AssetObj = FoundAsset->GetAsset();
|
||||||
|
if (!AssetObj)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to load asset '%s'"), *AssetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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->SetBoolField(TEXT("success"), bSuccess);
|
||||||
|
Result->SetStringField(TEXT("oldPath"), AssetPath);
|
||||||
|
Result->SetStringField(TEXT("newPath"), NewPath);
|
||||||
|
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."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
#include "MCPHandlers_DiffBlueprints.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPServer.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphNode.h"
|
|
||||||
#include "EdGraph/EdGraphPin.h"
|
|
||||||
|
|
||||||
void UMCPHandler_DiffTwoBlueprints::Handle(const FJsonObject* Json, FJsonObject* Result)
|
|
||||||
{
|
|
||||||
|
|
||||||
// Load both blueprints
|
|
||||||
FString LoadErrorA, LoadErrorB;
|
|
||||||
UBlueprint* BPA = UMCPAssetFinder::LoadBlueprintByName(BlueprintA, LoadErrorA);
|
|
||||||
if (!BPA) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("blueprintA: %s"), *LoadErrorA)); return; }
|
|
||||||
|
|
||||||
UBlueprint* BPB = UMCPAssetFinder::LoadBlueprintByName(BlueprintB, LoadErrorB);
|
|
||||||
if (!BPB) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("blueprintB: %s"), *LoadErrorB)); return; }
|
|
||||||
|
|
||||||
// Helper to gather graphs from a Blueprint
|
|
||||||
auto GatherGraphs = [this](UBlueprint* BP) -> TArray<UEdGraph*>
|
|
||||||
{
|
|
||||||
TArray<UEdGraph*> Graphs;
|
|
||||||
for (UEdGraph* G : BP->UbergraphPages)
|
|
||||||
{
|
|
||||||
if (!G) continue;
|
|
||||||
if (!Graph.IsEmpty() && (G->GetName() != Graph)) continue;
|
|
||||||
Graphs.Add(G);
|
|
||||||
}
|
|
||||||
for (UEdGraph* G : BP->FunctionGraphs)
|
|
||||||
{
|
|
||||||
if (!G) continue;
|
|
||||||
if (!Graph.IsEmpty() && (G->GetName() != Graph)) continue;
|
|
||||||
Graphs.Add(G);
|
|
||||||
}
|
|
||||||
return Graphs;
|
|
||||||
};
|
|
||||||
|
|
||||||
TArray<UEdGraph*> GraphsA = GatherGraphs(BPA);
|
|
||||||
TArray<UEdGraph*> GraphsB = GatherGraphs(BPB);
|
|
||||||
|
|
||||||
// Build graph name maps
|
|
||||||
TMap<FString, UEdGraph*> GraphMapA, GraphMapB;
|
|
||||||
for (UEdGraph* G : GraphsA) GraphMapA.Add(G->GetName(), G);
|
|
||||||
for (UEdGraph* G : GraphsB) GraphMapB.Add(G->GetName(), G);
|
|
||||||
|
|
||||||
// Compare graphs
|
|
||||||
TArray<TSharedPtr<FJsonValue>> GraphDiffs;
|
|
||||||
|
|
||||||
// Find all unique graph names
|
|
||||||
TSet<FString> AllGraphNames;
|
|
||||||
for (auto& Pair : GraphMapA) AllGraphNames.Add(Pair.Key);
|
|
||||||
for (auto& Pair : GraphMapB) AllGraphNames.Add(Pair.Key);
|
|
||||||
|
|
||||||
for (const FString& GraphName : AllGraphNames)
|
|
||||||
{
|
|
||||||
UEdGraph** pGA = GraphMapA.Find(GraphName);
|
|
||||||
UEdGraph** pGB = GraphMapB.Find(GraphName);
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> GD = MakeShared<FJsonObject>();
|
|
||||||
GD->SetStringField(TEXT("graph"), GraphName);
|
|
||||||
|
|
||||||
if (!pGA)
|
|
||||||
{
|
|
||||||
GD->SetStringField(TEXT("status"), TEXT("onlyInB"));
|
|
||||||
GD->SetNumberField(TEXT("nodeCountB"), (*pGB)->Nodes.Num());
|
|
||||||
GraphDiffs.Add(MakeShared<FJsonValueObject>(GD));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!pGB)
|
|
||||||
{
|
|
||||||
GD->SetStringField(TEXT("status"), TEXT("onlyInA"));
|
|
||||||
GD->SetNumberField(TEXT("nodeCountA"), (*pGA)->Nodes.Num());
|
|
||||||
GraphDiffs.Add(MakeShared<FJsonValueObject>(GD));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both exist — compare nodes
|
|
||||||
UEdGraph* GA = *pGA;
|
|
||||||
UEdGraph* GB = *pGB;
|
|
||||||
|
|
||||||
// Build node title maps for matching (title -> node list for each)
|
|
||||||
TMap<FString, TArray<UEdGraphNode*>> NodesA, NodesB;
|
|
||||||
for (UEdGraphNode* N : GA->Nodes)
|
|
||||||
{
|
|
||||||
if (!N) continue;
|
|
||||||
FString Title = N->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
||||||
NodesA.FindOrAdd(Title).Add(N);
|
|
||||||
}
|
|
||||||
for (UEdGraphNode* N : GB->Nodes)
|
|
||||||
{
|
|
||||||
if (!N) continue;
|
|
||||||
FString Title = N->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
||||||
NodesB.FindOrAdd(Title).Add(N);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nodes only in A
|
|
||||||
TArray<TSharedPtr<FJsonValue>> OnlyInA;
|
|
||||||
for (auto& Pair : NodesA)
|
|
||||||
{
|
|
||||||
int32 CountA = Pair.Value.Num();
|
|
||||||
int32 CountB = 0;
|
|
||||||
if (TArray<UEdGraphNode*>* pArr = NodesB.Find(Pair.Key))
|
|
||||||
{
|
|
||||||
CountB = pArr->Num();
|
|
||||||
}
|
|
||||||
if (CountA > CountB)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> NObj = MakeShared<FJsonObject>();
|
|
||||||
NObj->SetStringField(TEXT("title"), Pair.Key);
|
|
||||||
NObj->SetStringField(TEXT("class"), Pair.Value[0]->GetClass()->GetName());
|
|
||||||
NObj->SetNumberField(TEXT("extraCount"), CountA - CountB);
|
|
||||||
OnlyInA.Add(MakeShared<FJsonValueObject>(NObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nodes only in B
|
|
||||||
TArray<TSharedPtr<FJsonValue>> OnlyInB;
|
|
||||||
for (auto& Pair : NodesB)
|
|
||||||
{
|
|
||||||
int32 CountB = Pair.Value.Num();
|
|
||||||
int32 CountA = 0;
|
|
||||||
if (TArray<UEdGraphNode*>* pArr = NodesA.Find(Pair.Key))
|
|
||||||
{
|
|
||||||
CountA = pArr->Num();
|
|
||||||
}
|
|
||||||
if (CountB > CountA)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> NObj = MakeShared<FJsonObject>();
|
|
||||||
NObj->SetStringField(TEXT("title"), Pair.Key);
|
|
||||||
NObj->SetStringField(TEXT("class"), Pair.Value[0]->GetClass()->GetName());
|
|
||||||
NObj->SetNumberField(TEXT("extraCount"), CountB - CountA);
|
|
||||||
OnlyInB.Add(MakeShared<FJsonValueObject>(NObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connection diff: use connection key approach
|
|
||||||
auto MakeConnKey = [](UEdGraphPin* SrcPin, UEdGraphPin* TgtPin) -> FString
|
|
||||||
{
|
|
||||||
FString SrcTitle = SrcPin->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
||||||
FString TgtTitle = TgtPin->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
||||||
return FString::Printf(TEXT("%s|%s|%s|%s"), *SrcTitle, *SrcPin->PinName.ToString(), *TgtTitle, *TgtPin->PinName.ToString());
|
|
||||||
};
|
|
||||||
|
|
||||||
TSet<FString> ConnectionsA, ConnectionsB;
|
|
||||||
for (UEdGraphNode* N : GA->Nodes)
|
|
||||||
{
|
|
||||||
if (!N) continue;
|
|
||||||
for (UEdGraphPin* Pin : N->Pins)
|
|
||||||
{
|
|
||||||
if (!Pin || (Pin->Direction != EGPD_Output)) continue;
|
|
||||||
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
|
||||||
{
|
|
||||||
if (!Linked || !Linked->GetOwningNode()) continue;
|
|
||||||
ConnectionsA.Add(MakeConnKey(Pin, Linked));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (UEdGraphNode* N : GB->Nodes)
|
|
||||||
{
|
|
||||||
if (!N) continue;
|
|
||||||
for (UEdGraphPin* Pin : N->Pins)
|
|
||||||
{
|
|
||||||
if (!Pin || (Pin->Direction != EGPD_Output)) continue;
|
|
||||||
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
|
||||||
{
|
|
||||||
if (!Linked || !Linked->GetOwningNode()) continue;
|
|
||||||
ConnectionsB.Add(MakeConnKey(Pin, Linked));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ConnsOnlyInA, ConnsOnlyInB;
|
|
||||||
for (const FString& Key : ConnectionsA)
|
|
||||||
{
|
|
||||||
if (!ConnectionsB.Contains(Key))
|
|
||||||
{
|
|
||||||
ConnsOnlyInA.Add(MakeShared<FJsonValueString>(Key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const FString& Key : ConnectionsB)
|
|
||||||
{
|
|
||||||
if (!ConnectionsA.Contains(Key))
|
|
||||||
{
|
|
||||||
ConnsOnlyInB.Add(MakeShared<FJsonValueString>(Key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bIdentical = (OnlyInA.Num() == 0) && (OnlyInB.Num() == 0) && (ConnsOnlyInA.Num() == 0) && (ConnsOnlyInB.Num() == 0);
|
|
||||||
GD->SetStringField(TEXT("status"), bIdentical ? TEXT("identical") : TEXT("different"));
|
|
||||||
GD->SetNumberField(TEXT("nodeCountA"), GA->Nodes.Num());
|
|
||||||
GD->SetNumberField(TEXT("nodeCountB"), GB->Nodes.Num());
|
|
||||||
|
|
||||||
if (OnlyInA.Num() > 0) GD->SetArrayField(TEXT("nodesOnlyInA"), OnlyInA);
|
|
||||||
if (OnlyInB.Num() > 0) GD->SetArrayField(TEXT("nodesOnlyInB"), OnlyInB);
|
|
||||||
if (ConnsOnlyInA.Num() > 0) GD->SetArrayField(TEXT("connectionsOnlyInA"), ConnsOnlyInA);
|
|
||||||
if (ConnsOnlyInB.Num() > 0) GD->SetArrayField(TEXT("connectionsOnlyInB"), ConnsOnlyInB);
|
|
||||||
|
|
||||||
GraphDiffs.Add(MakeShared<FJsonValueObject>(GD));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare variables
|
|
||||||
TArray<TSharedPtr<FJsonValue>> VarsOnlyInA, VarsOnlyInB;
|
|
||||||
TSet<FString> VarNamesA, VarNamesB;
|
|
||||||
for (const FBPVariableDescription& V : BPA->NewVariables) VarNamesA.Add(V.VarName.ToString());
|
|
||||||
for (const FBPVariableDescription& V : BPB->NewVariables) VarNamesB.Add(V.VarName.ToString());
|
|
||||||
|
|
||||||
for (const FString& Name : VarNamesA)
|
|
||||||
{
|
|
||||||
if (!VarNamesB.Contains(Name))
|
|
||||||
{
|
|
||||||
VarsOnlyInA.Add(MakeShared<FJsonValueString>(Name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const FString& Name : VarNamesB)
|
|
||||||
{
|
|
||||||
if (!VarNamesA.Contains(Name))
|
|
||||||
{
|
|
||||||
VarsOnlyInB.Add(MakeShared<FJsonValueString>(Name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("success"), true);
|
|
||||||
Result->SetStringField(TEXT("blueprintA"), BlueprintA);
|
|
||||||
Result->SetStringField(TEXT("blueprintB"), BlueprintB);
|
|
||||||
Result->SetArrayField(TEXT("graphs"), GraphDiffs);
|
|
||||||
|
|
||||||
if (VarsOnlyInA.Num() > 0) Result->SetArrayField(TEXT("variablesOnlyInA"), VarsOnlyInA);
|
|
||||||
if (VarsOnlyInB.Num() > 0) Result->SetArrayField(TEXT("variablesOnlyInB"), VarsOnlyInB);
|
|
||||||
|
|
||||||
// Summary counts
|
|
||||||
int32 TotalDiffs = 0;
|
|
||||||
for (auto& GDVal : GraphDiffs)
|
|
||||||
{
|
|
||||||
auto GDObj = GDVal->AsObject();
|
|
||||||
FString Status = GDObj->GetStringField(TEXT("status"));
|
|
||||||
if (Status != TEXT("identical")) TotalDiffs++;
|
|
||||||
}
|
|
||||||
TotalDiffs += VarsOnlyInA.Num() + VarsOnlyInB.Num();
|
|
||||||
Result->SetNumberField(TEXT("totalDifferences"), TotalDiffs);
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,19 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "MCPHandler.h"
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
#include "MCPHandlers_DiffBlueprints.generated.h"
|
#include "MCPHandlers_DiffBlueprints.generated.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
UCLASS(meta=(ToolName="diff_two_blueprints"))
|
UCLASS(meta=(ToolName="diff_two_blueprints"))
|
||||||
class UMCPHandler_DiffTwoBlueprints : public UObject, public IMCPHandler
|
class UMCPHandler_DiffTwoBlueprints : public UObject, public IMCPHandler
|
||||||
{
|
{
|
||||||
@@ -26,5 +37,237 @@ public:
|
|||||||
"finding divergence after copy-paste, or auditing consistency.");
|
"finding divergence after copy-paste, or auditing consistency.");
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
// Load both blueprints
|
||||||
|
FString LoadErrorA, LoadErrorB;
|
||||||
|
UBlueprint* BPA = UMCPAssetFinder::LoadBlueprintByName(BlueprintA, LoadErrorA);
|
||||||
|
if (!BPA) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("blueprintA: %s"), *LoadErrorA)); return; }
|
||||||
|
|
||||||
|
UBlueprint* BPB = UMCPAssetFinder::LoadBlueprintByName(BlueprintB, LoadErrorB);
|
||||||
|
if (!BPB) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("blueprintB: %s"), *LoadErrorB)); return; }
|
||||||
|
|
||||||
|
// Helper to gather graphs from a Blueprint
|
||||||
|
auto GatherGraphs = [this](UBlueprint* BP) -> TArray<UEdGraph*>
|
||||||
|
{
|
||||||
|
TArray<UEdGraph*> Graphs;
|
||||||
|
for (UEdGraph* G : BP->UbergraphPages)
|
||||||
|
{
|
||||||
|
if (!G) continue;
|
||||||
|
if (!Graph.IsEmpty() && (G->GetName() != Graph)) continue;
|
||||||
|
Graphs.Add(G);
|
||||||
|
}
|
||||||
|
for (UEdGraph* G : BP->FunctionGraphs)
|
||||||
|
{
|
||||||
|
if (!G) continue;
|
||||||
|
if (!Graph.IsEmpty() && (G->GetName() != Graph)) continue;
|
||||||
|
Graphs.Add(G);
|
||||||
|
}
|
||||||
|
return Graphs;
|
||||||
|
};
|
||||||
|
|
||||||
|
TArray<UEdGraph*> GraphsA = GatherGraphs(BPA);
|
||||||
|
TArray<UEdGraph*> GraphsB = GatherGraphs(BPB);
|
||||||
|
|
||||||
|
// Build graph name maps
|
||||||
|
TMap<FString, UEdGraph*> GraphMapA, GraphMapB;
|
||||||
|
for (UEdGraph* G : GraphsA) GraphMapA.Add(G->GetName(), G);
|
||||||
|
for (UEdGraph* G : GraphsB) GraphMapB.Add(G->GetName(), G);
|
||||||
|
|
||||||
|
// Compare graphs
|
||||||
|
TArray<TSharedPtr<FJsonValue>> GraphDiffs;
|
||||||
|
|
||||||
|
// Find all unique graph names
|
||||||
|
TSet<FString> AllGraphNames;
|
||||||
|
for (auto& Pair : GraphMapA) AllGraphNames.Add(Pair.Key);
|
||||||
|
for (auto& Pair : GraphMapB) AllGraphNames.Add(Pair.Key);
|
||||||
|
|
||||||
|
for (const FString& GraphName : AllGraphNames)
|
||||||
|
{
|
||||||
|
UEdGraph** pGA = GraphMapA.Find(GraphName);
|
||||||
|
UEdGraph** pGB = GraphMapB.Find(GraphName);
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> GD = MakeShared<FJsonObject>();
|
||||||
|
GD->SetStringField(TEXT("graph"), GraphName);
|
||||||
|
|
||||||
|
if (!pGA)
|
||||||
|
{
|
||||||
|
GD->SetStringField(TEXT("status"), TEXT("onlyInB"));
|
||||||
|
GD->SetNumberField(TEXT("nodeCountB"), (*pGB)->Nodes.Num());
|
||||||
|
GraphDiffs.Add(MakeShared<FJsonValueObject>(GD));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!pGB)
|
||||||
|
{
|
||||||
|
GD->SetStringField(TEXT("status"), TEXT("onlyInA"));
|
||||||
|
GD->SetNumberField(TEXT("nodeCountA"), (*pGA)->Nodes.Num());
|
||||||
|
GraphDiffs.Add(MakeShared<FJsonValueObject>(GD));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both exist — compare nodes
|
||||||
|
UEdGraph* GA = *pGA;
|
||||||
|
UEdGraph* GB = *pGB;
|
||||||
|
|
||||||
|
// Build node title maps for matching (title -> node list for each)
|
||||||
|
TMap<FString, TArray<UEdGraphNode*>> NodesA, NodesB;
|
||||||
|
for (UEdGraphNode* N : GA->Nodes)
|
||||||
|
{
|
||||||
|
if (!N) continue;
|
||||||
|
FString Title = N->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||||
|
NodesA.FindOrAdd(Title).Add(N);
|
||||||
|
}
|
||||||
|
for (UEdGraphNode* N : GB->Nodes)
|
||||||
|
{
|
||||||
|
if (!N) continue;
|
||||||
|
FString Title = N->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||||
|
NodesB.FindOrAdd(Title).Add(N);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes only in A
|
||||||
|
TArray<TSharedPtr<FJsonValue>> OnlyInA;
|
||||||
|
for (auto& Pair : NodesA)
|
||||||
|
{
|
||||||
|
int32 CountA = Pair.Value.Num();
|
||||||
|
int32 CountB = 0;
|
||||||
|
if (TArray<UEdGraphNode*>* pArr = NodesB.Find(Pair.Key))
|
||||||
|
{
|
||||||
|
CountB = pArr->Num();
|
||||||
|
}
|
||||||
|
if (CountA > CountB)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> NObj = MakeShared<FJsonObject>();
|
||||||
|
NObj->SetStringField(TEXT("title"), Pair.Key);
|
||||||
|
NObj->SetStringField(TEXT("class"), Pair.Value[0]->GetClass()->GetName());
|
||||||
|
NObj->SetNumberField(TEXT("extraCount"), CountA - CountB);
|
||||||
|
OnlyInA.Add(MakeShared<FJsonValueObject>(NObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes only in B
|
||||||
|
TArray<TSharedPtr<FJsonValue>> OnlyInB;
|
||||||
|
for (auto& Pair : NodesB)
|
||||||
|
{
|
||||||
|
int32 CountB = Pair.Value.Num();
|
||||||
|
int32 CountA = 0;
|
||||||
|
if (TArray<UEdGraphNode*>* pArr = NodesA.Find(Pair.Key))
|
||||||
|
{
|
||||||
|
CountA = pArr->Num();
|
||||||
|
}
|
||||||
|
if (CountB > CountA)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> NObj = MakeShared<FJsonObject>();
|
||||||
|
NObj->SetStringField(TEXT("title"), Pair.Key);
|
||||||
|
NObj->SetStringField(TEXT("class"), Pair.Value[0]->GetClass()->GetName());
|
||||||
|
NObj->SetNumberField(TEXT("extraCount"), CountB - CountA);
|
||||||
|
OnlyInB.Add(MakeShared<FJsonValueObject>(NObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection diff: use connection key approach
|
||||||
|
auto MakeConnKey = [](UEdGraphPin* SrcPin, UEdGraphPin* TgtPin) -> FString
|
||||||
|
{
|
||||||
|
FString SrcTitle = SrcPin->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||||
|
FString TgtTitle = TgtPin->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||||
|
return FString::Printf(TEXT("%s|%s|%s|%s"), *SrcTitle, *SrcPin->PinName.ToString(), *TgtTitle, *TgtPin->PinName.ToString());
|
||||||
|
};
|
||||||
|
|
||||||
|
TSet<FString> ConnectionsA, ConnectionsB;
|
||||||
|
for (UEdGraphNode* N : GA->Nodes)
|
||||||
|
{
|
||||||
|
if (!N) continue;
|
||||||
|
for (UEdGraphPin* Pin : N->Pins)
|
||||||
|
{
|
||||||
|
if (!Pin || (Pin->Direction != EGPD_Output)) continue;
|
||||||
|
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
||||||
|
{
|
||||||
|
if (!Linked || !Linked->GetOwningNode()) continue;
|
||||||
|
ConnectionsA.Add(MakeConnKey(Pin, Linked));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (UEdGraphNode* N : GB->Nodes)
|
||||||
|
{
|
||||||
|
if (!N) continue;
|
||||||
|
for (UEdGraphPin* Pin : N->Pins)
|
||||||
|
{
|
||||||
|
if (!Pin || (Pin->Direction != EGPD_Output)) continue;
|
||||||
|
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
||||||
|
{
|
||||||
|
if (!Linked || !Linked->GetOwningNode()) continue;
|
||||||
|
ConnectionsB.Add(MakeConnKey(Pin, Linked));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ConnsOnlyInA, ConnsOnlyInB;
|
||||||
|
for (const FString& Key : ConnectionsA)
|
||||||
|
{
|
||||||
|
if (!ConnectionsB.Contains(Key))
|
||||||
|
{
|
||||||
|
ConnsOnlyInA.Add(MakeShared<FJsonValueString>(Key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const FString& Key : ConnectionsB)
|
||||||
|
{
|
||||||
|
if (!ConnectionsA.Contains(Key))
|
||||||
|
{
|
||||||
|
ConnsOnlyInB.Add(MakeShared<FJsonValueString>(Key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bIdentical = (OnlyInA.Num() == 0) && (OnlyInB.Num() == 0) && (ConnsOnlyInA.Num() == 0) && (ConnsOnlyInB.Num() == 0);
|
||||||
|
GD->SetStringField(TEXT("status"), bIdentical ? TEXT("identical") : TEXT("different"));
|
||||||
|
GD->SetNumberField(TEXT("nodeCountA"), GA->Nodes.Num());
|
||||||
|
GD->SetNumberField(TEXT("nodeCountB"), GB->Nodes.Num());
|
||||||
|
|
||||||
|
if (OnlyInA.Num() > 0) GD->SetArrayField(TEXT("nodesOnlyInA"), OnlyInA);
|
||||||
|
if (OnlyInB.Num() > 0) GD->SetArrayField(TEXT("nodesOnlyInB"), OnlyInB);
|
||||||
|
if (ConnsOnlyInA.Num() > 0) GD->SetArrayField(TEXT("connectionsOnlyInA"), ConnsOnlyInA);
|
||||||
|
if (ConnsOnlyInB.Num() > 0) GD->SetArrayField(TEXT("connectionsOnlyInB"), ConnsOnlyInB);
|
||||||
|
|
||||||
|
GraphDiffs.Add(MakeShared<FJsonValueObject>(GD));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare variables
|
||||||
|
TArray<TSharedPtr<FJsonValue>> VarsOnlyInA, VarsOnlyInB;
|
||||||
|
TSet<FString> VarNamesA, VarNamesB;
|
||||||
|
for (const FBPVariableDescription& V : BPA->NewVariables) VarNamesA.Add(V.VarName.ToString());
|
||||||
|
for (const FBPVariableDescription& V : BPB->NewVariables) VarNamesB.Add(V.VarName.ToString());
|
||||||
|
|
||||||
|
for (const FString& Name : VarNamesA)
|
||||||
|
{
|
||||||
|
if (!VarNamesB.Contains(Name))
|
||||||
|
{
|
||||||
|
VarsOnlyInA.Add(MakeShared<FJsonValueString>(Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const FString& Name : VarNamesB)
|
||||||
|
{
|
||||||
|
if (!VarNamesA.Contains(Name))
|
||||||
|
{
|
||||||
|
VarsOnlyInB.Add(MakeShared<FJsonValueString>(Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("success"), true);
|
||||||
|
Result->SetStringField(TEXT("blueprintA"), BlueprintA);
|
||||||
|
Result->SetStringField(TEXT("blueprintB"), BlueprintB);
|
||||||
|
Result->SetArrayField(TEXT("graphs"), GraphDiffs);
|
||||||
|
|
||||||
|
if (VarsOnlyInA.Num() > 0) Result->SetArrayField(TEXT("variablesOnlyInA"), VarsOnlyInA);
|
||||||
|
if (VarsOnlyInB.Num() > 0) Result->SetArrayField(TEXT("variablesOnlyInB"), VarsOnlyInB);
|
||||||
|
|
||||||
|
// Summary counts
|
||||||
|
int32 TotalDiffs = 0;
|
||||||
|
for (auto& GDVal : GraphDiffs)
|
||||||
|
{
|
||||||
|
auto GDObj = GDVal->AsObject();
|
||||||
|
FString Status = GDObj->GetStringField(TEXT("status"));
|
||||||
|
if (Status != TEXT("identical")) TotalDiffs++;
|
||||||
|
}
|
||||||
|
TotalDiffs += VarsOnlyInA.Num() + VarsOnlyInB.Num();
|
||||||
|
Result->SetNumberField(TEXT("totalDifferences"), TotalDiffs);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,255 +0,0 @@
|
|||||||
#include "MCPHandlers_Interfaces.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPServer.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "UObject/UObjectIterator.h"
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// ListInterfaces
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
void UMCPHandler_ListBlueprintInterfaces::Handle(const FJsonObject* Json, FJsonObject* Result)
|
|
||||||
{
|
|
||||||
|
|
||||||
FString LoadError;
|
|
||||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Blueprint, LoadError);
|
|
||||||
if (!BP)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> InterfacesArr;
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (!IfaceDesc.Interface)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> IfaceObj = MakeShared<FJsonObject>();
|
|
||||||
IfaceObj->SetStringField(TEXT("name"), IfaceDesc.Interface->GetName());
|
|
||||||
IfaceObj->SetStringField(TEXT("classPath"), IfaceDesc.Interface->GetPathName());
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> FuncArr;
|
|
||||||
for (const UEdGraph* Graph : IfaceDesc.Graphs)
|
|
||||||
{
|
|
||||||
if (Graph)
|
|
||||||
{
|
|
||||||
FuncArr.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IfaceObj->SetArrayField(TEXT("functions"), FuncArr);
|
|
||||||
|
|
||||||
InterfacesArr.Add(MakeShared<FJsonValueObject>(IfaceObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
|
||||||
Result->SetNumberField(TEXT("count"), InterfacesArr.Num());
|
|
||||||
Result->SetArrayField(TEXT("interfaces"), InterfacesArr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// AddInterface
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
void UMCPHandler_AddBlueprintInterface::Handle(const FJsonObject* Json, FJsonObject* Result)
|
|
||||||
{
|
|
||||||
|
|
||||||
FString LoadError;
|
|
||||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Blueprint, LoadError);
|
|
||||||
if (!BP)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the interface class
|
|
||||||
UClass* InterfaceClass = nullptr;
|
|
||||||
|
|
||||||
// Strategy 1: Search loaded UInterface classes by name
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
if (!It->IsChildOf(UInterface::StaticClass()))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
FString ClassName = It->GetName();
|
|
||||||
// Match by class name (e.g. "BPI_Foo_C") or by trimmed name (e.g. "BPI_Foo")
|
|
||||||
if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
InterfaceClass = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Strip the generated "_C" suffix for comparison
|
|
||||||
FString TrimmedName = ClassName;
|
|
||||||
if (TrimmedName.EndsWith(TEXT("_C")))
|
|
||||||
{
|
|
||||||
TrimmedName = TrimmedName.LeftChop(2);
|
|
||||||
}
|
|
||||||
if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
InterfaceClass = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 2: Try loading as a Blueprint Interface asset
|
|
||||||
if (!InterfaceClass)
|
|
||||||
{
|
|
||||||
FString IfaceLoadError;
|
|
||||||
UBlueprint* IfaceBP = UMCPAssetFinder::LoadBlueprintByName(InterfaceName, IfaceLoadError);
|
|
||||||
if (IfaceBP && IfaceBP->GeneratedClass && IfaceBP->GeneratedClass->IsChildOf(UInterface::StaticClass()))
|
|
||||||
{
|
|
||||||
InterfaceClass = IfaceBP->GeneratedClass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!InterfaceClass)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Interface '%s' not found. Provide a Blueprint Interface asset name (e.g. 'BPI_MyInterface') or a native UInterface class name."),
|
|
||||||
*InterfaceName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicates
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (IfaceDesc.Interface == InterfaceClass)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Interface '%s' is already implemented by Blueprint '%s'"),
|
|
||||||
*InterfaceName, *Blueprint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FTopLevelAssetPath InterfacePath = InterfaceClass->GetClassPathName();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding interface '%s' to Blueprint '%s'"),
|
|
||||||
*InterfaceClass->GetName(), *Blueprint);
|
|
||||||
|
|
||||||
bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath);
|
|
||||||
if (!bAdded)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("FBlueprintEditorUtils::ImplementNewInterface failed for interface '%s' on Blueprint '%s'"),
|
|
||||||
*InterfaceName, *Blueprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect stub function graph names from the newly added interface entry
|
|
||||||
TArray<FString> AddedFunctions;
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (IfaceDesc.Interface == InterfaceClass)
|
|
||||||
{
|
|
||||||
for (const UEdGraph* Graph : IfaceDesc.Graphs)
|
|
||||||
{
|
|
||||||
if (Graph)
|
|
||||||
{
|
|
||||||
AddedFunctions.Add(Graph->GetName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"),
|
|
||||||
*InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num());
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("success"), true);
|
|
||||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
|
||||||
Result->SetStringField(TEXT("interfaceName"), InterfaceClass->GetName());
|
|
||||||
Result->SetStringField(TEXT("interfacePath"), InterfaceClass->GetPathName());
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> FuncArr;
|
|
||||||
for (const FString& FuncName : AddedFunctions)
|
|
||||||
{
|
|
||||||
FuncArr.Add(MakeShared<FJsonValueString>(FuncName));
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("functionGraphsAdded"), FuncArr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// RemoveInterface
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
void UMCPHandler_RemoveBlueprintInterface::Handle(const FJsonObject* Json, FJsonObject* Result)
|
|
||||||
{
|
|
||||||
|
|
||||||
FString LoadError;
|
|
||||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Blueprint, LoadError);
|
|
||||||
if (!BP)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the interface in ImplementedInterfaces by name (case-insensitive)
|
|
||||||
UClass* FoundInterface = nullptr;
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (!IfaceDesc.Interface)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
FString ClassName = IfaceDesc.Interface->GetName();
|
|
||||||
if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
FoundInterface = IfaceDesc.Interface;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Strip "_C" suffix for comparison
|
|
||||||
FString TrimmedName = ClassName;
|
|
||||||
if (TrimmedName.EndsWith(TEXT("_C")))
|
|
||||||
{
|
|
||||||
TrimmedName = TrimmedName.LeftChop(2);
|
|
||||||
}
|
|
||||||
if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
FoundInterface = IfaceDesc.Interface;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!FoundInterface)
|
|
||||||
{
|
|
||||||
// Build helpful error with list of implemented interfaces
|
|
||||||
TArray<TSharedPtr<FJsonValue>> IfaceList;
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (IfaceDesc.Interface)
|
|
||||||
{
|
|
||||||
IfaceList.Add(MakeShared<FJsonValueString>(IfaceDesc.Interface->GetName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Interface '%s' is not implemented by Blueprint '%s'"),
|
|
||||||
*InterfaceName, *Blueprint));
|
|
||||||
Result->SetArrayField(TEXT("implementedInterfaces"), IfaceList);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FTopLevelAssetPath InterfacePath = FoundInterface->GetClassPathName();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing interface '%s' from Blueprint '%s' (preserveFunctions: %s)"),
|
|
||||||
*FoundInterface->GetName(), *Blueprint, PreserveFunctions ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions);
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"),
|
|
||||||
*FoundInterface->GetName(), *Blueprint);
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("success"), true);
|
|
||||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
|
||||||
Result->SetStringField(TEXT("interfaceName"), FoundInterface->GetName());
|
|
||||||
Result->SetBoolField(TEXT("preservedFunctions"), PreserveFunctions);
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,19 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "MCPHandler.h"
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
#include "MCPHandlers_Interfaces.generated.h"
|
#include "MCPHandlers_Interfaces.generated.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
UCLASS(meta=(ToolName="list_blueprint_interfaces"))
|
UCLASS(meta=(ToolName="list_blueprint_interfaces"))
|
||||||
class UMCPHandler_ListBlueprintInterfaces : public UObject, public IMCPHandler
|
class UMCPHandler_ListBlueprintInterfaces : public UObject, public IMCPHandler
|
||||||
{
|
{
|
||||||
@@ -19,9 +30,51 @@ public:
|
|||||||
"including their function graphs.");
|
"including their function graphs.");
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
FString LoadError;
|
||||||
|
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Blueprint, LoadError);
|
||||||
|
if (!BP)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> InterfacesArr;
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (!IfaceDesc.Interface)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> IfaceObj = MakeShared<FJsonObject>();
|
||||||
|
IfaceObj->SetStringField(TEXT("name"), IfaceDesc.Interface->GetName());
|
||||||
|
IfaceObj->SetStringField(TEXT("classPath"), IfaceDesc.Interface->GetPathName());
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> FuncArr;
|
||||||
|
for (const UEdGraph* Graph : IfaceDesc.Graphs)
|
||||||
|
{
|
||||||
|
if (Graph)
|
||||||
|
{
|
||||||
|
FuncArr.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IfaceObj->SetArrayField(TEXT("functions"), FuncArr);
|
||||||
|
|
||||||
|
InterfacesArr.Add(MakeShared<FJsonValueObject>(IfaceObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||||
|
Result->SetNumberField(TEXT("count"), InterfacesArr.Num());
|
||||||
|
Result->SetArrayField(TEXT("interfaces"), InterfacesArr);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
UCLASS(meta=(ToolName="add_blueprint_interface"))
|
UCLASS(meta=(ToolName="add_blueprint_interface"))
|
||||||
class UMCPHandler_AddBlueprintInterface : public UObject, public IMCPHandler
|
class UMCPHandler_AddBlueprintInterface : public UObject, public IMCPHandler
|
||||||
{
|
{
|
||||||
@@ -40,9 +93,130 @@ public:
|
|||||||
"Creates stub function graphs for each interface function.");
|
"Creates stub function graphs for each interface function.");
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
FString LoadError;
|
||||||
|
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Blueprint, LoadError);
|
||||||
|
if (!BP)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the interface class
|
||||||
|
UClass* InterfaceClass = nullptr;
|
||||||
|
|
||||||
|
// Strategy 1: Search loaded UInterface classes by name
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (!It->IsChildOf(UInterface::StaticClass()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ClassName = It->GetName();
|
||||||
|
// Match by class name (e.g. "BPI_Foo_C") or by trimmed name (e.g. "BPI_Foo")
|
||||||
|
if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
InterfaceClass = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Strip the generated "_C" suffix for comparison
|
||||||
|
FString TrimmedName = ClassName;
|
||||||
|
if (TrimmedName.EndsWith(TEXT("_C")))
|
||||||
|
{
|
||||||
|
TrimmedName = TrimmedName.LeftChop(2);
|
||||||
|
}
|
||||||
|
if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
InterfaceClass = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Try loading as a Blueprint Interface asset
|
||||||
|
if (!InterfaceClass)
|
||||||
|
{
|
||||||
|
FString IfaceLoadError;
|
||||||
|
UBlueprint* IfaceBP = UMCPAssetFinder::LoadBlueprintByName(InterfaceName, IfaceLoadError);
|
||||||
|
if (IfaceBP && IfaceBP->GeneratedClass && IfaceBP->GeneratedClass->IsChildOf(UInterface::StaticClass()))
|
||||||
|
{
|
||||||
|
InterfaceClass = IfaceBP->GeneratedClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!InterfaceClass)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Interface '%s' not found. Provide a Blueprint Interface asset name (e.g. 'BPI_MyInterface') or a native UInterface class name."),
|
||||||
|
*InterfaceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicates
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (IfaceDesc.Interface == InterfaceClass)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Interface '%s' is already implemented by Blueprint '%s'"),
|
||||||
|
*InterfaceName, *Blueprint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FTopLevelAssetPath InterfacePath = InterfaceClass->GetClassPathName();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding interface '%s' to Blueprint '%s'"),
|
||||||
|
*InterfaceClass->GetName(), *Blueprint);
|
||||||
|
|
||||||
|
bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath);
|
||||||
|
if (!bAdded)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("FBlueprintEditorUtils::ImplementNewInterface failed for interface '%s' on Blueprint '%s'"),
|
||||||
|
*InterfaceName, *Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect stub function graph names from the newly added interface entry
|
||||||
|
TArray<FString> AddedFunctions;
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (IfaceDesc.Interface == InterfaceClass)
|
||||||
|
{
|
||||||
|
for (const UEdGraph* Graph : IfaceDesc.Graphs)
|
||||||
|
{
|
||||||
|
if (Graph)
|
||||||
|
{
|
||||||
|
AddedFunctions.Add(Graph->GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"),
|
||||||
|
*InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num());
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("success"), true);
|
||||||
|
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||||
|
Result->SetStringField(TEXT("interfaceName"), InterfaceClass->GetName());
|
||||||
|
Result->SetStringField(TEXT("interfacePath"), InterfaceClass->GetPathName());
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> FuncArr;
|
||||||
|
for (const FString& FuncName : AddedFunctions)
|
||||||
|
{
|
||||||
|
FuncArr.Add(MakeShared<FJsonValueString>(FuncName));
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("functionGraphsAdded"), FuncArr);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
UCLASS(meta=(ToolName="remove_blueprint_interface"))
|
UCLASS(meta=(ToolName="remove_blueprint_interface"))
|
||||||
class UMCPHandler_RemoveBlueprintInterface : public UObject, public IMCPHandler
|
class UMCPHandler_RemoveBlueprintInterface : public UObject, public IMCPHandler
|
||||||
{
|
{
|
||||||
@@ -64,5 +238,79 @@ public:
|
|||||||
"Optionally preserve the function graphs as regular functions.");
|
"Optionally preserve the function graphs as regular functions.");
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
FString LoadError;
|
||||||
|
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Blueprint, LoadError);
|
||||||
|
if (!BP)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the interface in ImplementedInterfaces by name (case-insensitive)
|
||||||
|
UClass* FoundInterface = nullptr;
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (!IfaceDesc.Interface)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ClassName = IfaceDesc.Interface->GetName();
|
||||||
|
if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
FoundInterface = IfaceDesc.Interface;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Strip "_C" suffix for comparison
|
||||||
|
FString TrimmedName = ClassName;
|
||||||
|
if (TrimmedName.EndsWith(TEXT("_C")))
|
||||||
|
{
|
||||||
|
TrimmedName = TrimmedName.LeftChop(2);
|
||||||
|
}
|
||||||
|
if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
FoundInterface = IfaceDesc.Interface;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FoundInterface)
|
||||||
|
{
|
||||||
|
// Build helpful error with list of implemented interfaces
|
||||||
|
TArray<TSharedPtr<FJsonValue>> IfaceList;
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (IfaceDesc.Interface)
|
||||||
|
{
|
||||||
|
IfaceList.Add(MakeShared<FJsonValueString>(IfaceDesc.Interface->GetName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Interface '%s' is not implemented by Blueprint '%s'"),
|
||||||
|
*InterfaceName, *Blueprint));
|
||||||
|
Result->SetArrayField(TEXT("implementedInterfaces"), IfaceList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTopLevelAssetPath InterfacePath = FoundInterface->GetClassPathName();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing interface '%s' from Blueprint '%s' (preserveFunctions: %s)"),
|
||||||
|
*FoundInterface->GetName(), *Blueprint, PreserveFunctions ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions);
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"),
|
||||||
|
*FoundInterface->GetName(), *Blueprint);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("success"), true);
|
||||||
|
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||||
|
Result->SetStringField(TEXT("interfaceName"), FoundInterface->GetName());
|
||||||
|
Result->SetBoolField(TEXT("preservedFunctions"), PreserveFunctions);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,427 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "MCPHandlers_PinMutation.generated.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
USTRUCT()
|
||||||
|
struct FSetPinDefaultEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString NodeId;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString PinName;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString Value;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
UCLASS(meta=(ToolName="set_pin_default_values"))
|
||||||
|
class UMCPHandler_SetPinDefaultValues : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Array of {blueprint, nodeId, pinName, value} objects"))
|
||||||
|
FMCPJsonArray Pins;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Set the default value of input pins on nodes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Results;
|
||||||
|
int32 SuccessCount = 0;
|
||||||
|
TSet<UEdGraphNode*> ModifiedNodes;
|
||||||
|
TSet<UBlueprint*> ModifiedBlueprints;
|
||||||
|
|
||||||
|
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
||||||
|
|
||||||
|
FSetPinDefaultEntry Entry;
|
||||||
|
FString PopulateError = MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal);
|
||||||
|
if (!PopulateError.IsEmpty())
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), PopulateError);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntryResult->SetStringField(TEXT("blueprint"), Entry.Blueprint);
|
||||||
|
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
|
||||||
|
EntryResult->SetStringField(TEXT("pinName"), Entry.PinName);
|
||||||
|
|
||||||
|
FString LoadError;
|
||||||
|
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Entry.Blueprint, LoadError);
|
||||||
|
if (!BP)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), LoadError);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraph* Graph = nullptr;
|
||||||
|
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId, &Graph);
|
||||||
|
if (!Node)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
|
||||||
|
if (!Pin)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.NodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pin->Direction != EGPD_Input)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' is an output pin"), *Entry.PinName));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
||||||
|
if (Schema)
|
||||||
|
{
|
||||||
|
FString ValidationError = Schema->IsPinDefaultValid(Pin, Entry.Value, nullptr, FText::GetEmpty());
|
||||||
|
if (!ValidationError.IsEmpty())
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
||||||
|
TEXT("Invalid value for pin '%s': %s"), *Entry.PinName, *ValidationError));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString OldValue = Pin->DefaultValue;
|
||||||
|
Pin->DefaultValue = Entry.Value;
|
||||||
|
|
||||||
|
EntryResult->SetBoolField(TEXT("success"), true);
|
||||||
|
EntryResult->SetStringField(TEXT("oldValue"), OldValue);
|
||||||
|
EntryResult->SetStringField(TEXT("newValue"), Pin->DefaultValue);
|
||||||
|
SuccessCount++;
|
||||||
|
ModifiedNodes.Add(Node);
|
||||||
|
ModifiedBlueprints.Add(BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UEdGraphNode* Node : ModifiedNodes)
|
||||||
|
{
|
||||||
|
Node->ReconstructNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UBlueprint* BP : ModifiedBlueprints)
|
||||||
|
{
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetPinDefault — %d/%d succeeded"),
|
||||||
|
SuccessCount, Pins.Array.Num());
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("success"), true);
|
||||||
|
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
||||||
|
Result->SetNumberField(TEXT("totalCount"), Pins.Array.Num());
|
||||||
|
Result->SetArrayField(TEXT("results"), Results);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
USTRUCT()
|
||||||
|
struct FConnectPinsEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString SourceNodeId;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString SourcePinName;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString TargetNodeId;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString TargetPinName;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
UCLASS(meta=(ToolName="connect_blueprint_pins"))
|
||||||
|
class UMCPHandler_ConnectBlueprintPins : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Array of {sourceNodeId, sourcePinName, targetNodeId, targetPinName} objects"))
|
||||||
|
FMCPJsonArray Connections;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Connect pins between nodes in a Blueprint graph.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
FString LoadError;
|
||||||
|
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Blueprint, LoadError);
|
||||||
|
if (!BP)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Results;
|
||||||
|
int32 SuccessCount = 0;
|
||||||
|
|
||||||
|
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
||||||
|
|
||||||
|
FConnectPinsEntry Entry;
|
||||||
|
FString PopulateError = MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal);
|
||||||
|
if (!PopulateError.IsEmpty())
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), PopulateError);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntryResult->SetStringField(TEXT("sourceNodeId"), Entry.SourceNodeId);
|
||||||
|
EntryResult->SetStringField(TEXT("sourcePinName"), Entry.SourcePinName);
|
||||||
|
EntryResult->SetStringField(TEXT("targetNodeId"), Entry.TargetNodeId);
|
||||||
|
EntryResult->SetStringField(TEXT("targetPinName"), Entry.TargetPinName);
|
||||||
|
|
||||||
|
UEdGraph* SourceGraph = nullptr;
|
||||||
|
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph);
|
||||||
|
if (!SourceNode)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId);
|
||||||
|
if (!TargetNode)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Entry.SourcePinName));
|
||||||
|
if (!SourcePin)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *Entry.SourcePinName, *Entry.SourceNodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
|
||||||
|
if (!TargetPin)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UEdGraphSchema* Schema = SourceGraph->GetSchema();
|
||||||
|
if (!Schema)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), TEXT("Graph schema not found"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
|
||||||
|
if (!bConnected)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
||||||
|
TEXT("Cannot connect %s (%s) to %s (%s) — types are incompatible"),
|
||||||
|
*Entry.SourcePinName, *SourcePin->PinType.PinCategory.ToString(),
|
||||||
|
*Entry.TargetPinName, *TargetPin->PinType.PinCategory.ToString()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntryResult->SetBoolField(TEXT("success"), true);
|
||||||
|
SuccessCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SuccessCount > 0)
|
||||||
|
{
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: ConnectPins — %d/%d succeeded in '%s'"),
|
||||||
|
SuccessCount, Connections.Array.Num(), *Blueprint);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("success"), true);
|
||||||
|
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||||
|
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
||||||
|
Result->SetNumberField(TEXT("totalCount"), Connections.Array.Num());
|
||||||
|
Result->SetArrayField(TEXT("results"), Results);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
USTRUCT()
|
||||||
|
struct FDisconnectPinEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString NodeId;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString PinName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional))
|
||||||
|
FString TargetNodeId;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional))
|
||||||
|
FString TargetPinName;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
UCLASS(meta=(ToolName="disconnect_blueprint_pins"))
|
||||||
|
class UMCPHandler_DisconnectBlueprintPins : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Array of {nodeId, pinName, targetNodeId?, targetPinName?} objects. If target is omitted, all connections on the pin are broken."))
|
||||||
|
FMCPJsonArray Disconnections;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Disconnect pins in a Blueprint graph. "
|
||||||
|
"Can disconnect a specific link or all links on a pin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
FString LoadError;
|
||||||
|
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Blueprint, LoadError);
|
||||||
|
if (!BP)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Results;
|
||||||
|
int32 SuccessCount = 0;
|
||||||
|
int32 TotalDisconnected = 0;
|
||||||
|
|
||||||
|
for (const TSharedPtr<FJsonValue>& DiscVal : Disconnections.Array)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
||||||
|
|
||||||
|
FDisconnectPinEntry Entry;
|
||||||
|
FString PopulateError = MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal);
|
||||||
|
if (!PopulateError.IsEmpty())
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), PopulateError);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
|
||||||
|
EntryResult->SetStringField(TEXT("pinName"), Entry.PinName);
|
||||||
|
|
||||||
|
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId);
|
||||||
|
if (!Node)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
|
||||||
|
if (!Pin)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.NodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 DisconnectedCount = 0;
|
||||||
|
|
||||||
|
if (!Entry.TargetNodeId.IsEmpty() && !Entry.TargetPinName.IsEmpty())
|
||||||
|
{
|
||||||
|
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId);
|
||||||
|
if (!TargetNode)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
|
||||||
|
if (!TargetPin)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNodeId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Pin->LinkedTo.Contains(TargetPin))
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), TEXT("The specified pins are not connected to each other"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pin->BreakLinkTo(TargetPin);
|
||||||
|
DisconnectedCount = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisconnectedCount = Pin->LinkedTo.Num();
|
||||||
|
if (DisconnectedCount > 0)
|
||||||
|
{
|
||||||
|
Pin->BreakAllPinLinks(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EntryResult->SetBoolField(TEXT("success"), true);
|
||||||
|
EntryResult->SetNumberField(TEXT("disconnectedCount"), DisconnectedCount);
|
||||||
|
SuccessCount++;
|
||||||
|
TotalDisconnected += DisconnectedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TotalDisconnected > 0)
|
||||||
|
{
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DisconnectPin — %d/%d succeeded, %d links broken in '%s'"),
|
||||||
|
SuccessCount, Disconnections.Array.Num(), TotalDisconnected, *Blueprint);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("success"), true);
|
||||||
|
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||||
|
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
||||||
|
Result->SetNumberField(TEXT("totalCount"), Disconnections.Array.Num());
|
||||||
|
Result->SetNumberField(TEXT("totalDisconnected"), TotalDisconnected);
|
||||||
|
Result->SetArrayField(TEXT("results"), Results);
|
||||||
|
}
|
||||||
|
};
|
||||||
124
tools/inline-methods.py
Normal file
124
tools/inline-methods.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Inlines method bodies from a .cpp file into the corresponding .h file.
|
||||||
|
|
||||||
|
Scans the .cpp for method definitions matching "ClassName::MethodName(...)"
|
||||||
|
and extracts the body (from opening '{' to closing '}'). Then reads the .h
|
||||||
|
file and replaces matching declaration-only lines (ending with ';') with the
|
||||||
|
declaration (minus ';') followed by the body.
|
||||||
|
|
||||||
|
Usage: python3 tools/inline-methods.py <header.h> <source.cpp>
|
||||||
|
|
||||||
|
Outputs the new header to stdout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def extract_bodies(cpp_lines):
|
||||||
|
"""Extract method bodies from cpp file.
|
||||||
|
Returns dict of (ClassName, MethodName) -> list of body lines (from '{' to '}')."""
|
||||||
|
bodies = {}
|
||||||
|
i = 0
|
||||||
|
while i < len(cpp_lines):
|
||||||
|
# Match lines like: void UMCPHandler_Foo::Handle(...)
|
||||||
|
m = re.match(r'^\S.*?\s+(\w+)::(\w+)\s*\(', cpp_lines[i])
|
||||||
|
if not m:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
class_name = m.group(1)
|
||||||
|
method_name = m.group(2)
|
||||||
|
|
||||||
|
# Skip forward to the line containing '{'
|
||||||
|
while i < len(cpp_lines) and '{' not in cpp_lines[i]:
|
||||||
|
i += 1
|
||||||
|
if i >= len(cpp_lines):
|
||||||
|
break
|
||||||
|
|
||||||
|
# Collect from '{' to matching '}'
|
||||||
|
brace_depth = 0
|
||||||
|
body_lines = []
|
||||||
|
while i < len(cpp_lines):
|
||||||
|
line = cpp_lines[i]
|
||||||
|
brace_depth += line.count('{') - line.count('}')
|
||||||
|
body_lines.append(line)
|
||||||
|
i += 1
|
||||||
|
if brace_depth == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
bodies[(class_name, method_name)] = body_lines
|
||||||
|
|
||||||
|
return bodies
|
||||||
|
|
||||||
|
|
||||||
|
def inline_into_header(h_lines, bodies):
|
||||||
|
"""Replace declaration-only methods in header with inlined bodies."""
|
||||||
|
output = []
|
||||||
|
for line in h_lines:
|
||||||
|
# Match declaration lines like:
|
||||||
|
# \tvirtual void Handle(const FJsonObject* Json, ...) override;
|
||||||
|
# Capture: indent, everything before method name, method name, rest up to ';'
|
||||||
|
m = re.match(r'^(\t+)(.*?\s+)(\w+)\s*(\(.*\)\s*(?:const\s*)?(?:override\s*)?);', line)
|
||||||
|
if m:
|
||||||
|
indent = m.group(1)
|
||||||
|
prefix = m.group(2) # e.g. "virtual void "
|
||||||
|
method_name = m.group(3)
|
||||||
|
params = m.group(4) # e.g. "(const FJsonObject* Json, FJsonObject* Result) override"
|
||||||
|
|
||||||
|
# Find which class we're inside
|
||||||
|
class_name = None
|
||||||
|
for prev in reversed(output):
|
||||||
|
cm = re.match(r'^class\s+(\w+)\s*', prev)
|
||||||
|
if cm:
|
||||||
|
class_name = cm.group(1)
|
||||||
|
break
|
||||||
|
|
||||||
|
if class_name and (class_name, method_name) in bodies:
|
||||||
|
body_lines = bodies[(class_name, method_name)]
|
||||||
|
# Emit the declaration as a definition (replace ';' with body)
|
||||||
|
output.append(f'{indent}{prefix}{method_name}{params}')
|
||||||
|
for bline in body_lines:
|
||||||
|
if bline.strip():
|
||||||
|
output.append(indent + bline)
|
||||||
|
else:
|
||||||
|
output.append('')
|
||||||
|
continue
|
||||||
|
|
||||||
|
output.append(line)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print(f"Usage: {sys.argv[0]} <header.h> <source.cpp>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
h_path = sys.argv[1]
|
||||||
|
cpp_path = sys.argv[2]
|
||||||
|
|
||||||
|
with open(cpp_path) as f:
|
||||||
|
cpp_lines = [line.rstrip('\n') for line in f]
|
||||||
|
|
||||||
|
with open(h_path) as f:
|
||||||
|
h_lines = [line.rstrip('\n') for line in f]
|
||||||
|
|
||||||
|
bodies = extract_bodies(cpp_lines)
|
||||||
|
|
||||||
|
if not bodies:
|
||||||
|
print("No method bodies found in cpp file.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Found {len(bodies)} method(s) to inline:", file=sys.stderr)
|
||||||
|
for (cls, method) in bodies:
|
||||||
|
print(f" {cls}::{method}", file=sys.stderr)
|
||||||
|
|
||||||
|
result = inline_into_header(h_lines, bodies)
|
||||||
|
|
||||||
|
for line in result:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user