@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.
- 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 +3 -2
- package/dist/mcp/local/local-backend.js +4 -4
- 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 +66 -1
- package/dist/unreal/types.d.ts +4 -0
- package/package.json +1 -1
- package/vendor/GitNexusUnreal/Source/GitNexusUnreal/Private/GitNexusBlueprintAnalyzerCommandlet.cpp +930 -465
- package/vendor/GitNexusUnreal/Source/GitNexusUnreal/Public/GitNexusBlueprintAnalyzerCommandlet.h +25 -3
package/vendor/GitNexusUnreal/Source/GitNexusUnreal/Private/GitNexusBlueprintAnalyzerCommandlet.cpp
CHANGED
|
@@ -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 "
|
|
13
|
-
#include "
|
|
14
|
-
#include "
|
|
15
|
-
#include "
|
|
16
|
-
#include "
|
|
17
|
-
#include "
|
|
18
|
-
#include "
|
|
19
|
-
#include "
|
|
20
|
-
#include "
|
|
21
|
-
#include "
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (Operation.
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
FString
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
FParse::Value(*Params, TEXT("
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
TArray<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
{
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
const FString&
|
|
383
|
-
const FString&
|
|
384
|
-
const FString&
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return
|
|
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
|
+
}
|