@duytransipher/gitnexus 1.2.1 → 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.
@@ -9,8 +9,13 @@
9
9
  #include "EdGraph/EdGraphNode.h"
10
10
  #include "EdGraph/EdGraphPin.h"
11
11
  #include "Engine/Blueprint.h"
12
+ #include "EdGraphSchema_K2.h"
12
13
  #include "K2Node_CallFunction.h"
13
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"
14
19
  #include "Kismet2/BlueprintEditorUtils.h"
15
20
  #include "Misc/FileHelper.h"
16
21
  #include "Misc/Guid.h"
@@ -19,6 +24,7 @@
19
24
  #include "Serialization/JsonReader.h"
20
25
  #include "Serialization/JsonSerializer.h"
21
26
  #include "UObject/SoftObjectPath.h"
27
+ #include "UObject/GarbageCollection.h"
22
28
 
23
29
  UGitNexusBlueprintAnalyzerCommandlet::UGitNexusBlueprintAnalyzerCommandlet()
24
30
  {
@@ -43,7 +49,17 @@ int32 UGitNexusBlueprintAnalyzerCommandlet::Main(const FString& Params)
43
49
 
44
50
  if (Operation.Equals(TEXT("SyncAssets"), ESearchCase::IgnoreCase))
45
51
  {
46
- return RunSyncAssets(OutputJsonPath);
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);
47
63
  }
48
64
 
49
65
  if (Operation.Equals(TEXT("FindNativeBlueprintReferences"), ESearchCase::IgnoreCase))
@@ -76,15 +92,141 @@ int32 UGitNexusBlueprintAnalyzerCommandlet::Main(const FString& Params)
76
92
  return 1;
77
93
  }
78
94
 
79
- int32 UGitNexusBlueprintAnalyzerCommandlet::RunSyncAssets(const FString& OutputJsonPath)
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)
80
121
  {
81
122
  TArray<TSharedPtr<FJsonValue>> AssetValues;
123
+ IAssetRegistry& AssetRegistry = IAssetRegistry::GetChecked();
82
124
 
83
- for (const FAssetData& AssetData : GetAllBlueprintAssets())
125
+ for (const FAssetData& AssetData : Assets)
84
126
  {
85
- UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
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;
86
223
  if (!Blueprint)
87
224
  {
225
+ if (!LoadedAsset)
226
+ {
227
+ SkippedCount++;
228
+ UE_LOG(LogTemp, Warning, TEXT("GitNexusBlueprintAnalyzer: Skipped asset (failed to load): %s"), *AssetData.PackageName.ToString());
229
+ }
88
230
  continue;
89
231
  }
90
232
 
@@ -154,17 +296,35 @@ int32 UGitNexusBlueprintAnalyzerCommandlet::RunSyncAssets(const FString& OutputJ
154
296
  AssetObject->SetArrayField(TEXT("native_function_refs"), NativeFunctionRefValues);
155
297
 
156
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
+ }
157
308
  }
158
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
+
159
316
  TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
160
317
  RootObject->SetNumberField(TEXT("version"), 1);
161
318
  RootObject->SetStringField(TEXT("generated_at"), FDateTime::UtcNow().ToIso8601());
162
319
  RootObject->SetStringField(TEXT("project_path"), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()));
320
+ RootObject->SetStringField(TEXT("mode"), TEXT("deep"));
163
321
  RootObject->SetArrayField(TEXT("assets"), AssetValues);
164
322
 
165
323
  return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
166
324
  }
167
325
 
326
+ // ── FindNativeBlueprintReferences ───────────────────────────────────────
327
+
168
328
  int32 UGitNexusBlueprintAnalyzerCommandlet::RunFindNativeBlueprintReferences(
169
329
  const FString& OutputJsonPath,
170
330
  const FString& CandidatesJsonPath,
@@ -215,6 +375,8 @@ int32 UGitNexusBlueprintAnalyzerCommandlet::RunFindNativeBlueprintReferences(
215
375
  return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
216
376
  }
217
377
 
378
+ // ── ExpandBlueprintChain ────────────────────────────────────────────────
379
+
218
380
  int32 UGitNexusBlueprintAnalyzerCommandlet::RunExpandBlueprintChain(
219
381
  const FString& OutputJsonPath,
220
382
  const FString& AssetPath,
@@ -235,26 +397,36 @@ int32 UGitNexusBlueprintAnalyzerCommandlet::RunExpandBlueprintChain(
235
397
  return 1;
236
398
  }
237
399
 
400
+ struct FChainFrontierEntry
401
+ {
402
+ UEdGraphNode* Node;
403
+ int32 Depth;
404
+ FString TraversedFromPinName;
405
+ FGuid TraversedFromNodeId;
406
+ };
407
+
238
408
  TArray<TSharedPtr<FJsonValue>> NodeValues;
239
409
  TSet<FGuid> Visited;
240
- TArray<TPair<UEdGraphNode*, int32>> Frontier;
241
- Frontier.Add(TPair<UEdGraphNode*, int32>(StartNode, 0));
410
+ TArray<FChainFrontierEntry> Frontier;
411
+ Frontier.Add({ StartNode, 0, FString(), FGuid() });
242
412
  Visited.Add(StartNode->NodeGuid);
243
413
 
244
414
  const bool bUpstream = Direction.Equals(TEXT("upstream"), ESearchCase::IgnoreCase);
245
415
 
246
416
  while (Frontier.Num() > 0)
247
417
  {
248
- const TPair<UEdGraphNode*, int32> Current = Frontier[0];
418
+ const FChainFrontierEntry Current = Frontier[0];
249
419
  Frontier.RemoveAt(0);
250
420
 
251
- NodeValues.Add(MakeShared<FJsonValueObject>(BuildChainNodeJson(Current.Key->GetGraph(), Current.Key, Current.Value)));
252
- if (Current.Value >= MaxDepth)
421
+ NodeValues.Add(MakeShared<FJsonValueObject>(BuildChainNodeJson(
422
+ Current.Node->GetGraph(), Current.Node, Current.Depth,
423
+ Current.TraversedFromPinName, Current.TraversedFromNodeId)));
424
+ if (Current.Depth >= MaxDepth)
253
425
  {
254
426
  continue;
255
427
  }
256
428
 
257
- for (UEdGraphPin* Pin : Current.Key->Pins)
429
+ for (UEdGraphPin* Pin : Current.Node->Pins)
258
430
  {
259
431
  if (!Pin)
260
432
  {
@@ -280,8 +452,9 @@ int32 UGitNexusBlueprintAnalyzerCommandlet::RunExpandBlueprintChain(
280
452
  continue;
281
453
  }
282
454
 
455
+ // NOTE: In diamond-shaped graphs, BFS only records the first-discovered parent.
283
456
  Visited.Add(NextNode->NodeGuid);
284
- Frontier.Add(TPair<UEdGraphNode*, int32>(NextNode, Current.Value + 1));
457
+ Frontier.Add({ NextNode, Current.Depth + 1, Pin->PinName.ToString(), Current.Node->NodeGuid });
285
458
  }
286
459
  }
287
460
  }
@@ -291,6 +464,8 @@ int32 UGitNexusBlueprintAnalyzerCommandlet::RunExpandBlueprintChain(
291
464
  return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
292
465
  }
293
466
 
467
+ // ── Utility functions ───────────────────────────────────────────────────
468
+
294
469
  bool UGitNexusBlueprintAnalyzerCommandlet::WriteJsonToFile(const FString& OutputJsonPath, const TSharedPtr<FJsonObject>& RootObject) const
295
470
  {
296
471
  FString JsonText;
@@ -300,7 +475,7 @@ bool UGitNexusBlueprintAnalyzerCommandlet::WriteJsonToFile(const FString& Output
300
475
  return false;
301
476
  }
302
477
 
303
- return FFileHelper::SaveStringToFile(JsonText, *OutputJsonPath);
478
+ return FFileHelper::SaveStringToFile(JsonText, *OutputJsonPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
304
479
  }
305
480
 
306
481
  TArray<FString> UGitNexusBlueprintAnalyzerCommandlet::LoadCandidateAssets(const FString& CandidatesJsonPath) const
@@ -346,7 +521,70 @@ TArray<FString> UGitNexusBlueprintAnalyzerCommandlet::LoadCandidateAssets(const
346
521
  return Result;
347
522
  }
348
523
 
349
- TArray<FAssetData> UGitNexusBlueprintAnalyzerCommandlet::GetAllBlueprintAssets() const
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
350
588
  {
351
589
  FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
352
590
  IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
@@ -356,9 +594,69 @@ TArray<FAssetData> UGitNexusBlueprintAnalyzerCommandlet::GetAllBlueprintAssets()
356
594
  Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
357
595
  Filter.bRecursiveClasses = true;
358
596
 
359
- TArray<FAssetData> Assets;
360
- AssetRegistry.GetAssets(Filter, Assets);
361
- return Assets;
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;
362
660
  }
363
661
 
364
662
  UBlueprint* UGitNexusBlueprintAnalyzerCommandlet::LoadBlueprintFromAssetPath(const FString& AssetPath) const
@@ -419,7 +717,9 @@ TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildReferenceJson
419
717
  return Object;
420
718
  }
421
719
 
422
- TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildChainNodeJson(const UEdGraph* Graph, const UEdGraphNode* Node, int32 Depth) const
720
+ TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildChainNodeJson(
721
+ const UEdGraph* Graph, const UEdGraphNode* Node, int32 Depth,
722
+ const FString& TraversedFromPinName, const FGuid& TraversedFromNodeId) const
423
723
  {
424
724
  TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
425
725
  Object->SetStringField(TEXT("node_id"), Node ? Node->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens) : FString());
@@ -427,9 +727,174 @@ TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildChainNodeJson
427
727
  Object->SetStringField(TEXT("node_kind"), Node ? Node->GetClass()->GetName() : FString());
428
728
  Object->SetStringField(TEXT("node_title"), Node ? Node->GetNodeTitle(ENodeTitleType::ListView).ToString() : FString());
429
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
+
430
747
  return Object;
431
748
  }
432
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
+
433
898
  UEdGraphNode* UGitNexusBlueprintAnalyzerCommandlet::FindNodeByGuid(UBlueprint* Blueprint, const FString& NodeGuid) const
434
899
  {
435
900
  if (!Blueprint)
@@ -6,6 +6,7 @@
6
6
  class UBlueprint;
7
7
  class UEdGraph;
8
8
  class UEdGraphNode;
9
+ class UEdGraphPin;
9
10
 
10
11
  UCLASS()
11
12
  class GITNEXUSUNREAL_API UGitNexusBlueprintAnalyzerCommandlet : public UCommandlet
@@ -18,7 +19,12 @@ public:
18
19
  virtual int32 Main(const FString& Params) override;
19
20
 
20
21
  private:
21
- int32 RunSyncAssets(const FString& OutputJsonPath);
22
+ // ── SyncAssets ────────────────────────────────────────────────────────
23
+ int32 RunSyncAssets(const FString& OutputJsonPath, const FString& FilterJsonPath, bool bDeepMode);
24
+ int32 RunSyncAssetsMetadata(const FString& OutputJsonPath, const TArray<FAssetData>& Assets);
25
+ int32 RunSyncAssetsDeep(const FString& OutputJsonPath, const TArray<FAssetData>& Assets);
26
+
27
+ // ── Other operations ─────────────────────────────────────────────────
22
28
  int32 RunFindNativeBlueprintReferences(
23
29
  const FString& OutputJsonPath,
24
30
  const FString& CandidatesJsonPath,
@@ -34,13 +40,29 @@ private:
34
40
  int32 MaxDepth
35
41
  );
36
42
 
43
+ // ── Helpers ──────────────────────────────────────────────────────────
44
+
45
+ struct FFilterPrefixes
46
+ {
47
+ TArray<FString> IncludePrefixes;
48
+ TArray<FString> ExcludePrefixes;
49
+ };
50
+
37
51
  bool WriteJsonToFile(const FString& OutputJsonPath, const TSharedPtr<FJsonObject>& RootObject) const;
38
52
  TArray<FString> LoadCandidateAssets(const FString& CandidatesJsonPath) const;
39
- TArray<FAssetData> GetAllBlueprintAssets() const;
53
+ FFilterPrefixes LoadFilterPrefixes(const FString& FilterJsonPath) const;
54
+ TArray<FAssetData> GetAllBlueprintAssets(const FFilterPrefixes& Filters = FFilterPrefixes()) const;
40
55
  UBlueprint* LoadBlueprintFromAssetPath(const FString& AssetPath) const;
41
56
  void CollectBlueprintGraphs(UBlueprint* Blueprint, TArray<UEdGraph*>& OutGraphs) const;
42
57
  bool IsTargetFunctionNode(const UEdGraphNode* Node, const FString& TargetSymbolKey, const FString& TargetClassName, const FString& TargetFunctionName) const;
43
58
  TSharedPtr<FJsonObject> BuildReferenceJson(UBlueprint* Blueprint, const UEdGraph* Graph, const UEdGraphNode* Node) const;
44
- TSharedPtr<FJsonObject> BuildChainNodeJson(const UEdGraph* Graph, const UEdGraphNode* Node, int32 Depth) const;
59
+ TSharedPtr<FJsonObject> BuildChainNodeJson(
60
+ const UEdGraph* Graph, const UEdGraphNode* Node, int32 Depth,
61
+ const FString& TraversedFromPinName = FString(),
62
+ const FGuid& TraversedFromNodeId = FGuid()) const;
63
+ TSharedPtr<FJsonObject> BuildPinJson(const UEdGraphPin* Pin) const;
64
+ TSharedPtr<FJsonObject> BuildPinsJson(const UEdGraphNode* Node) const;
65
+ void AnnotateNodeMetadata(TSharedPtr<FJsonObject>& NodeObj, const UEdGraphNode* Node) const;
66
+ void AnnotateNodeDetails(TSharedPtr<FJsonObject>& NodeObj, const UEdGraphNode* Node) const;
45
67
  UEdGraphNode* FindNodeByGuid(UBlueprint* Blueprint, const FString& NodeGuid) const;
46
68
  };