#pragma once #include "CoreMinimal.h" #include "WingServer.h" #include "WingHandler.h" #include "WingFetcher.h" #include "WingUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphPin.h" #include "Blueprint_Diff.generated.h" // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- UCLASS() class UWing_Blueprint_Diff : public UObject, public IWingHandler { GENERATED_BODY() public: UPROPERTY(meta=(Description="First blueprint package path")) FString BlueprintA; UPROPERTY(meta=(Description="Second blueprint 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() override { // Load both blueprints WingFetcher FA; UBlueprint* BPA = FA.Asset(BlueprintA).Cast(); if (!BPA) return; WingFetcher FB; UBlueprint* BPB = FB.Asset(BlueprintB).Cast(); if (!BPB) return; // Gather graphs, optionally filtering by name auto GatherGraphs = [this](UBlueprint* BP) -> TArray { TArray Graphs; for (UEdGraph* G : BP->UbergraphPages) { if (!G) continue; if (!Graph.IsEmpty() && !WingUtils::Identifies(Graph, G)) continue; Graphs.Add(G); } for (UEdGraph* G : BP->FunctionGraphs) { if (!G) continue; if (!Graph.IsEmpty() && !WingUtils::Identifies(Graph, G)) continue; Graphs.Add(G); } return Graphs; }; TArray GraphsA = GatherGraphs(BPA); TArray GraphsB = GatherGraphs(BPB); // Build graph name maps TMap GraphMapA, GraphMapB; for (UEdGraph* G : GraphsA) GraphMapA.Add(WingUtils::FormatName(G), G); for (UEdGraph* G : GraphsB) GraphMapB.Add(WingUtils::FormatName(G), G); // Find all unique graph names TSet 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) { UWingServer::Printf(TEXT("Graph %s: only in B (%d nodes)\n"), *GraphName, (*pGB)->Nodes.Num()); TotalDiffs++; continue; } if (!pGB) { UWingServer::Printf(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> NodesA, NodesB; for (UEdGraphNode* N : GA->Nodes) { if (!N) continue; NodesA.FindOrAdd(WingUtils::FormatName(N)).Add(N); } for (UEdGraphNode* N : GB->Nodes) { if (!N) continue; NodesB.FindOrAdd(WingUtils::FormatName(N)).Add(N); } // Nodes only in A TArray 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 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"), *WingUtils::FormatName(SrcPin->GetOwningNode()), *WingUtils::FormatName(SrcPin), *WingUtils::FormatName(TgtPin->GetOwningNode()), *WingUtils::FormatName(TgtPin)); }; auto GatherConnections = [&MakeConnKey](UEdGraph* G) -> TSet { TSet 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 ConnectionsA = GatherConnections(GA); TSet ConnectionsB = GatherConnections(GB); TArray 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) { UWingServer::Printf(TEXT("Graph %s: identical (%d nodes)\n"), *GraphName, GA->Nodes.Num()); continue; } TotalDiffs++; UWingServer::Printf(TEXT("Graph %s: different (A=%d nodes, B=%d nodes)\n"), *GraphName, GA->Nodes.Num(), GB->Nodes.Num()); if (!OnlyInA.IsEmpty()) { UWingServer::Print(TEXT(" Nodes only in A:\n")); for (const FString& Line : OnlyInA) UWingServer::Printf(TEXT(" %s\n"), *Line); } if (!OnlyInB.IsEmpty()) { UWingServer::Print(TEXT(" Nodes only in B:\n")); for (const FString& Line : OnlyInB) UWingServer::Printf(TEXT(" %s\n"), *Line); } if (!ConnsOnlyInA.IsEmpty()) { UWingServer::Print(TEXT(" Connections only in A:\n")); for (const FString& Line : ConnsOnlyInA) UWingServer::Printf(TEXT(" %s\n"), *Line); } if (!ConnsOnlyInB.IsEmpty()) { UWingServer::Print(TEXT(" Connections only in B:\n")); for (const FString& Line : ConnsOnlyInB) UWingServer::Printf(TEXT(" %s\n"), *Line); } } // Compare variables TSet VarNamesA, VarNamesB; for (const FBPVariableDescription& V : BPA->NewVariables) VarNamesA.Add(WingUtils::FormatName(V)); for (const FBPVariableDescription& V : BPB->NewVariables) VarNamesB.Add(WingUtils::FormatName(V)); TArray 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()) { UWingServer::Print(TEXT("Variables only in A:\n")); for (const FString& Name : VarsOnlyInA) UWingServer::Printf(TEXT(" %s\n"), *Name); TotalDiffs += VarsOnlyInA.Num(); } if (!VarsOnlyInB.IsEmpty()) { UWingServer::Print(TEXT("Variables only in B:\n")); for (const FString& Name : VarsOnlyInB) UWingServer::Printf(TEXT(" %s\n"), *Name); TotalDiffs += VarsOnlyInB.Num(); } UWingServer::Printf(TEXT("Total differences: %d\n"), TotalDiffs); } };