@duytransipher/gitnexus 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,465 +1,930 @@
1
- #include "GitNexusBlueprintAnalyzerCommandlet.h"
2
-
3
- #include "AssetRegistry/AssetRegistryModule.h"
4
- #include "AssetRegistry/IAssetRegistry.h"
5
- #include "Blueprint/BlueprintSupport.h"
6
- #include "Blueprint/UserWidget.h"
7
- #include "Dom/JsonObject.h"
8
- #include "EdGraph/EdGraph.h"
9
- #include "EdGraph/EdGraphNode.h"
10
- #include "EdGraph/EdGraphPin.h"
11
- #include "Engine/Blueprint.h"
12
- #include "K2Node_CallFunction.h"
13
- #include "K2Node_Event.h"
14
- #include "Kismet2/BlueprintEditorUtils.h"
15
- #include "Misc/FileHelper.h"
16
- #include "Misc/Guid.h"
17
- #include "Misc/PackageName.h"
18
- #include "Misc/Paths.h"
19
- #include "Serialization/JsonReader.h"
20
- #include "Serialization/JsonSerializer.h"
21
- #include "UObject/SoftObjectPath.h"
22
-
23
- UGitNexusBlueprintAnalyzerCommandlet::UGitNexusBlueprintAnalyzerCommandlet()
24
- {
25
- IsClient = false;
26
- IsEditor = true;
27
- LogToConsole = true;
28
- ShowErrorCount = true;
29
- }
30
-
31
- int32 UGitNexusBlueprintAnalyzerCommandlet::Main(const FString& Params)
32
- {
33
- FString Operation;
34
- FString OutputJsonPath;
35
- FParse::Value(*Params, TEXT("Operation="), Operation);
36
- FParse::Value(*Params, TEXT("OutputJson="), OutputJsonPath);
37
-
38
- if (Operation.IsEmpty() || OutputJsonPath.IsEmpty())
39
- {
40
- UE_LOG(LogTemp, Error, TEXT("GitNexusBlueprintAnalyzer requires Operation= and OutputJson= parameters."));
41
- return 1;
42
- }
43
-
44
- if (Operation.Equals(TEXT("SyncAssets"), ESearchCase::IgnoreCase))
45
- {
46
- return RunSyncAssets(OutputJsonPath);
47
- }
48
-
49
- if (Operation.Equals(TEXT("FindNativeBlueprintReferences"), ESearchCase::IgnoreCase))
50
- {
51
- FString CandidatesJsonPath;
52
- FString TargetSymbolKey;
53
- FString TargetClassName;
54
- FString TargetFunctionName;
55
- FParse::Value(*Params, TEXT("CandidatesJson="), CandidatesJsonPath);
56
- FParse::Value(*Params, TEXT("TargetSymbolKey="), TargetSymbolKey);
57
- FParse::Value(*Params, TEXT("TargetClass="), TargetClassName);
58
- FParse::Value(*Params, TEXT("TargetFunction="), TargetFunctionName);
59
- return RunFindNativeBlueprintReferences(OutputJsonPath, CandidatesJsonPath, TargetSymbolKey, TargetClassName, TargetFunctionName);
60
- }
61
-
62
- if (Operation.Equals(TEXT("ExpandBlueprintChain"), ESearchCase::IgnoreCase))
63
- {
64
- FString AssetPath;
65
- FString ChainAnchorId;
66
- FString Direction = TEXT("downstream");
67
- int32 MaxDepth = 5;
68
- FParse::Value(*Params, TEXT("AssetPath="), AssetPath);
69
- FParse::Value(*Params, TEXT("ChainAnchorId="), ChainAnchorId);
70
- FParse::Value(*Params, TEXT("Direction="), Direction);
71
- FParse::Value(*Params, TEXT("MaxDepth="), MaxDepth);
72
- return RunExpandBlueprintChain(OutputJsonPath, AssetPath, ChainAnchorId, Direction, MaxDepth);
73
- }
74
-
75
- UE_LOG(LogTemp, Error, TEXT("Unsupported GitNexusBlueprintAnalyzer operation: %s"), *Operation);
76
- return 1;
77
- }
78
-
79
- int32 UGitNexusBlueprintAnalyzerCommandlet::RunSyncAssets(const FString& OutputJsonPath)
80
- {
81
- TArray<TSharedPtr<FJsonValue>> AssetValues;
82
-
83
- for (const FAssetData& AssetData : GetAllBlueprintAssets())
84
- {
85
- UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
86
- if (!Blueprint)
87
- {
88
- continue;
89
- }
90
-
91
- TSharedPtr<FJsonObject> AssetObject = MakeShared<FJsonObject>();
92
- AssetObject->SetStringField(TEXT("asset_path"), AssetData.GetSoftObjectPath().ToString());
93
-
94
- if (Blueprint->GeneratedClass)
95
- {
96
- AssetObject->SetStringField(TEXT("generated_class"), Blueprint->GeneratedClass->GetPathName());
97
- }
98
-
99
- if (Blueprint->ParentClass)
100
- {
101
- AssetObject->SetStringField(TEXT("parent_class"), Blueprint->ParentClass->GetPathName());
102
- }
103
-
104
- TArray<TSharedPtr<FJsonValue>> NativeParents;
105
- for (UClass* Class = Blueprint->ParentClass; Class; Class = Class->GetSuperClass())
106
- {
107
- if (Class->ClassGeneratedBy == nullptr)
108
- {
109
- NativeParents.Add(MakeShared<FJsonValueString>(Class->GetName()));
110
- }
111
- }
112
- AssetObject->SetArrayField(TEXT("native_parents"), NativeParents);
113
-
114
- TArray<FName> Dependencies;
115
- IAssetRegistry::GetChecked().GetDependencies(AssetData.PackageName, Dependencies, UE::AssetRegistry::EDependencyCategory::Package);
116
- TArray<TSharedPtr<FJsonValue>> DependencyValues;
117
- for (const FName& Dependency : Dependencies)
118
- {
119
- DependencyValues.Add(MakeShared<FJsonValueString>(Dependency.ToString()));
120
- }
121
- AssetObject->SetArrayField(TEXT("dependencies"), DependencyValues);
122
-
123
- TArray<UEdGraph*> Graphs;
124
- CollectBlueprintGraphs(Blueprint, Graphs);
125
- TSet<FString> NativeFunctionRefs;
126
- for (const UEdGraph* Graph : Graphs)
127
- {
128
- if (!Graph)
129
- {
130
- continue;
131
- }
132
-
133
- for (const UEdGraphNode* Node : Graph->Nodes)
134
- {
135
- if (const UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Node))
136
- {
137
- if (const UFunction* TargetFunction = CallNode->GetTargetFunction())
138
- {
139
- const UClass* OwnerClass = TargetFunction->GetOwnerClass();
140
- const FString SymbolKey = OwnerClass
141
- ? FString::Printf(TEXT("%s::%s"), *OwnerClass->GetName(), *TargetFunction->GetName())
142
- : TargetFunction->GetName();
143
- NativeFunctionRefs.Add(SymbolKey);
144
- }
145
- }
146
- }
147
- }
148
-
149
- TArray<TSharedPtr<FJsonValue>> NativeFunctionRefValues;
150
- for (const FString& Ref : NativeFunctionRefs)
151
- {
152
- NativeFunctionRefValues.Add(MakeShared<FJsonValueString>(Ref));
153
- }
154
- AssetObject->SetArrayField(TEXT("native_function_refs"), NativeFunctionRefValues);
155
-
156
- AssetValues.Add(MakeShared<FJsonValueObject>(AssetObject));
157
- }
158
-
159
- TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
160
- RootObject->SetNumberField(TEXT("version"), 1);
161
- RootObject->SetStringField(TEXT("generated_at"), FDateTime::UtcNow().ToIso8601());
162
- RootObject->SetStringField(TEXT("project_path"), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()));
163
- RootObject->SetArrayField(TEXT("assets"), AssetValues);
164
-
165
- return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
166
- }
167
-
168
- int32 UGitNexusBlueprintAnalyzerCommandlet::RunFindNativeBlueprintReferences(
169
- const FString& OutputJsonPath,
170
- const FString& CandidatesJsonPath,
171
- const FString& TargetSymbolKey,
172
- const FString& TargetClassName,
173
- const FString& TargetFunctionName
174
- )
175
- {
176
- TArray<TSharedPtr<FJsonValue>> ConfirmedReferences;
177
- TArray<FString> CandidateAssets = LoadCandidateAssets(CandidatesJsonPath);
178
-
179
- for (const FString& AssetPath : CandidateAssets)
180
- {
181
- UBlueprint* Blueprint = LoadBlueprintFromAssetPath(AssetPath);
182
- if (!Blueprint)
183
- {
184
- continue;
185
- }
186
-
187
- TArray<UEdGraph*> Graphs;
188
- CollectBlueprintGraphs(Blueprint, Graphs);
189
- for (const UEdGraph* Graph : Graphs)
190
- {
191
- if (!Graph)
192
- {
193
- continue;
194
- }
195
-
196
- for (const UEdGraphNode* Node : Graph->Nodes)
197
- {
198
- if (Node && IsTargetFunctionNode(Node, TargetSymbolKey, TargetClassName, TargetFunctionName))
199
- {
200
- ConfirmedReferences.Add(MakeShared<FJsonValueObject>(BuildReferenceJson(Blueprint, Graph, Node)));
201
- }
202
- }
203
- }
204
- }
205
-
206
- TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
207
- TSharedPtr<FJsonObject> TargetObject = MakeShared<FJsonObject>();
208
- TargetObject->SetStringField(TEXT("symbol_key"), TargetSymbolKey);
209
- TargetObject->SetStringField(TEXT("class_name"), TargetClassName);
210
- TargetObject->SetStringField(TEXT("symbol_name"), TargetFunctionName);
211
- RootObject->SetObjectField(TEXT("target_function"), TargetObject);
212
- RootObject->SetNumberField(TEXT("candidates_scanned"), CandidateAssets.Num());
213
- RootObject->SetArrayField(TEXT("confirmed_references"), ConfirmedReferences);
214
-
215
- return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
216
- }
217
-
218
- int32 UGitNexusBlueprintAnalyzerCommandlet::RunExpandBlueprintChain(
219
- const FString& OutputJsonPath,
220
- const FString& AssetPath,
221
- const FString& ChainAnchorId,
222
- const FString& Direction,
223
- int32 MaxDepth
224
- )
225
- {
226
- UBlueprint* Blueprint = LoadBlueprintFromAssetPath(AssetPath);
227
- if (!Blueprint)
228
- {
229
- return 1;
230
- }
231
-
232
- UEdGraphNode* StartNode = FindNodeByGuid(Blueprint, ChainAnchorId);
233
- if (!StartNode)
234
- {
235
- return 1;
236
- }
237
-
238
- TArray<TSharedPtr<FJsonValue>> NodeValues;
239
- TSet<FGuid> Visited;
240
- TArray<TPair<UEdGraphNode*, int32>> Frontier;
241
- Frontier.Add(TPair<UEdGraphNode*, int32>(StartNode, 0));
242
- Visited.Add(StartNode->NodeGuid);
243
-
244
- const bool bUpstream = Direction.Equals(TEXT("upstream"), ESearchCase::IgnoreCase);
245
-
246
- while (Frontier.Num() > 0)
247
- {
248
- const TPair<UEdGraphNode*, int32> Current = Frontier[0];
249
- Frontier.RemoveAt(0);
250
-
251
- NodeValues.Add(MakeShared<FJsonValueObject>(BuildChainNodeJson(Current.Key->GetGraph(), Current.Key, Current.Value)));
252
- if (Current.Value >= MaxDepth)
253
- {
254
- continue;
255
- }
256
-
257
- for (UEdGraphPin* Pin : Current.Key->Pins)
258
- {
259
- if (!Pin)
260
- {
261
- continue;
262
- }
263
-
264
- const bool bMatchesDirection = bUpstream ? Pin->Direction == EGPD_Input : Pin->Direction == EGPD_Output;
265
- if (!bMatchesDirection)
266
- {
267
- continue;
268
- }
269
-
270
- for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
271
- {
272
- if (!LinkedPin || !LinkedPin->GetOwningNode())
273
- {
274
- continue;
275
- }
276
-
277
- UEdGraphNode* NextNode = LinkedPin->GetOwningNode();
278
- if (Visited.Contains(NextNode->NodeGuid))
279
- {
280
- continue;
281
- }
282
-
283
- Visited.Add(NextNode->NodeGuid);
284
- Frontier.Add(TPair<UEdGraphNode*, int32>(NextNode, Current.Value + 1));
285
- }
286
- }
287
- }
288
-
289
- TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
290
- RootObject->SetArrayField(TEXT("nodes"), NodeValues);
291
- return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
292
- }
293
-
294
- bool UGitNexusBlueprintAnalyzerCommandlet::WriteJsonToFile(const FString& OutputJsonPath, const TSharedPtr<FJsonObject>& RootObject) const
295
- {
296
- FString JsonText;
297
- const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
298
- if (!FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer))
299
- {
300
- return false;
301
- }
302
-
303
- return FFileHelper::SaveStringToFile(JsonText, *OutputJsonPath);
304
- }
305
-
306
- TArray<FString> UGitNexusBlueprintAnalyzerCommandlet::LoadCandidateAssets(const FString& CandidatesJsonPath) const
307
- {
308
- TArray<FString> Result;
309
- if (CandidatesJsonPath.IsEmpty())
310
- {
311
- return Result;
312
- }
313
-
314
- FString RawJson;
315
- if (!FFileHelper::LoadFileToString(RawJson, *CandidatesJsonPath))
316
- {
317
- return Result;
318
- }
319
-
320
- TSharedPtr<FJsonObject> RootObject;
321
- const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(RawJson);
322
- if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
323
- {
324
- return Result;
325
- }
326
-
327
- const TArray<TSharedPtr<FJsonValue>>* CandidateValues = nullptr;
328
- if (!RootObject->TryGetArrayField(TEXT("candidate_assets"), CandidateValues) || !CandidateValues)
329
- {
330
- return Result;
331
- }
332
-
333
- for (const TSharedPtr<FJsonValue>& Value : *CandidateValues)
334
- {
335
- const TSharedPtr<FJsonObject>* CandidateObject = nullptr;
336
- if (Value.IsValid() && Value->TryGetObject(CandidateObject) && CandidateObject && CandidateObject->IsValid())
337
- {
338
- FString AssetPath;
339
- if ((*CandidateObject)->TryGetStringField(TEXT("asset_path"), AssetPath))
340
- {
341
- Result.Add(AssetPath);
342
- }
343
- }
344
- }
345
-
346
- return Result;
347
- }
348
-
349
- TArray<FAssetData> UGitNexusBlueprintAnalyzerCommandlet::GetAllBlueprintAssets() const
350
- {
351
- FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
352
- IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
353
- AssetRegistry.SearchAllAssets(true);
354
-
355
- FARFilter Filter;
356
- Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
357
- Filter.bRecursiveClasses = true;
358
-
359
- TArray<FAssetData> Assets;
360
- AssetRegistry.GetAssets(Filter, Assets);
361
- return Assets;
362
- }
363
-
364
- UBlueprint* UGitNexusBlueprintAnalyzerCommandlet::LoadBlueprintFromAssetPath(const FString& AssetPath) const
365
- {
366
- const FSoftObjectPath SoftObjectPath(AssetPath);
367
- return Cast<UBlueprint>(SoftObjectPath.TryLoad());
368
- }
369
-
370
- void UGitNexusBlueprintAnalyzerCommandlet::CollectBlueprintGraphs(UBlueprint* Blueprint, TArray<UEdGraph*>& OutGraphs) const
371
- {
372
- if (!Blueprint)
373
- {
374
- return;
375
- }
376
-
377
- Blueprint->GetAllGraphs(OutGraphs);
378
- }
379
-
380
- bool UGitNexusBlueprintAnalyzerCommandlet::IsTargetFunctionNode(
381
- const UEdGraphNode* Node,
382
- const FString& TargetSymbolKey,
383
- const FString& TargetClassName,
384
- const FString& TargetFunctionName
385
- ) const
386
- {
387
- if (const UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Node))
388
- {
389
- if (const UFunction* TargetFunction = CallNode->GetTargetFunction())
390
- {
391
- const UClass* OwnerClass = TargetFunction->GetOwnerClass();
392
- const FString SymbolKey = OwnerClass
393
- ? FString::Printf(TEXT("%s::%s"), *OwnerClass->GetName(), *TargetFunction->GetName())
394
- : TargetFunction->GetName();
395
- return SymbolKey == TargetSymbolKey
396
- || TargetFunction->GetName() == TargetFunctionName
397
- || (OwnerClass && OwnerClass->GetName() == TargetClassName && TargetFunction->GetName() == TargetFunctionName);
398
- }
399
- }
400
-
401
- if (const UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node))
402
- {
403
- return EventNode->GetFunctionName().ToString() == TargetFunctionName;
404
- }
405
-
406
- return false;
407
- }
408
-
409
- TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildReferenceJson(UBlueprint* Blueprint, const UEdGraph* Graph, const UEdGraphNode* Node) const
410
- {
411
- TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
412
- Object->SetStringField(TEXT("asset_path"), Blueprint ? Blueprint->GetPathName() : FString());
413
- Object->SetStringField(TEXT("graph_name"), Graph ? Graph->GetName() : FString());
414
- Object->SetStringField(TEXT("node_kind"), Node ? Node->GetClass()->GetName() : FString());
415
- Object->SetStringField(TEXT("node_title"), Node ? Node->GetNodeTitle(ENodeTitleType::ListView).ToString() : FString());
416
- Object->SetStringField(TEXT("blueprint_owner_function"), Graph ? Graph->GetName() : FString());
417
- Object->SetStringField(TEXT("chain_anchor_id"), Node ? Node->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens) : FString());
418
- Object->SetStringField(TEXT("source"), TEXT("editor_confirmed"));
419
- return Object;
420
- }
421
-
422
- TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildChainNodeJson(const UEdGraph* Graph, const UEdGraphNode* Node, int32 Depth) const
423
- {
424
- TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
425
- Object->SetStringField(TEXT("node_id"), Node ? Node->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens) : FString());
426
- Object->SetStringField(TEXT("graph_name"), Graph ? Graph->GetName() : FString());
427
- Object->SetStringField(TEXT("node_kind"), Node ? Node->GetClass()->GetName() : FString());
428
- Object->SetStringField(TEXT("node_title"), Node ? Node->GetNodeTitle(ENodeTitleType::ListView).ToString() : FString());
429
- Object->SetNumberField(TEXT("depth"), Depth);
430
- return Object;
431
- }
432
-
433
- UEdGraphNode* UGitNexusBlueprintAnalyzerCommandlet::FindNodeByGuid(UBlueprint* Blueprint, const FString& NodeGuid) const
434
- {
435
- if (!Blueprint)
436
- {
437
- return nullptr;
438
- }
439
-
440
- FGuid Guid;
441
- if (!FGuid::Parse(NodeGuid, Guid))
442
- {
443
- return nullptr;
444
- }
445
-
446
- TArray<UEdGraph*> Graphs;
447
- CollectBlueprintGraphs(Blueprint, Graphs);
448
- for (UEdGraph* Graph : Graphs)
449
- {
450
- if (!Graph)
451
- {
452
- continue;
453
- }
454
-
455
- for (UEdGraphNode* Node : Graph->Nodes)
456
- {
457
- if (Node && Node->NodeGuid == Guid)
458
- {
459
- return Node;
460
- }
461
- }
462
- }
463
-
464
- return nullptr;
465
- }
1
+ #include "GitNexusBlueprintAnalyzerCommandlet.h"
2
+
3
+ #include "AssetRegistry/AssetRegistryModule.h"
4
+ #include "AssetRegistry/IAssetRegistry.h"
5
+ #include "Blueprint/BlueprintSupport.h"
6
+ #include "Blueprint/UserWidget.h"
7
+ #include "Dom/JsonObject.h"
8
+ #include "EdGraph/EdGraph.h"
9
+ #include "EdGraph/EdGraphNode.h"
10
+ #include "EdGraph/EdGraphPin.h"
11
+ #include "Engine/Blueprint.h"
12
+ #include "EdGraphSchema_K2.h"
13
+ #include "K2Node_CallFunction.h"
14
+ #include "K2Node_Event.h"
15
+ #include "K2Node_IfThenElse.h"
16
+ #include "K2Node_Switch.h"
17
+ #include "K2Node_VariableGet.h"
18
+ #include "K2Node_VariableSet.h"
19
+ #include "Kismet2/BlueprintEditorUtils.h"
20
+ #include "Misc/FileHelper.h"
21
+ #include "Misc/Guid.h"
22
+ #include "Misc/PackageName.h"
23
+ #include "Misc/Paths.h"
24
+ #include "Serialization/JsonReader.h"
25
+ #include "Serialization/JsonSerializer.h"
26
+ #include "UObject/SoftObjectPath.h"
27
+ #include "UObject/GarbageCollection.h"
28
+
29
+ UGitNexusBlueprintAnalyzerCommandlet::UGitNexusBlueprintAnalyzerCommandlet()
30
+ {
31
+ IsClient = false;
32
+ IsEditor = true;
33
+ LogToConsole = true;
34
+ ShowErrorCount = true;
35
+ }
36
+
37
+ int32 UGitNexusBlueprintAnalyzerCommandlet::Main(const FString& Params)
38
+ {
39
+ FString Operation;
40
+ FString OutputJsonPath;
41
+ FParse::Value(*Params, TEXT("Operation="), Operation);
42
+ FParse::Value(*Params, TEXT("OutputJson="), OutputJsonPath);
43
+
44
+ if (Operation.IsEmpty() || OutputJsonPath.IsEmpty())
45
+ {
46
+ UE_LOG(LogTemp, Error, TEXT("GitNexusBlueprintAnalyzer requires Operation= and OutputJson= parameters."));
47
+ return 1;
48
+ }
49
+
50
+ if (Operation.Equals(TEXT("SyncAssets"), ESearchCase::IgnoreCase))
51
+ {
52
+ FString FilterJsonPath;
53
+ FParse::Value(*Params, TEXT("FilterJson="), FilterJsonPath);
54
+ // Legacy support: also check IgnoreJson= for backward compatibility
55
+ if (FilterJsonPath.IsEmpty())
56
+ {
57
+ FParse::Value(*Params, TEXT("IgnoreJson="), FilterJsonPath);
58
+ }
59
+ FString ModeStr;
60
+ FParse::Value(*Params, TEXT("Mode="), ModeStr);
61
+ const bool bDeepMode = ModeStr.Equals(TEXT("deep"), ESearchCase::IgnoreCase);
62
+ return RunSyncAssets(OutputJsonPath, FilterJsonPath, bDeepMode);
63
+ }
64
+
65
+ if (Operation.Equals(TEXT("FindNativeBlueprintReferences"), ESearchCase::IgnoreCase))
66
+ {
67
+ FString CandidatesJsonPath;
68
+ FString TargetSymbolKey;
69
+ FString TargetClassName;
70
+ FString TargetFunctionName;
71
+ FParse::Value(*Params, TEXT("CandidatesJson="), CandidatesJsonPath);
72
+ FParse::Value(*Params, TEXT("TargetSymbolKey="), TargetSymbolKey);
73
+ FParse::Value(*Params, TEXT("TargetClass="), TargetClassName);
74
+ FParse::Value(*Params, TEXT("TargetFunction="), TargetFunctionName);
75
+ return RunFindNativeBlueprintReferences(OutputJsonPath, CandidatesJsonPath, TargetSymbolKey, TargetClassName, TargetFunctionName);
76
+ }
77
+
78
+ if (Operation.Equals(TEXT("ExpandBlueprintChain"), ESearchCase::IgnoreCase))
79
+ {
80
+ FString AssetPath;
81
+ FString ChainAnchorId;
82
+ FString Direction = TEXT("downstream");
83
+ int32 MaxDepth = 5;
84
+ FParse::Value(*Params, TEXT("AssetPath="), AssetPath);
85
+ FParse::Value(*Params, TEXT("ChainAnchorId="), ChainAnchorId);
86
+ FParse::Value(*Params, TEXT("Direction="), Direction);
87
+ FParse::Value(*Params, TEXT("MaxDepth="), MaxDepth);
88
+ return RunExpandBlueprintChain(OutputJsonPath, AssetPath, ChainAnchorId, Direction, MaxDepth);
89
+ }
90
+
91
+ UE_LOG(LogTemp, Error, TEXT("Unsupported GitNexusBlueprintAnalyzer operation: %s"), *Operation);
92
+ return 1;
93
+ }
94
+
95
+ // ── SyncAssets entry point ──────────────────────────────────────────────
96
+
97
+ int32 UGitNexusBlueprintAnalyzerCommandlet::RunSyncAssets(
98
+ const FString& OutputJsonPath,
99
+ const FString& FilterJsonPath,
100
+ bool bDeepMode)
101
+ {
102
+ const FFilterPrefixes Filters = LoadFilterPrefixes(FilterJsonPath);
103
+ const TArray<FAssetData> Assets = GetAllBlueprintAssets(Filters);
104
+
105
+ if (bDeepMode)
106
+ {
107
+ UE_LOG(LogTemp, Display, TEXT("GitNexusBlueprintAnalyzer: Running in DEEP mode (full Blueprint loading)"));
108
+ return RunSyncAssetsDeep(OutputJsonPath, Assets);
109
+ }
110
+
111
+ UE_LOG(LogTemp, Display, TEXT("GitNexusBlueprintAnalyzer: Running in METADATA mode (no asset loading)"));
112
+ return RunSyncAssetsMetadata(OutputJsonPath, Assets);
113
+ }
114
+
115
+ // ── Metadata-only sync (default) ────────────────────────────────────────
116
+ // Uses FAssetData tags + AssetRegistry dependencies. Zero asset loading.
117
+
118
+ int32 UGitNexusBlueprintAnalyzerCommandlet::RunSyncAssetsMetadata(
119
+ const FString& OutputJsonPath,
120
+ const TArray<FAssetData>& Assets)
121
+ {
122
+ TArray<TSharedPtr<FJsonValue>> AssetValues;
123
+ IAssetRegistry& AssetRegistry = IAssetRegistry::GetChecked();
124
+
125
+ for (const FAssetData& AssetData : Assets)
126
+ {
127
+ TSharedPtr<FJsonObject> AssetObject = MakeShared<FJsonObject>();
128
+ AssetObject->SetStringField(TEXT("asset_path"), AssetData.GetSoftObjectPath().ToString());
129
+
130
+ // GeneratedClass from tag
131
+ FString GeneratedClassTag;
132
+ if (AssetData.GetTagValue(FBlueprintTags::GeneratedClassPath, GeneratedClassTag))
133
+ {
134
+ AssetObject->SetStringField(TEXT("generated_class"), GeneratedClassTag);
135
+ }
136
+
137
+ // ParentClass from tag
138
+ FString ParentClassTag;
139
+ if (AssetData.GetTagValue(FBlueprintTags::ParentClassPath, ParentClassTag))
140
+ {
141
+ AssetObject->SetStringField(TEXT("parent_class"), ParentClassTag);
142
+
143
+ // Extract native parent from the tag (class name after the last '.')
144
+ const int32 DotIdx = ParentClassTag.Find(TEXT("."), ESearchCase::IgnoreCase, ESearchDir::FromEnd);
145
+ if (DotIdx != INDEX_NONE)
146
+ {
147
+ FString ParentClassName = ParentClassTag.Mid(DotIdx + 1);
148
+ // Remove trailing _C suffix if present (generated class suffix)
149
+ if (ParentClassName.EndsWith(TEXT("_C")))
150
+ {
151
+ ParentClassName = ParentClassName.LeftChop(2);
152
+ }
153
+ // Remove trailing ' (quote) from path notation
154
+ ParentClassName.RemoveFromEnd(TEXT("'"));
155
+ TArray<TSharedPtr<FJsonValue>> NativeParents;
156
+ NativeParents.Add(MakeShared<FJsonValueString>(ParentClassName));
157
+ AssetObject->SetArrayField(TEXT("native_parents"), NativeParents);
158
+ }
159
+ }
160
+
161
+ // NativeParentClass tag (more reliable for native parents)
162
+ FString NativeParentClassTag;
163
+ if (AssetData.GetTagValue(FBlueprintTags::NativeParentClassPath, NativeParentClassTag))
164
+ {
165
+ const int32 DotIdx = NativeParentClassTag.Find(TEXT("."), ESearchCase::IgnoreCase, ESearchDir::FromEnd);
166
+ if (DotIdx != INDEX_NONE)
167
+ {
168
+ FString NativeClassName = NativeParentClassTag.Mid(DotIdx + 1);
169
+ NativeClassName.RemoveFromEnd(TEXT("'"));
170
+ TArray<TSharedPtr<FJsonValue>> NativeParents;
171
+ NativeParents.Add(MakeShared<FJsonValueString>(NativeClassName));
172
+ AssetObject->SetArrayField(TEXT("native_parents"), NativeParents);
173
+ }
174
+ }
175
+
176
+ // Dependencies from AssetRegistry (no loading needed)
177
+ TArray<FName> Dependencies;
178
+ AssetRegistry.GetDependencies(AssetData.PackageName, Dependencies, UE::AssetRegistry::EDependencyCategory::Package);
179
+ TArray<TSharedPtr<FJsonValue>> DependencyValues;
180
+ for (const FName& Dependency : Dependencies)
181
+ {
182
+ DependencyValues.Add(MakeShared<FJsonValueString>(Dependency.ToString()));
183
+ }
184
+ AssetObject->SetArrayField(TEXT("dependencies"), DependencyValues);
185
+
186
+ // native_function_refs not available in metadata mode
187
+ AssetObject->SetArrayField(TEXT("native_function_refs"), TArray<TSharedPtr<FJsonValue>>());
188
+
189
+ AssetValues.Add(MakeShared<FJsonValueObject>(AssetObject));
190
+ }
191
+
192
+ UE_LOG(LogTemp, Display, TEXT("GitNexusBlueprintAnalyzer: %d assets indexed (metadata mode)"), AssetValues.Num());
193
+
194
+ TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
195
+ RootObject->SetNumberField(TEXT("version"), 1);
196
+ RootObject->SetStringField(TEXT("generated_at"), FDateTime::UtcNow().ToIso8601());
197
+ RootObject->SetStringField(TEXT("project_path"), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()));
198
+ RootObject->SetStringField(TEXT("mode"), TEXT("metadata"));
199
+ RootObject->SetArrayField(TEXT("assets"), AssetValues);
200
+
201
+ return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
202
+ }
203
+
204
+ // ── Deep sync (--deep) ──────────────────────────────────────────────────
205
+ // Loads each Blueprint fully to extract native_function_refs from graphs.
206
+ // Uses batch GC to prevent OOM on large projects.
207
+
208
+ static constexpr int32 DEEP_BATCH_SIZE = 50;
209
+
210
+ int32 UGitNexusBlueprintAnalyzerCommandlet::RunSyncAssetsDeep(
211
+ const FString& OutputJsonPath,
212
+ const TArray<FAssetData>& Assets)
213
+ {
214
+ TArray<TSharedPtr<FJsonValue>> AssetValues;
215
+ int32 SkippedCount = 0;
216
+ int32 BatchCounter = 0;
217
+
218
+ for (const FAssetData& AssetData : Assets)
219
+ {
220
+ const FSoftObjectPath SoftPath = AssetData.GetSoftObjectPath();
221
+ UObject* LoadedAsset = SoftPath.TryLoad();
222
+ UBlueprint* Blueprint = LoadedAsset ? Cast<UBlueprint>(LoadedAsset) : nullptr;
223
+ if (!Blueprint)
224
+ {
225
+ if (!LoadedAsset)
226
+ {
227
+ SkippedCount++;
228
+ UE_LOG(LogTemp, Warning, TEXT("GitNexusBlueprintAnalyzer: Skipped asset (failed to load): %s"), *AssetData.PackageName.ToString());
229
+ }
230
+ continue;
231
+ }
232
+
233
+ TSharedPtr<FJsonObject> AssetObject = MakeShared<FJsonObject>();
234
+ AssetObject->SetStringField(TEXT("asset_path"), AssetData.GetSoftObjectPath().ToString());
235
+
236
+ if (Blueprint->GeneratedClass)
237
+ {
238
+ AssetObject->SetStringField(TEXT("generated_class"), Blueprint->GeneratedClass->GetPathName());
239
+ }
240
+
241
+ if (Blueprint->ParentClass)
242
+ {
243
+ AssetObject->SetStringField(TEXT("parent_class"), Blueprint->ParentClass->GetPathName());
244
+ }
245
+
246
+ TArray<TSharedPtr<FJsonValue>> NativeParents;
247
+ for (UClass* Class = Blueprint->ParentClass; Class; Class = Class->GetSuperClass())
248
+ {
249
+ if (Class->ClassGeneratedBy == nullptr)
250
+ {
251
+ NativeParents.Add(MakeShared<FJsonValueString>(Class->GetName()));
252
+ }
253
+ }
254
+ AssetObject->SetArrayField(TEXT("native_parents"), NativeParents);
255
+
256
+ TArray<FName> Dependencies;
257
+ IAssetRegistry::GetChecked().GetDependencies(AssetData.PackageName, Dependencies, UE::AssetRegistry::EDependencyCategory::Package);
258
+ TArray<TSharedPtr<FJsonValue>> DependencyValues;
259
+ for (const FName& Dependency : Dependencies)
260
+ {
261
+ DependencyValues.Add(MakeShared<FJsonValueString>(Dependency.ToString()));
262
+ }
263
+ AssetObject->SetArrayField(TEXT("dependencies"), DependencyValues);
264
+
265
+ TArray<UEdGraph*> Graphs;
266
+ CollectBlueprintGraphs(Blueprint, Graphs);
267
+ TSet<FString> NativeFunctionRefs;
268
+ for (const UEdGraph* Graph : Graphs)
269
+ {
270
+ if (!Graph)
271
+ {
272
+ continue;
273
+ }
274
+
275
+ for (const UEdGraphNode* Node : Graph->Nodes)
276
+ {
277
+ if (const UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Node))
278
+ {
279
+ if (const UFunction* TargetFunction = CallNode->GetTargetFunction())
280
+ {
281
+ const UClass* OwnerClass = TargetFunction->GetOwnerClass();
282
+ const FString SymbolKey = OwnerClass
283
+ ? FString::Printf(TEXT("%s::%s"), *OwnerClass->GetName(), *TargetFunction->GetName())
284
+ : TargetFunction->GetName();
285
+ NativeFunctionRefs.Add(SymbolKey);
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ TArray<TSharedPtr<FJsonValue>> NativeFunctionRefValues;
292
+ for (const FString& Ref : NativeFunctionRefs)
293
+ {
294
+ NativeFunctionRefValues.Add(MakeShared<FJsonValueString>(Ref));
295
+ }
296
+ AssetObject->SetArrayField(TEXT("native_function_refs"), NativeFunctionRefValues);
297
+
298
+ AssetValues.Add(MakeShared<FJsonValueObject>(AssetObject));
299
+
300
+ // Batch GC to prevent OOM on large projects
301
+ BatchCounter++;
302
+ if (BatchCounter >= DEEP_BATCH_SIZE)
303
+ {
304
+ CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
305
+ BatchCounter = 0;
306
+ UE_LOG(LogTemp, Display, TEXT("GitNexusBlueprintAnalyzer: Processed %d / %d assets (GC)"), AssetValues.Num(), Assets.Num());
307
+ }
308
+ }
309
+
310
+ if (SkippedCount > 0)
311
+ {
312
+ UE_LOG(LogTemp, Warning, TEXT("GitNexusBlueprintAnalyzer: %d assets skipped (failed to load)"), SkippedCount);
313
+ }
314
+ UE_LOG(LogTemp, Display, TEXT("GitNexusBlueprintAnalyzer: %d assets indexed (deep mode)"), AssetValues.Num());
315
+
316
+ TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
317
+ RootObject->SetNumberField(TEXT("version"), 1);
318
+ RootObject->SetStringField(TEXT("generated_at"), FDateTime::UtcNow().ToIso8601());
319
+ RootObject->SetStringField(TEXT("project_path"), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()));
320
+ RootObject->SetStringField(TEXT("mode"), TEXT("deep"));
321
+ RootObject->SetArrayField(TEXT("assets"), AssetValues);
322
+
323
+ return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
324
+ }
325
+
326
+ // ── FindNativeBlueprintReferences ───────────────────────────────────────
327
+
328
+ int32 UGitNexusBlueprintAnalyzerCommandlet::RunFindNativeBlueprintReferences(
329
+ const FString& OutputJsonPath,
330
+ const FString& CandidatesJsonPath,
331
+ const FString& TargetSymbolKey,
332
+ const FString& TargetClassName,
333
+ const FString& TargetFunctionName
334
+ )
335
+ {
336
+ TArray<TSharedPtr<FJsonValue>> ConfirmedReferences;
337
+ TArray<FString> CandidateAssets = LoadCandidateAssets(CandidatesJsonPath);
338
+
339
+ for (const FString& AssetPath : CandidateAssets)
340
+ {
341
+ UBlueprint* Blueprint = LoadBlueprintFromAssetPath(AssetPath);
342
+ if (!Blueprint)
343
+ {
344
+ continue;
345
+ }
346
+
347
+ TArray<UEdGraph*> Graphs;
348
+ CollectBlueprintGraphs(Blueprint, Graphs);
349
+ for (const UEdGraph* Graph : Graphs)
350
+ {
351
+ if (!Graph)
352
+ {
353
+ continue;
354
+ }
355
+
356
+ for (const UEdGraphNode* Node : Graph->Nodes)
357
+ {
358
+ if (Node && IsTargetFunctionNode(Node, TargetSymbolKey, TargetClassName, TargetFunctionName))
359
+ {
360
+ ConfirmedReferences.Add(MakeShared<FJsonValueObject>(BuildReferenceJson(Blueprint, Graph, Node)));
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
367
+ TSharedPtr<FJsonObject> TargetObject = MakeShared<FJsonObject>();
368
+ TargetObject->SetStringField(TEXT("symbol_key"), TargetSymbolKey);
369
+ TargetObject->SetStringField(TEXT("class_name"), TargetClassName);
370
+ TargetObject->SetStringField(TEXT("symbol_name"), TargetFunctionName);
371
+ RootObject->SetObjectField(TEXT("target_function"), TargetObject);
372
+ RootObject->SetNumberField(TEXT("candidates_scanned"), CandidateAssets.Num());
373
+ RootObject->SetArrayField(TEXT("confirmed_references"), ConfirmedReferences);
374
+
375
+ return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
376
+ }
377
+
378
+ // ── ExpandBlueprintChain ────────────────────────────────────────────────
379
+
380
+ int32 UGitNexusBlueprintAnalyzerCommandlet::RunExpandBlueprintChain(
381
+ const FString& OutputJsonPath,
382
+ const FString& AssetPath,
383
+ const FString& ChainAnchorId,
384
+ const FString& Direction,
385
+ int32 MaxDepth
386
+ )
387
+ {
388
+ UBlueprint* Blueprint = LoadBlueprintFromAssetPath(AssetPath);
389
+ if (!Blueprint)
390
+ {
391
+ return 1;
392
+ }
393
+
394
+ UEdGraphNode* StartNode = FindNodeByGuid(Blueprint, ChainAnchorId);
395
+ if (!StartNode)
396
+ {
397
+ return 1;
398
+ }
399
+
400
+ struct FChainFrontierEntry
401
+ {
402
+ UEdGraphNode* Node;
403
+ int32 Depth;
404
+ FString TraversedFromPinName;
405
+ FGuid TraversedFromNodeId;
406
+ };
407
+
408
+ TArray<TSharedPtr<FJsonValue>> NodeValues;
409
+ TSet<FGuid> Visited;
410
+ TArray<FChainFrontierEntry> Frontier;
411
+ Frontier.Add({ StartNode, 0, FString(), FGuid() });
412
+ Visited.Add(StartNode->NodeGuid);
413
+
414
+ const bool bUpstream = Direction.Equals(TEXT("upstream"), ESearchCase::IgnoreCase);
415
+
416
+ while (Frontier.Num() > 0)
417
+ {
418
+ const FChainFrontierEntry Current = Frontier[0];
419
+ Frontier.RemoveAt(0);
420
+
421
+ NodeValues.Add(MakeShared<FJsonValueObject>(BuildChainNodeJson(
422
+ Current.Node->GetGraph(), Current.Node, Current.Depth,
423
+ Current.TraversedFromPinName, Current.TraversedFromNodeId)));
424
+ if (Current.Depth >= MaxDepth)
425
+ {
426
+ continue;
427
+ }
428
+
429
+ for (UEdGraphPin* Pin : Current.Node->Pins)
430
+ {
431
+ if (!Pin)
432
+ {
433
+ continue;
434
+ }
435
+
436
+ const bool bMatchesDirection = bUpstream ? Pin->Direction == EGPD_Input : Pin->Direction == EGPD_Output;
437
+ if (!bMatchesDirection)
438
+ {
439
+ continue;
440
+ }
441
+
442
+ for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
443
+ {
444
+ if (!LinkedPin || !LinkedPin->GetOwningNode())
445
+ {
446
+ continue;
447
+ }
448
+
449
+ UEdGraphNode* NextNode = LinkedPin->GetOwningNode();
450
+ if (Visited.Contains(NextNode->NodeGuid))
451
+ {
452
+ continue;
453
+ }
454
+
455
+ // NOTE: In diamond-shaped graphs, BFS only records the first-discovered parent.
456
+ Visited.Add(NextNode->NodeGuid);
457
+ Frontier.Add({ NextNode, Current.Depth + 1, Pin->PinName.ToString(), Current.Node->NodeGuid });
458
+ }
459
+ }
460
+ }
461
+
462
+ TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
463
+ RootObject->SetArrayField(TEXT("nodes"), NodeValues);
464
+ return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
465
+ }
466
+
467
+ // ── Utility functions ───────────────────────────────────────────────────
468
+
469
+ bool UGitNexusBlueprintAnalyzerCommandlet::WriteJsonToFile(const FString& OutputJsonPath, const TSharedPtr<FJsonObject>& RootObject) const
470
+ {
471
+ FString JsonText;
472
+ const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
473
+ if (!FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer))
474
+ {
475
+ return false;
476
+ }
477
+
478
+ return FFileHelper::SaveStringToFile(JsonText, *OutputJsonPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
479
+ }
480
+
481
+ TArray<FString> UGitNexusBlueprintAnalyzerCommandlet::LoadCandidateAssets(const FString& CandidatesJsonPath) const
482
+ {
483
+ TArray<FString> Result;
484
+ if (CandidatesJsonPath.IsEmpty())
485
+ {
486
+ return Result;
487
+ }
488
+
489
+ FString RawJson;
490
+ if (!FFileHelper::LoadFileToString(RawJson, *CandidatesJsonPath))
491
+ {
492
+ return Result;
493
+ }
494
+
495
+ TSharedPtr<FJsonObject> RootObject;
496
+ const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(RawJson);
497
+ if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
498
+ {
499
+ return Result;
500
+ }
501
+
502
+ const TArray<TSharedPtr<FJsonValue>>* CandidateValues = nullptr;
503
+ if (!RootObject->TryGetArrayField(TEXT("candidate_assets"), CandidateValues) || !CandidateValues)
504
+ {
505
+ return Result;
506
+ }
507
+
508
+ for (const TSharedPtr<FJsonValue>& Value : *CandidateValues)
509
+ {
510
+ const TSharedPtr<FJsonObject>* CandidateObject = nullptr;
511
+ if (Value.IsValid() && Value->TryGetObject(CandidateObject) && CandidateObject && CandidateObject->IsValid())
512
+ {
513
+ FString AssetPath;
514
+ if ((*CandidateObject)->TryGetStringField(TEXT("asset_path"), AssetPath))
515
+ {
516
+ Result.Add(AssetPath);
517
+ }
518
+ }
519
+ }
520
+
521
+ return Result;
522
+ }
523
+
524
+ UGitNexusBlueprintAnalyzerCommandlet::FFilterPrefixes
525
+ UGitNexusBlueprintAnalyzerCommandlet::LoadFilterPrefixes(const FString& FilterJsonPath) const
526
+ {
527
+ FFilterPrefixes Result;
528
+ if (FilterJsonPath.IsEmpty())
529
+ {
530
+ return Result;
531
+ }
532
+
533
+ FString RawJson;
534
+ if (!FFileHelper::LoadFileToString(RawJson, *FilterJsonPath))
535
+ {
536
+ UE_LOG(LogTemp, Warning, TEXT("GitNexusBlueprintAnalyzer: Could not read filter file: %s"), *FilterJsonPath);
537
+ return Result;
538
+ }
539
+
540
+ TSharedPtr<FJsonObject> RootObject;
541
+ const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(RawJson);
542
+ if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
543
+ {
544
+ return Result;
545
+ }
546
+
547
+ // Read include_prefixes (whitelist — if set, ONLY these paths are included)
548
+ const TArray<TSharedPtr<FJsonValue>>* IncludeValues = nullptr;
549
+ if (RootObject->TryGetArrayField(TEXT("include_prefixes"), IncludeValues) && IncludeValues)
550
+ {
551
+ for (const TSharedPtr<FJsonValue>& Value : *IncludeValues)
552
+ {
553
+ FString Prefix;
554
+ if (Value.IsValid() && Value->TryGetString(Prefix))
555
+ {
556
+ Result.IncludePrefixes.Add(Prefix);
557
+ }
558
+ }
559
+ }
560
+
561
+ // Read exclude_prefixes (blacklist)
562
+ const TArray<TSharedPtr<FJsonValue>>* ExcludeValues = nullptr;
563
+ if (RootObject->TryGetArrayField(TEXT("exclude_prefixes"), ExcludeValues) && ExcludeValues)
564
+ {
565
+ for (const TSharedPtr<FJsonValue>& Value : *ExcludeValues)
566
+ {
567
+ FString Prefix;
568
+ if (Value.IsValid() && Value->TryGetString(Prefix))
569
+ {
570
+ Result.ExcludePrefixes.Add(Prefix);
571
+ }
572
+ }
573
+ }
574
+
575
+ if (Result.IncludePrefixes.Num() > 0)
576
+ {
577
+ UE_LOG(LogTemp, Display, TEXT("GitNexusBlueprintAnalyzer: Loaded %d include prefixes (whitelist)"), Result.IncludePrefixes.Num());
578
+ }
579
+ if (Result.ExcludePrefixes.Num() > 0)
580
+ {
581
+ UE_LOG(LogTemp, Display, TEXT("GitNexusBlueprintAnalyzer: Loaded %d exclude prefixes"), Result.ExcludePrefixes.Num());
582
+ }
583
+
584
+ return Result;
585
+ }
586
+
587
+ TArray<FAssetData> UGitNexusBlueprintAnalyzerCommandlet::GetAllBlueprintAssets(const FFilterPrefixes& Filters) const
588
+ {
589
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
590
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
591
+ AssetRegistry.SearchAllAssets(true);
592
+
593
+ FARFilter Filter;
594
+ Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
595
+ Filter.bRecursiveClasses = true;
596
+
597
+ TArray<FAssetData> AllAssets;
598
+ AssetRegistry.GetAssets(Filter, AllAssets);
599
+
600
+ const bool bHasIncludes = Filters.IncludePrefixes.Num() > 0;
601
+ const bool bHasExcludes = Filters.ExcludePrefixes.Num() > 0;
602
+
603
+ if (!bHasIncludes && !bHasExcludes)
604
+ {
605
+ return AllAssets;
606
+ }
607
+
608
+ TArray<FAssetData> FilteredAssets;
609
+ int32 IncludedCount = 0;
610
+ int32 ExcludedCount = 0;
611
+
612
+ for (const FAssetData& Asset : AllAssets)
613
+ {
614
+ const FString PackagePath = Asset.PackageName.ToString();
615
+
616
+ // Include filter (whitelist): if set, asset MUST match at least one prefix
617
+ if (bHasIncludes)
618
+ {
619
+ bool bIncluded = false;
620
+ for (const FString& Prefix : Filters.IncludePrefixes)
621
+ {
622
+ if (PackagePath.StartsWith(Prefix))
623
+ {
624
+ bIncluded = true;
625
+ break;
626
+ }
627
+ }
628
+ if (!bIncluded)
629
+ {
630
+ IncludedCount++;
631
+ continue;
632
+ }
633
+ }
634
+
635
+ // Exclude filter (blacklist): skip assets matching any prefix
636
+ if (bHasExcludes)
637
+ {
638
+ bool bExcluded = false;
639
+ for (const FString& Prefix : Filters.ExcludePrefixes)
640
+ {
641
+ if (PackagePath.StartsWith(Prefix))
642
+ {
643
+ bExcluded = true;
644
+ break;
645
+ }
646
+ }
647
+ if (bExcluded)
648
+ {
649
+ ExcludedCount++;
650
+ continue;
651
+ }
652
+ }
653
+
654
+ FilteredAssets.Add(Asset);
655
+ }
656
+
657
+ UE_LOG(LogTemp, Display, TEXT("GitNexusBlueprintAnalyzer: %d assets after filtering (%d outside include scope, %d excluded)"),
658
+ FilteredAssets.Num(), IncludedCount, ExcludedCount);
659
+ return FilteredAssets;
660
+ }
661
+
662
+ UBlueprint* UGitNexusBlueprintAnalyzerCommandlet::LoadBlueprintFromAssetPath(const FString& AssetPath) const
663
+ {
664
+ const FSoftObjectPath SoftObjectPath(AssetPath);
665
+ return Cast<UBlueprint>(SoftObjectPath.TryLoad());
666
+ }
667
+
668
+ void UGitNexusBlueprintAnalyzerCommandlet::CollectBlueprintGraphs(UBlueprint* Blueprint, TArray<UEdGraph*>& OutGraphs) const
669
+ {
670
+ if (!Blueprint)
671
+ {
672
+ return;
673
+ }
674
+
675
+ Blueprint->GetAllGraphs(OutGraphs);
676
+ }
677
+
678
+ bool UGitNexusBlueprintAnalyzerCommandlet::IsTargetFunctionNode(
679
+ const UEdGraphNode* Node,
680
+ const FString& TargetSymbolKey,
681
+ const FString& TargetClassName,
682
+ const FString& TargetFunctionName
683
+ ) const
684
+ {
685
+ if (const UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Node))
686
+ {
687
+ if (const UFunction* TargetFunction = CallNode->GetTargetFunction())
688
+ {
689
+ const UClass* OwnerClass = TargetFunction->GetOwnerClass();
690
+ const FString SymbolKey = OwnerClass
691
+ ? FString::Printf(TEXT("%s::%s"), *OwnerClass->GetName(), *TargetFunction->GetName())
692
+ : TargetFunction->GetName();
693
+ return SymbolKey == TargetSymbolKey
694
+ || TargetFunction->GetName() == TargetFunctionName
695
+ || (OwnerClass && OwnerClass->GetName() == TargetClassName && TargetFunction->GetName() == TargetFunctionName);
696
+ }
697
+ }
698
+
699
+ if (const UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node))
700
+ {
701
+ return EventNode->GetFunctionName().ToString() == TargetFunctionName;
702
+ }
703
+
704
+ return false;
705
+ }
706
+
707
+ TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildReferenceJson(UBlueprint* Blueprint, const UEdGraph* Graph, const UEdGraphNode* Node) const
708
+ {
709
+ TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
710
+ Object->SetStringField(TEXT("asset_path"), Blueprint ? Blueprint->GetPathName() : FString());
711
+ Object->SetStringField(TEXT("graph_name"), Graph ? Graph->GetName() : FString());
712
+ Object->SetStringField(TEXT("node_kind"), Node ? Node->GetClass()->GetName() : FString());
713
+ Object->SetStringField(TEXT("node_title"), Node ? Node->GetNodeTitle(ENodeTitleType::ListView).ToString() : FString());
714
+ Object->SetStringField(TEXT("blueprint_owner_function"), Graph ? Graph->GetName() : FString());
715
+ Object->SetStringField(TEXT("chain_anchor_id"), Node ? Node->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens) : FString());
716
+ Object->SetStringField(TEXT("source"), TEXT("editor_confirmed"));
717
+ return Object;
718
+ }
719
+
720
+ TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildChainNodeJson(
721
+ const UEdGraph* Graph, const UEdGraphNode* Node, int32 Depth,
722
+ const FString& TraversedFromPinName, const FGuid& TraversedFromNodeId) const
723
+ {
724
+ TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
725
+ Object->SetStringField(TEXT("node_id"), Node ? Node->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens) : FString());
726
+ Object->SetStringField(TEXT("graph_name"), Graph ? Graph->GetName() : FString());
727
+ Object->SetStringField(TEXT("node_kind"), Node ? Node->GetClass()->GetName() : FString());
728
+ Object->SetStringField(TEXT("node_title"), Node ? Node->GetNodeTitle(ENodeTitleType::ListView).ToString() : FString());
729
+ Object->SetNumberField(TEXT("depth"), Depth);
730
+
731
+ if (!TraversedFromPinName.IsEmpty())
732
+ {
733
+ Object->SetStringField(TEXT("traversed_from_pin"), TraversedFromPinName);
734
+ }
735
+ if (TraversedFromNodeId.IsValid())
736
+ {
737
+ Object->SetStringField(TEXT("traversed_from_node"), TraversedFromNodeId.ToString(EGuidFormats::DigitsWithHyphens));
738
+ }
739
+
740
+ if (Node)
741
+ {
742
+ AnnotateNodeMetadata(Object, Node);
743
+ Object->SetObjectField(TEXT("pins"), BuildPinsJson(Node));
744
+ AnnotateNodeDetails(Object, Node);
745
+ }
746
+
747
+ return Object;
748
+ }
749
+
750
+ TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildPinJson(const UEdGraphPin* Pin) const
751
+ {
752
+ TSharedPtr<FJsonObject> PinObj = MakeShared<FJsonObject>();
753
+ if (!Pin)
754
+ {
755
+ return PinObj;
756
+ }
757
+
758
+ PinObj->SetStringField(TEXT("name"), Pin->PinName.ToString());
759
+ PinObj->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("input") : TEXT("output"));
760
+ PinObj->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
761
+
762
+ if (Pin->PinType.PinSubCategoryObject.IsValid())
763
+ {
764
+ PinObj->SetStringField(TEXT("sub_type"), Pin->PinType.PinSubCategoryObject->GetName());
765
+ }
766
+
767
+ if (!Pin->DefaultValue.IsEmpty())
768
+ {
769
+ PinObj->SetStringField(TEXT("default_value"), Pin->DefaultValue);
770
+ }
771
+ else if (Pin->DefaultObject)
772
+ {
773
+ PinObj->SetStringField(TEXT("default_value"), Pin->DefaultObject->GetPathName());
774
+ }
775
+
776
+ if (Pin->LinkedTo.Num() > 0)
777
+ {
778
+ TArray<TSharedPtr<FJsonValue>> ConnectedTo;
779
+ TArray<TSharedPtr<FJsonValue>> ConnectedToTitle;
780
+ for (const UEdGraphPin* LinkedPin : Pin->LinkedTo)
781
+ {
782
+ if (LinkedPin && LinkedPin->GetOwningNode())
783
+ {
784
+ ConnectedTo.Add(MakeShared<FJsonValueString>(
785
+ LinkedPin->GetOwningNode()->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens)));
786
+ ConnectedToTitle.Add(MakeShared<FJsonValueString>(
787
+ LinkedPin->GetOwningNode()->GetNodeTitle(ENodeTitleType::ListView).ToString()));
788
+ }
789
+ }
790
+ PinObj->SetArrayField(TEXT("connected_to"), ConnectedTo);
791
+ PinObj->SetArrayField(TEXT("connected_to_title"), ConnectedToTitle);
792
+ }
793
+
794
+ return PinObj;
795
+ }
796
+
797
+ TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildPinsJson(const UEdGraphNode* Node) const
798
+ {
799
+ TSharedPtr<FJsonObject> PinsObj = MakeShared<FJsonObject>();
800
+ if (!Node)
801
+ {
802
+ return PinsObj;
803
+ }
804
+
805
+ TArray<TSharedPtr<FJsonValue>> ExecPins;
806
+ TArray<TSharedPtr<FJsonValue>> DataPins;
807
+
808
+ for (const UEdGraphPin* Pin : Node->Pins)
809
+ {
810
+ if (!Pin)
811
+ {
812
+ continue;
813
+ }
814
+
815
+ TSharedPtr<FJsonObject> PinJson = BuildPinJson(Pin);
816
+ if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec)
817
+ {
818
+ ExecPins.Add(MakeShared<FJsonValueObject>(PinJson));
819
+ }
820
+ else
821
+ {
822
+ DataPins.Add(MakeShared<FJsonValueObject>(PinJson));
823
+ }
824
+ }
825
+
826
+ PinsObj->SetArrayField(TEXT("exec_pins"), ExecPins);
827
+ PinsObj->SetArrayField(TEXT("data_pins"), DataPins);
828
+ return PinsObj;
829
+ }
830
+
831
+ void UGitNexusBlueprintAnalyzerCommandlet::AnnotateNodeMetadata(TSharedPtr<FJsonObject>& NodeObj, const UEdGraphNode* Node) const
832
+ {
833
+ if (!Node)
834
+ {
835
+ return;
836
+ }
837
+
838
+ NodeObj->SetBoolField(TEXT("is_enabled"), Node->IsNodeEnabled());
839
+
840
+ if (!Node->NodeComment.IsEmpty())
841
+ {
842
+ NodeObj->SetStringField(TEXT("comment"), Node->NodeComment);
843
+ }
844
+ }
845
+
846
+ void UGitNexusBlueprintAnalyzerCommandlet::AnnotateNodeDetails(TSharedPtr<FJsonObject>& NodeObj, const UEdGraphNode* Node) const
847
+ {
848
+ if (!Node)
849
+ {
850
+ return;
851
+ }
852
+
853
+ TSharedPtr<FJsonObject> Details = MakeShared<FJsonObject>();
854
+ bool bHasDetails = false;
855
+
856
+ if (const UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Node))
857
+ {
858
+ Details->SetBoolField(TEXT("is_pure"), CallNode->IsNodePure());
859
+ if (const UFunction* TargetFunction = CallNode->GetTargetFunction())
860
+ {
861
+ Details->SetStringField(TEXT("function_name"), TargetFunction->GetName());
862
+ if (const UClass* OwnerClass = TargetFunction->GetOwnerClass())
863
+ {
864
+ Details->SetStringField(TEXT("target_class"), OwnerClass->GetName());
865
+ }
866
+ }
867
+ bHasDetails = true;
868
+ }
869
+ else if (const UK2Node_VariableGet* GetNode = Cast<UK2Node_VariableGet>(Node))
870
+ {
871
+ Details->SetStringField(TEXT("variable_name"), GetNode->GetVarName().ToString());
872
+ Details->SetStringField(TEXT("node_role"), TEXT("variable_get"));
873
+ bHasDetails = true;
874
+ }
875
+ else if (const UK2Node_VariableSet* SetNode = Cast<UK2Node_VariableSet>(Node))
876
+ {
877
+ Details->SetStringField(TEXT("variable_name"), SetNode->GetVarName().ToString());
878
+ Details->SetStringField(TEXT("node_role"), TEXT("variable_set"));
879
+ bHasDetails = true;
880
+ }
881
+ else if (Cast<UK2Node_IfThenElse>(Node))
882
+ {
883
+ Details->SetStringField(TEXT("branch_type"), TEXT("if_then_else"));
884
+ bHasDetails = true;
885
+ }
886
+ else if (Cast<UK2Node_Switch>(Node))
887
+ {
888
+ Details->SetStringField(TEXT("branch_type"), TEXT("switch"));
889
+ bHasDetails = true;
890
+ }
891
+
892
+ if (bHasDetails)
893
+ {
894
+ NodeObj->SetObjectField(TEXT("details"), Details);
895
+ }
896
+ }
897
+
898
+ UEdGraphNode* UGitNexusBlueprintAnalyzerCommandlet::FindNodeByGuid(UBlueprint* Blueprint, const FString& NodeGuid) const
899
+ {
900
+ if (!Blueprint)
901
+ {
902
+ return nullptr;
903
+ }
904
+
905
+ FGuid Guid;
906
+ if (!FGuid::Parse(NodeGuid, Guid))
907
+ {
908
+ return nullptr;
909
+ }
910
+
911
+ TArray<UEdGraph*> Graphs;
912
+ CollectBlueprintGraphs(Blueprint, Graphs);
913
+ for (UEdGraph* Graph : Graphs)
914
+ {
915
+ if (!Graph)
916
+ {
917
+ continue;
918
+ }
919
+
920
+ for (UEdGraphNode* Node : Graph->Nodes)
921
+ {
922
+ if (Node && Node->NodeGuid == Guid)
923
+ {
924
+ return Node;
925
+ }
926
+ }
927
+ }
928
+
929
+ return nullptr;
930
+ }