@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.
- package/dist/cli/analyze.js +1 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/tool.d.ts +1 -0
- package/dist/cli/tool.js +31 -9
- package/dist/cli/unreal-progress.d.ts +12 -0
- package/dist/cli/unreal-progress.js +66 -0
- package/dist/mcp/local/local-backend.js +11 -5
- package/dist/mcp/tools.js +108 -103
- package/dist/unreal/blueprint-ingestion.d.ts +8 -1
- package/dist/unreal/blueprint-ingestion.js +35 -3
- package/dist/unreal/bridge.d.ts +1 -1
- package/dist/unreal/bridge.js +94 -2
- package/dist/unreal/types.d.ts +4 -0
- package/package.json +1 -1
- package/vendor/GitNexusUnreal/Source/GitNexusUnreal/Private/GitNexusBlueprintAnalyzerCommandlet.cpp +482 -17
- package/vendor/GitNexusUnreal/Source/GitNexusUnreal/Public/GitNexusBlueprintAnalyzerCommandlet.h +25 -3
package/vendor/GitNexusUnreal/Source/GitNexusUnreal/Private/GitNexusBlueprintAnalyzerCommandlet.cpp
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 :
|
|
125
|
+
for (const FAssetData& AssetData : Assets)
|
|
84
126
|
{
|
|
85
|
-
|
|
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<
|
|
241
|
-
Frontier.Add(
|
|
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
|
|
418
|
+
const FChainFrontierEntry Current = Frontier[0];
|
|
249
419
|
Frontier.RemoveAt(0);
|
|
250
420
|
|
|
251
|
-
NodeValues.Add(MakeShared<FJsonValueObject>(BuildChainNodeJson(
|
|
252
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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>
|
|
360
|
-
AssetRegistry.GetAssets(Filter,
|
|
361
|
-
|
|
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(
|
|
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)
|
package/vendor/GitNexusUnreal/Source/GitNexusUnreal/Public/GitNexusBlueprintAnalyzerCommandlet.h
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
};
|