242 lines
7.3 KiB
C++
242 lines
7.3 KiB
C++
#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 "UMCPHandler_DiffTwoBlueprints.generated.h"
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS()
|
|
class UMCPHandler_DiffTwoBlueprints : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="First blueprint name or package path"))
|
|
FString BlueprintA;
|
|
|
|
UPROPERTY(meta=(Description="Second blueprint name or package path"))
|
|
FString BlueprintB;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Filter to a specific graph name"))
|
|
FString Graph;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Structural diff between two different Blueprints. Compares nodes, "
|
|
"connections, and variables across graphs. Use for comparing variants, "
|
|
"finding divergence after copy-paste, or auditing consistency.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
|
{
|
|
// Load both blueprints
|
|
MCPAssets<UBlueprint> AssetsA;
|
|
if (!AssetsA.Exact(BlueprintA).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BPA = AssetsA.Object();
|
|
|
|
MCPAssets<UBlueprint> AssetsB;
|
|
if (!AssetsB.Exact(BlueprintB).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BPB = AssetsB.Object();
|
|
|
|
// Gather graphs, optionally filtering by name
|
|
auto GatherGraphs = [this](UBlueprint* BP) -> TArray<UEdGraph*>
|
|
{
|
|
TArray<UEdGraph*> Graphs;
|
|
for (UEdGraph* G : BP->UbergraphPages)
|
|
{
|
|
if (!G) continue;
|
|
if (!Graph.IsEmpty() && !MCPUtils::Identifies(Graph, G)) continue;
|
|
Graphs.Add(G);
|
|
}
|
|
for (UEdGraph* G : BP->FunctionGraphs)
|
|
{
|
|
if (!G) continue;
|
|
if (!Graph.IsEmpty() && !MCPUtils::Identifies(Graph, G)) 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(MCPUtils::FormatName(G), G);
|
|
for (UEdGraph* G : GraphsB) GraphMapB.Add(MCPUtils::FormatName(G), G);
|
|
|
|
// Find all unique graph names
|
|
TSet<FString> AllGraphNames;
|
|
for (auto& Pair : GraphMapA) AllGraphNames.Add(Pair.Key);
|
|
for (auto& Pair : GraphMapB) AllGraphNames.Add(Pair.Key);
|
|
|
|
int32 TotalDiffs = 0;
|
|
|
|
for (const FString& GraphName : AllGraphNames)
|
|
{
|
|
UEdGraph** pGA = GraphMapA.Find(GraphName);
|
|
UEdGraph** pGB = GraphMapB.Find(GraphName);
|
|
|
|
if (!pGA)
|
|
{
|
|
Result.Appendf(TEXT("Graph %s: only in B (%d nodes)\n"), *GraphName, (*pGB)->Nodes.Num());
|
|
TotalDiffs++;
|
|
continue;
|
|
}
|
|
if (!pGB)
|
|
{
|
|
Result.Appendf(TEXT("Graph %s: only in A (%d nodes)\n"), *GraphName, (*pGA)->Nodes.Num());
|
|
TotalDiffs++;
|
|
continue;
|
|
}
|
|
|
|
// Both exist -- compare nodes
|
|
UEdGraph* GA = *pGA;
|
|
UEdGraph* GB = *pGB;
|
|
|
|
// Build node title maps for matching
|
|
TMap<FString, TArray<UEdGraphNode*>> NodesA, NodesB;
|
|
for (UEdGraphNode* N : GA->Nodes)
|
|
{
|
|
if (!N) continue;
|
|
NodesA.FindOrAdd(MCPUtils::FormatName(N)).Add(N);
|
|
}
|
|
for (UEdGraphNode* N : GB->Nodes)
|
|
{
|
|
if (!N) continue;
|
|
NodesB.FindOrAdd(MCPUtils::FormatName(N)).Add(N);
|
|
}
|
|
|
|
// Nodes only in A
|
|
TArray<FString> OnlyInA;
|
|
for (auto& Pair : NodesA)
|
|
{
|
|
int32 CountA = Pair.Value.Num();
|
|
int32 CountB = 0;
|
|
if (auto* pArr = NodesB.Find(Pair.Key)) CountB = pArr->Num();
|
|
if (CountA > CountB)
|
|
OnlyInA.Add(FString::Printf(TEXT(" %s (x%d)"), *Pair.Key, CountA - CountB));
|
|
}
|
|
|
|
// Nodes only in B
|
|
TArray<FString> OnlyInB;
|
|
for (auto& Pair : NodesB)
|
|
{
|
|
int32 CountB = Pair.Value.Num();
|
|
int32 CountA = 0;
|
|
if (auto* pArr = NodesA.Find(Pair.Key)) CountA = pArr->Num();
|
|
if (CountB > CountA)
|
|
OnlyInB.Add(FString::Printf(TEXT(" %s (x%d)"), *Pair.Key, CountB - CountA));
|
|
}
|
|
|
|
// Connection diff
|
|
auto MakeConnKey = [](UEdGraphPin* SrcPin, UEdGraphPin* TgtPin) -> FString
|
|
{
|
|
return FString::Printf(TEXT("%s|%s|%s|%s"),
|
|
*MCPUtils::FormatName(SrcPin->GetOwningNode()), *MCPUtils::FormatName(SrcPin),
|
|
*MCPUtils::FormatName(TgtPin->GetOwningNode()), *MCPUtils::FormatName(TgtPin));
|
|
};
|
|
|
|
auto GatherConnections = [&MakeConnKey](UEdGraph* G) -> TSet<FString>
|
|
{
|
|
TSet<FString> Conns;
|
|
for (UEdGraphNode* N : G->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;
|
|
Conns.Add(MakeConnKey(Pin, Linked));
|
|
}
|
|
}
|
|
}
|
|
return Conns;
|
|
};
|
|
|
|
TSet<FString> ConnectionsA = GatherConnections(GA);
|
|
TSet<FString> ConnectionsB = GatherConnections(GB);
|
|
|
|
TArray<FString> ConnsOnlyInA, ConnsOnlyInB;
|
|
for (const FString& Key : ConnectionsA)
|
|
if (!ConnectionsB.Contains(Key))
|
|
ConnsOnlyInA.Add(FString::Printf(TEXT(" %s"), *Key));
|
|
for (const FString& Key : ConnectionsB)
|
|
if (!ConnectionsA.Contains(Key))
|
|
ConnsOnlyInB.Add(FString::Printf(TEXT(" %s"), *Key));
|
|
|
|
bool bIdentical = OnlyInA.IsEmpty() && OnlyInB.IsEmpty() && ConnsOnlyInA.IsEmpty() && ConnsOnlyInB.IsEmpty();
|
|
|
|
if (bIdentical)
|
|
{
|
|
Result.Appendf(TEXT("Graph %s: identical (%d nodes)\n"), *GraphName, GA->Nodes.Num());
|
|
continue;
|
|
}
|
|
|
|
TotalDiffs++;
|
|
Result.Appendf(TEXT("Graph %s: different (A=%d nodes, B=%d nodes)\n"), *GraphName, GA->Nodes.Num(), GB->Nodes.Num());
|
|
|
|
if (!OnlyInA.IsEmpty())
|
|
{
|
|
Result.Append(TEXT(" Nodes only in A:\n"));
|
|
for (const FString& Line : OnlyInA) Result.Appendf(TEXT(" %s\n"), *Line);
|
|
}
|
|
if (!OnlyInB.IsEmpty())
|
|
{
|
|
Result.Append(TEXT(" Nodes only in B:\n"));
|
|
for (const FString& Line : OnlyInB) Result.Appendf(TEXT(" %s\n"), *Line);
|
|
}
|
|
if (!ConnsOnlyInA.IsEmpty())
|
|
{
|
|
Result.Append(TEXT(" Connections only in A:\n"));
|
|
for (const FString& Line : ConnsOnlyInA) Result.Appendf(TEXT(" %s\n"), *Line);
|
|
}
|
|
if (!ConnsOnlyInB.IsEmpty())
|
|
{
|
|
Result.Append(TEXT(" Connections only in B:\n"));
|
|
for (const FString& Line : ConnsOnlyInB) Result.Appendf(TEXT(" %s\n"), *Line);
|
|
}
|
|
}
|
|
|
|
// Compare variables
|
|
TSet<FString> VarNamesA, VarNamesB;
|
|
for (const FBPVariableDescription& V : BPA->NewVariables) VarNamesA.Add(MCPUtils::FormatName(V));
|
|
for (const FBPVariableDescription& V : BPB->NewVariables) VarNamesB.Add(MCPUtils::FormatName(V));
|
|
|
|
TArray<FString> VarsOnlyInA, VarsOnlyInB;
|
|
for (const FString& Name : VarNamesA)
|
|
if (!VarNamesB.Contains(Name))
|
|
VarsOnlyInA.Add(Name);
|
|
for (const FString& Name : VarNamesB)
|
|
if (!VarNamesA.Contains(Name))
|
|
VarsOnlyInB.Add(Name);
|
|
|
|
if (!VarsOnlyInA.IsEmpty())
|
|
{
|
|
Result.Append(TEXT("Variables only in A:\n"));
|
|
for (const FString& Name : VarsOnlyInA) Result.Appendf(TEXT(" %s\n"), *Name);
|
|
TotalDiffs += VarsOnlyInA.Num();
|
|
}
|
|
if (!VarsOnlyInB.IsEmpty())
|
|
{
|
|
Result.Append(TEXT("Variables only in B:\n"));
|
|
for (const FString& Name : VarsOnlyInB) Result.Appendf(TEXT(" %s\n"), *Name);
|
|
TotalDiffs += VarsOnlyInB.Num();
|
|
}
|
|
|
|
Result.Appendf(TEXT("Total differences: %d\n"), TotalDiffs);
|
|
}
|
|
};
|