@duytransipher/gitnexus 1.2.1 → 1.2.2

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/tool.js CHANGED
@@ -17,6 +17,8 @@
17
17
  */
18
18
  import { writeSync } from 'node:fs';
19
19
  import { LocalBackend } from '../mcp/local/local-backend.js';
20
+ import { withUnrealProgress } from './unreal-progress.js';
21
+ const isUnrealError = (r) => r?.status === 'error' || r?.error != null;
20
22
  let _backend = null;
21
23
  async function getBackend() {
22
24
  if (_backend)
@@ -127,9 +129,16 @@ export async function cypherCommand(query, options) {
127
129
  }
128
130
  export async function syncUnrealAssetManifestCommand(options) {
129
131
  const backend = await getBackend();
130
- const result = await backend.callTool('sync_unreal_asset_manifest', {
131
- repo: options?.repo,
132
- });
132
+ const result = await withUnrealProgress(() => backend.callTool('sync_unreal_asset_manifest', { repo: options?.repo }), { phaseLabel: 'Syncing Unreal asset manifest', successLabel: 'Manifest synced', failLabel: 'Manifest sync failed', isError: isUnrealError });
133
+ if (result?.status === 'error') {
134
+ console.error(`\n Error: ${result.error}\n`);
135
+ process.exit(1);
136
+ }
137
+ if (result?.asset_count != null) {
138
+ console.log(` ${result.asset_count.toLocaleString()} Blueprint assets indexed`);
139
+ if (result.manifest_path)
140
+ console.log(` ${result.manifest_path}`);
141
+ }
133
142
  output(result);
134
143
  }
135
144
  export async function findNativeBlueprintReferencesCommand(functionName, options) {
@@ -138,7 +147,7 @@ export async function findNativeBlueprintReferencesCommand(functionName, options
138
147
  process.exit(1);
139
148
  }
140
149
  const backend = await getBackend();
141
- const result = await backend.callTool('find_native_blueprint_references', {
150
+ const result = await withUnrealProgress(() => backend.callTool('find_native_blueprint_references', {
142
151
  function: functionName || undefined,
143
152
  symbol_uid: options?.uid,
144
153
  class_name: options?.className,
@@ -146,7 +155,11 @@ export async function findNativeBlueprintReferencesCommand(functionName, options
146
155
  refresh_manifest: options?.refreshManifest ?? false,
147
156
  max_candidates: options?.maxCandidates ? parseInt(options.maxCandidates, 10) : undefined,
148
157
  repo: options?.repo,
149
- });
158
+ }), { phaseLabel: 'Scanning Blueprints for native references', successLabel: 'Blueprint scan complete', failLabel: 'Blueprint scan failed', isError: isUnrealError });
159
+ if (result?.status === 'error') {
160
+ console.error(`\n Error: ${result.error}\n`);
161
+ process.exit(1);
162
+ }
150
163
  output(result);
151
164
  }
152
165
  export async function expandBlueprintChainCommand(assetPath, chainAnchorId, options) {
@@ -155,13 +168,17 @@ export async function expandBlueprintChainCommand(assetPath, chainAnchorId, opti
155
168
  process.exit(1);
156
169
  }
157
170
  const backend = await getBackend();
158
- const result = await backend.callTool('expand_blueprint_chain', {
171
+ const result = await withUnrealProgress(() => backend.callTool('expand_blueprint_chain', {
159
172
  asset_path: assetPath,
160
173
  chain_anchor_id: chainAnchorId,
161
174
  direction: options?.direction || 'downstream',
162
175
  max_depth: options?.depth ? parseInt(options.depth, 10) : undefined,
163
176
  repo: options?.repo,
164
- });
177
+ }), { phaseLabel: 'Expanding Blueprint chain', successLabel: 'Chain expanded', failLabel: 'Chain expansion failed', isError: isUnrealError });
178
+ if (result?.status === 'error') {
179
+ console.error(`\n Error: ${result.error}\n`);
180
+ process.exit(1);
181
+ }
165
182
  output(result);
166
183
  }
167
184
  export async function findBlueprintsDerivedFromNativeClassCommand(className, options) {
@@ -170,11 +187,15 @@ export async function findBlueprintsDerivedFromNativeClassCommand(className, opt
170
187
  process.exit(1);
171
188
  }
172
189
  const backend = await getBackend();
173
- const result = await backend.callTool('find_blueprints_derived_from_native_class', {
190
+ const result = await withUnrealProgress(() => backend.callTool('find_blueprints_derived_from_native_class', {
174
191
  class_name: className,
175
192
  refresh_manifest: options?.refreshManifest ?? false,
176
193
  max_results: options?.maxResults ? parseInt(options.maxResults, 10) : undefined,
177
194
  repo: options?.repo,
178
- });
195
+ }), { phaseLabel: 'Finding derived Blueprints', successLabel: 'Derived Blueprint search complete', failLabel: 'Derived Blueprint search failed', isError: isUnrealError });
196
+ if (result?.status === 'error') {
197
+ console.error(`\n Error: ${result.error}\n`);
198
+ process.exit(1);
199
+ }
179
200
  output(result);
180
201
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Spinner helper for long-running Unreal CLI commands.
3
+ * Uses braille-dot animation with elapsed time — CLI layer only.
4
+ */
5
+ export interface SpinnerOptions {
6
+ phaseLabel: string;
7
+ successLabel?: string;
8
+ failLabel?: string;
9
+ /** Check if the result indicates an error (for non-throwing error returns). */
10
+ isError?: (result: any) => boolean;
11
+ }
12
+ export declare function withUnrealProgress<T>(operation: () => Promise<T>, opts: SpinnerOptions): Promise<T>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Spinner helper for long-running Unreal CLI commands.
3
+ * Uses braille-dot animation with elapsed time — CLI layer only.
4
+ */
5
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+ const GREEN = '\x1b[92m';
7
+ const RED = '\x1b[91m';
8
+ const YELLOW = '\x1b[93m';
9
+ const RESET = '\x1b[0m';
10
+ const BOLD = '\x1b[1m';
11
+ const DIM = '\x1b[2m';
12
+ export async function withUnrealProgress(operation, opts) {
13
+ const start = Date.now();
14
+ let frame = 0;
15
+ let aborted = false;
16
+ const clearLine = () => process.stdout.write('\r\x1b[2K');
17
+ const render = () => {
18
+ const elapsed = Math.round((Date.now() - start) / 1000);
19
+ const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
20
+ const time = elapsed > 0 ? ` ${DIM}(${elapsed}s)${RESET}` : '';
21
+ clearLine();
22
+ process.stdout.write(` ${YELLOW}${spinner}${RESET} ${opts.phaseLabel}...${time}`);
23
+ frame++;
24
+ };
25
+ // Initial render + tick every 80ms for smooth animation
26
+ render();
27
+ const timer = setInterval(render, 80);
28
+ const sigintHandler = () => {
29
+ if (aborted)
30
+ process.exit(1);
31
+ aborted = true;
32
+ clearInterval(timer);
33
+ clearLine();
34
+ process.stdout.write(` ${RED}✗${RESET} Interrupted\n`);
35
+ process.exit(130);
36
+ };
37
+ process.on('SIGINT', sigintHandler);
38
+ const cleanup = () => {
39
+ clearInterval(timer);
40
+ process.removeListener('SIGINT', sigintHandler);
41
+ };
42
+ try {
43
+ const result = await operation();
44
+ cleanup();
45
+ const totalSec = ((Date.now() - start) / 1000).toFixed(1);
46
+ if (opts.isError?.(result)) {
47
+ const label = opts.failLabel || 'Failed';
48
+ clearLine();
49
+ process.stdout.write(` ${RED}${BOLD}✗${RESET} ${label} ${DIM}(${totalSec}s)${RESET}\n`);
50
+ }
51
+ else {
52
+ const label = opts.successLabel || 'Done';
53
+ clearLine();
54
+ process.stdout.write(` ${GREEN}${BOLD}✓${RESET} ${label} ${DIM}(${totalSec}s)${RESET}\n`);
55
+ }
56
+ return result;
57
+ }
58
+ catch (error) {
59
+ cleanup();
60
+ const totalSec = ((Date.now() - start) / 1000).toFixed(1);
61
+ const label = opts.failLabel || 'Failed';
62
+ clearLine();
63
+ process.stdout.write(` ${RED}${BOLD}✗${RESET} ${label} ${DIM}(${totalSec}s)${RESET}\n`);
64
+ throw error;
65
+ }
66
+ }
@@ -199,7 +199,13 @@ export class LocalBackend {
199
199
  if (this.repos.size === 1) {
200
200
  return this.repos.values().next().value;
201
201
  }
202
- return null; // Multiple repos, no param ambiguous
202
+ // Multiple repos try to match by current working directory
203
+ const cwd = process.cwd();
204
+ for (const handle of this.repos.values()) {
205
+ if (cwd.startsWith(handle.repoPath))
206
+ return handle;
207
+ }
208
+ return null; // Multiple repos, no CWD match — ambiguous
203
209
  }
204
210
  // ─── Lazy LadybugDB Init ────────────────────────────────────────────
205
211
  async ensureInitialized(repoId) {
@@ -40,7 +40,17 @@ async function readUELogErrors(config) {
40
40
  const logPath = path.join(projectDir, 'Saved', 'Logs', `${projectName}.log`);
41
41
  const content = await fs.readFile(logPath, 'utf-8');
42
42
  const lines = content.split(/\r?\n/);
43
- const errorLines = lines.filter(l => /\bError\b/i.test(l) && !/^LogWindows.*Failed to get driver/i.test(l.replace(/^\[.*?\]\[\s*\d+\]/, '')));
43
+ const stripped = (l) => l.replace(/^\[.*?\]\[\s*\d+\]/, '');
44
+ // Skip callstack lines, driver errors, and empty error lines
45
+ const isNoise = (l) => {
46
+ const s = stripped(l);
47
+ return /^LogWindows.*Failed to get driver/i.test(s)
48
+ || /\[Callstack\]/i.test(s)
49
+ || /^LogWindows: Error:\s*$/i.test(s)
50
+ || /^LogWindows: Error: ===/.test(s)
51
+ || /^LogWindows: Error: Fatal error!/i.test(s);
52
+ };
53
+ const errorLines = lines.filter(l => /\bError\b/i.test(l) && !isNoise(l));
44
54
  if (errorLines.length === 0)
45
55
  return '';
46
56
  return 'UE Log errors:\n' + errorLines.slice(-10).join('\n');
@@ -75,6 +85,23 @@ export async function syncUnrealAssetManifest(storagePath, config) {
75
85
  };
76
86
  }
77
87
  catch (error) {
88
+ // UE may exit non-zero due to Blueprint compilation warnings even though
89
+ // the commandlet completed and wrote valid output. Try reading the file first.
90
+ try {
91
+ const stdout = error?.stdout ? String(error.stdout).trim() : '';
92
+ const manifest = await readOutputJson(outputPath, stdout);
93
+ if (manifest && Array.isArray(manifest.assets) && manifest.assets.length > 0) {
94
+ const manifestPath = await saveUnrealAssetManifest(storagePath, manifest);
95
+ return {
96
+ status: 'success',
97
+ manifest_path: manifestPath,
98
+ asset_count: manifest.assets.length,
99
+ generated_at: manifest.generated_at,
100
+ warnings: ['UE exited with non-zero code (likely Blueprint compilation warnings)'],
101
+ };
102
+ }
103
+ }
104
+ catch { /* output file not readable, fall through to error */ }
78
105
  const stderr = error?.stderr ? String(error.stderr).trim() : '';
79
106
  const stdout = error?.stdout ? String(error.stdout).trim() : '';
80
107
  const msg = error instanceof Error ? error.message : String(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duytransipher/gitnexus",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Sipher-maintained fork of GitNexus for graph-powered code intelligence via MCP and CLI.",
5
5
  "author": "DuyTranSipher",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -1,465 +1,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 "K2Node_CallFunction.h"
13
- #include "K2Node_Event.h"
14
- #include "Kismet2/BlueprintEditorUtils.h"
15
- #include "Misc/FileHelper.h"
16
- #include "Misc/Guid.h"
17
- #include "Misc/PackageName.h"
18
- #include "Misc/Paths.h"
19
- #include "Serialization/JsonReader.h"
20
- #include "Serialization/JsonSerializer.h"
21
- #include "UObject/SoftObjectPath.h"
22
-
23
- UGitNexusBlueprintAnalyzerCommandlet::UGitNexusBlueprintAnalyzerCommandlet()
24
- {
25
- IsClient = false;
26
- IsEditor = true;
27
- LogToConsole = true;
28
- ShowErrorCount = true;
29
- }
30
-
31
- int32 UGitNexusBlueprintAnalyzerCommandlet::Main(const FString& Params)
32
- {
33
- FString Operation;
34
- FString OutputJsonPath;
35
- FParse::Value(*Params, TEXT("Operation="), Operation);
36
- FParse::Value(*Params, TEXT("OutputJson="), OutputJsonPath);
37
-
38
- if (Operation.IsEmpty() || OutputJsonPath.IsEmpty())
39
- {
40
- UE_LOG(LogTemp, Error, TEXT("GitNexusBlueprintAnalyzer requires Operation= and OutputJson= parameters."));
41
- return 1;
42
- }
43
-
44
- if (Operation.Equals(TEXT("SyncAssets"), ESearchCase::IgnoreCase))
45
- {
46
- return RunSyncAssets(OutputJsonPath);
47
- }
48
-
49
- if (Operation.Equals(TEXT("FindNativeBlueprintReferences"), ESearchCase::IgnoreCase))
50
- {
51
- FString CandidatesJsonPath;
52
- FString TargetSymbolKey;
53
- FString TargetClassName;
54
- FString TargetFunctionName;
55
- FParse::Value(*Params, TEXT("CandidatesJson="), CandidatesJsonPath);
56
- FParse::Value(*Params, TEXT("TargetSymbolKey="), TargetSymbolKey);
57
- FParse::Value(*Params, TEXT("TargetClass="), TargetClassName);
58
- FParse::Value(*Params, TEXT("TargetFunction="), TargetFunctionName);
59
- return RunFindNativeBlueprintReferences(OutputJsonPath, CandidatesJsonPath, TargetSymbolKey, TargetClassName, TargetFunctionName);
60
- }
61
-
62
- if (Operation.Equals(TEXT("ExpandBlueprintChain"), ESearchCase::IgnoreCase))
63
- {
64
- FString AssetPath;
65
- FString ChainAnchorId;
66
- FString Direction = TEXT("downstream");
67
- int32 MaxDepth = 5;
68
- FParse::Value(*Params, TEXT("AssetPath="), AssetPath);
69
- FParse::Value(*Params, TEXT("ChainAnchorId="), ChainAnchorId);
70
- FParse::Value(*Params, TEXT("Direction="), Direction);
71
- FParse::Value(*Params, TEXT("MaxDepth="), MaxDepth);
72
- return RunExpandBlueprintChain(OutputJsonPath, AssetPath, ChainAnchorId, Direction, MaxDepth);
73
- }
74
-
75
- UE_LOG(LogTemp, Error, TEXT("Unsupported GitNexusBlueprintAnalyzer operation: %s"), *Operation);
76
- return 1;
77
- }
78
-
79
- int32 UGitNexusBlueprintAnalyzerCommandlet::RunSyncAssets(const FString& OutputJsonPath)
80
- {
81
- TArray<TSharedPtr<FJsonValue>> AssetValues;
82
-
83
- for (const FAssetData& AssetData : GetAllBlueprintAssets())
84
- {
85
- UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
86
- if (!Blueprint)
87
- {
88
- continue;
89
- }
90
-
91
- TSharedPtr<FJsonObject> AssetObject = MakeShared<FJsonObject>();
92
- AssetObject->SetStringField(TEXT("asset_path"), AssetData.GetSoftObjectPath().ToString());
93
-
94
- if (Blueprint->GeneratedClass)
95
- {
96
- AssetObject->SetStringField(TEXT("generated_class"), Blueprint->GeneratedClass->GetPathName());
97
- }
98
-
99
- if (Blueprint->ParentClass)
100
- {
101
- AssetObject->SetStringField(TEXT("parent_class"), Blueprint->ParentClass->GetPathName());
102
- }
103
-
104
- TArray<TSharedPtr<FJsonValue>> NativeParents;
105
- for (UClass* Class = Blueprint->ParentClass; Class; Class = Class->GetSuperClass())
106
- {
107
- if (Class->ClassGeneratedBy == nullptr)
108
- {
109
- NativeParents.Add(MakeShared<FJsonValueString>(Class->GetName()));
110
- }
111
- }
112
- AssetObject->SetArrayField(TEXT("native_parents"), NativeParents);
113
-
114
- TArray<FName> Dependencies;
115
- IAssetRegistry::GetChecked().GetDependencies(AssetData.PackageName, Dependencies, UE::AssetRegistry::EDependencyCategory::Package);
116
- TArray<TSharedPtr<FJsonValue>> DependencyValues;
117
- for (const FName& Dependency : Dependencies)
118
- {
119
- DependencyValues.Add(MakeShared<FJsonValueString>(Dependency.ToString()));
120
- }
121
- AssetObject->SetArrayField(TEXT("dependencies"), DependencyValues);
122
-
123
- TArray<UEdGraph*> Graphs;
124
- CollectBlueprintGraphs(Blueprint, Graphs);
125
- TSet<FString> NativeFunctionRefs;
126
- for (const UEdGraph* Graph : Graphs)
127
- {
128
- if (!Graph)
129
- {
130
- continue;
131
- }
132
-
133
- for (const UEdGraphNode* Node : Graph->Nodes)
134
- {
135
- if (const UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Node))
136
- {
137
- if (const UFunction* TargetFunction = CallNode->GetTargetFunction())
138
- {
139
- const UClass* OwnerClass = TargetFunction->GetOwnerClass();
140
- const FString SymbolKey = OwnerClass
141
- ? FString::Printf(TEXT("%s::%s"), *OwnerClass->GetName(), *TargetFunction->GetName())
142
- : TargetFunction->GetName();
143
- NativeFunctionRefs.Add(SymbolKey);
144
- }
145
- }
146
- }
147
- }
148
-
149
- TArray<TSharedPtr<FJsonValue>> NativeFunctionRefValues;
150
- for (const FString& Ref : NativeFunctionRefs)
151
- {
152
- NativeFunctionRefValues.Add(MakeShared<FJsonValueString>(Ref));
153
- }
154
- AssetObject->SetArrayField(TEXT("native_function_refs"), NativeFunctionRefValues);
155
-
156
- AssetValues.Add(MakeShared<FJsonValueObject>(AssetObject));
157
- }
158
-
159
- TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
160
- RootObject->SetNumberField(TEXT("version"), 1);
161
- RootObject->SetStringField(TEXT("generated_at"), FDateTime::UtcNow().ToIso8601());
162
- RootObject->SetStringField(TEXT("project_path"), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()));
163
- RootObject->SetArrayField(TEXT("assets"), AssetValues);
164
-
165
- return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
166
- }
167
-
168
- int32 UGitNexusBlueprintAnalyzerCommandlet::RunFindNativeBlueprintReferences(
169
- const FString& OutputJsonPath,
170
- const FString& CandidatesJsonPath,
171
- const FString& TargetSymbolKey,
172
- const FString& TargetClassName,
173
- const FString& TargetFunctionName
174
- )
175
- {
176
- TArray<TSharedPtr<FJsonValue>> ConfirmedReferences;
177
- TArray<FString> CandidateAssets = LoadCandidateAssets(CandidatesJsonPath);
178
-
179
- for (const FString& AssetPath : CandidateAssets)
180
- {
181
- UBlueprint* Blueprint = LoadBlueprintFromAssetPath(AssetPath);
182
- if (!Blueprint)
183
- {
184
- continue;
185
- }
186
-
187
- TArray<UEdGraph*> Graphs;
188
- CollectBlueprintGraphs(Blueprint, Graphs);
189
- for (const UEdGraph* Graph : Graphs)
190
- {
191
- if (!Graph)
192
- {
193
- continue;
194
- }
195
-
196
- for (const UEdGraphNode* Node : Graph->Nodes)
197
- {
198
- if (Node && IsTargetFunctionNode(Node, TargetSymbolKey, TargetClassName, TargetFunctionName))
199
- {
200
- ConfirmedReferences.Add(MakeShared<FJsonValueObject>(BuildReferenceJson(Blueprint, Graph, Node)));
201
- }
202
- }
203
- }
204
- }
205
-
206
- TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
207
- TSharedPtr<FJsonObject> TargetObject = MakeShared<FJsonObject>();
208
- TargetObject->SetStringField(TEXT("symbol_key"), TargetSymbolKey);
209
- TargetObject->SetStringField(TEXT("class_name"), TargetClassName);
210
- TargetObject->SetStringField(TEXT("symbol_name"), TargetFunctionName);
211
- RootObject->SetObjectField(TEXT("target_function"), TargetObject);
212
- RootObject->SetNumberField(TEXT("candidates_scanned"), CandidateAssets.Num());
213
- RootObject->SetArrayField(TEXT("confirmed_references"), ConfirmedReferences);
214
-
215
- return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
216
- }
217
-
218
- int32 UGitNexusBlueprintAnalyzerCommandlet::RunExpandBlueprintChain(
219
- const FString& OutputJsonPath,
220
- const FString& AssetPath,
221
- const FString& ChainAnchorId,
222
- const FString& Direction,
223
- int32 MaxDepth
224
- )
225
- {
226
- UBlueprint* Blueprint = LoadBlueprintFromAssetPath(AssetPath);
227
- if (!Blueprint)
228
- {
229
- return 1;
230
- }
231
-
232
- UEdGraphNode* StartNode = FindNodeByGuid(Blueprint, ChainAnchorId);
233
- if (!StartNode)
234
- {
235
- return 1;
236
- }
237
-
238
- TArray<TSharedPtr<FJsonValue>> NodeValues;
239
- TSet<FGuid> Visited;
240
- TArray<TPair<UEdGraphNode*, int32>> Frontier;
241
- Frontier.Add(TPair<UEdGraphNode*, int32>(StartNode, 0));
242
- Visited.Add(StartNode->NodeGuid);
243
-
244
- const bool bUpstream = Direction.Equals(TEXT("upstream"), ESearchCase::IgnoreCase);
245
-
246
- while (Frontier.Num() > 0)
247
- {
248
- const TPair<UEdGraphNode*, int32> Current = Frontier[0];
249
- Frontier.RemoveAt(0);
250
-
251
- NodeValues.Add(MakeShared<FJsonValueObject>(BuildChainNodeJson(Current.Key->GetGraph(), Current.Key, Current.Value)));
252
- if (Current.Value >= MaxDepth)
253
- {
254
- continue;
255
- }
256
-
257
- for (UEdGraphPin* Pin : Current.Key->Pins)
258
- {
259
- if (!Pin)
260
- {
261
- continue;
262
- }
263
-
264
- const bool bMatchesDirection = bUpstream ? Pin->Direction == EGPD_Input : Pin->Direction == EGPD_Output;
265
- if (!bMatchesDirection)
266
- {
267
- continue;
268
- }
269
-
270
- for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
271
- {
272
- if (!LinkedPin || !LinkedPin->GetOwningNode())
273
- {
274
- continue;
275
- }
276
-
277
- UEdGraphNode* NextNode = LinkedPin->GetOwningNode();
278
- if (Visited.Contains(NextNode->NodeGuid))
279
- {
280
- continue;
281
- }
282
-
283
- Visited.Add(NextNode->NodeGuid);
284
- Frontier.Add(TPair<UEdGraphNode*, int32>(NextNode, Current.Value + 1));
285
- }
286
- }
287
- }
288
-
289
- TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
290
- RootObject->SetArrayField(TEXT("nodes"), NodeValues);
291
- return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
292
- }
293
-
294
- bool UGitNexusBlueprintAnalyzerCommandlet::WriteJsonToFile(const FString& OutputJsonPath, const TSharedPtr<FJsonObject>& RootObject) const
295
- {
296
- FString JsonText;
297
- const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
298
- if (!FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer))
299
- {
300
- return false;
301
- }
302
-
303
- return FFileHelper::SaveStringToFile(JsonText, *OutputJsonPath);
304
- }
305
-
306
- TArray<FString> UGitNexusBlueprintAnalyzerCommandlet::LoadCandidateAssets(const FString& CandidatesJsonPath) const
307
- {
308
- TArray<FString> Result;
309
- if (CandidatesJsonPath.IsEmpty())
310
- {
311
- return Result;
312
- }
313
-
314
- FString RawJson;
315
- if (!FFileHelper::LoadFileToString(RawJson, *CandidatesJsonPath))
316
- {
317
- return Result;
318
- }
319
-
320
- TSharedPtr<FJsonObject> RootObject;
321
- const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(RawJson);
322
- if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
323
- {
324
- return Result;
325
- }
326
-
327
- const TArray<TSharedPtr<FJsonValue>>* CandidateValues = nullptr;
328
- if (!RootObject->TryGetArrayField(TEXT("candidate_assets"), CandidateValues) || !CandidateValues)
329
- {
330
- return Result;
331
- }
332
-
333
- for (const TSharedPtr<FJsonValue>& Value : *CandidateValues)
334
- {
335
- const TSharedPtr<FJsonObject>* CandidateObject = nullptr;
336
- if (Value.IsValid() && Value->TryGetObject(CandidateObject) && CandidateObject && CandidateObject->IsValid())
337
- {
338
- FString AssetPath;
339
- if ((*CandidateObject)->TryGetStringField(TEXT("asset_path"), AssetPath))
340
- {
341
- Result.Add(AssetPath);
342
- }
343
- }
344
- }
345
-
346
- return Result;
347
- }
348
-
349
- TArray<FAssetData> UGitNexusBlueprintAnalyzerCommandlet::GetAllBlueprintAssets() const
350
- {
351
- FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
352
- IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
353
- AssetRegistry.SearchAllAssets(true);
354
-
355
- FARFilter Filter;
356
- Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
357
- Filter.bRecursiveClasses = true;
358
-
359
- TArray<FAssetData> Assets;
360
- AssetRegistry.GetAssets(Filter, Assets);
361
- return Assets;
362
- }
363
-
364
- UBlueprint* UGitNexusBlueprintAnalyzerCommandlet::LoadBlueprintFromAssetPath(const FString& AssetPath) const
365
- {
366
- const FSoftObjectPath SoftObjectPath(AssetPath);
367
- return Cast<UBlueprint>(SoftObjectPath.TryLoad());
368
- }
369
-
370
- void UGitNexusBlueprintAnalyzerCommandlet::CollectBlueprintGraphs(UBlueprint* Blueprint, TArray<UEdGraph*>& OutGraphs) const
371
- {
372
- if (!Blueprint)
373
- {
374
- return;
375
- }
376
-
377
- Blueprint->GetAllGraphs(OutGraphs);
378
- }
379
-
380
- bool UGitNexusBlueprintAnalyzerCommandlet::IsTargetFunctionNode(
381
- const UEdGraphNode* Node,
382
- const FString& TargetSymbolKey,
383
- const FString& TargetClassName,
384
- const FString& TargetFunctionName
385
- ) const
386
- {
387
- if (const UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Node))
388
- {
389
- if (const UFunction* TargetFunction = CallNode->GetTargetFunction())
390
- {
391
- const UClass* OwnerClass = TargetFunction->GetOwnerClass();
392
- const FString SymbolKey = OwnerClass
393
- ? FString::Printf(TEXT("%s::%s"), *OwnerClass->GetName(), *TargetFunction->GetName())
394
- : TargetFunction->GetName();
395
- return SymbolKey == TargetSymbolKey
396
- || TargetFunction->GetName() == TargetFunctionName
397
- || (OwnerClass && OwnerClass->GetName() == TargetClassName && TargetFunction->GetName() == TargetFunctionName);
398
- }
399
- }
400
-
401
- if (const UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node))
402
- {
403
- return EventNode->GetFunctionName().ToString() == TargetFunctionName;
404
- }
405
-
406
- return false;
407
- }
408
-
409
- TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildReferenceJson(UBlueprint* Blueprint, const UEdGraph* Graph, const UEdGraphNode* Node) const
410
- {
411
- TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
412
- Object->SetStringField(TEXT("asset_path"), Blueprint ? Blueprint->GetPathName() : FString());
413
- Object->SetStringField(TEXT("graph_name"), Graph ? Graph->GetName() : FString());
414
- Object->SetStringField(TEXT("node_kind"), Node ? Node->GetClass()->GetName() : FString());
415
- Object->SetStringField(TEXT("node_title"), Node ? Node->GetNodeTitle(ENodeTitleType::ListView).ToString() : FString());
416
- Object->SetStringField(TEXT("blueprint_owner_function"), Graph ? Graph->GetName() : FString());
417
- Object->SetStringField(TEXT("chain_anchor_id"), Node ? Node->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens) : FString());
418
- Object->SetStringField(TEXT("source"), TEXT("editor_confirmed"));
419
- return Object;
420
- }
421
-
422
- TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildChainNodeJson(const UEdGraph* Graph, const UEdGraphNode* Node, int32 Depth) const
423
- {
424
- TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
425
- Object->SetStringField(TEXT("node_id"), Node ? Node->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens) : FString());
426
- Object->SetStringField(TEXT("graph_name"), Graph ? Graph->GetName() : FString());
427
- Object->SetStringField(TEXT("node_kind"), Node ? Node->GetClass()->GetName() : FString());
428
- Object->SetStringField(TEXT("node_title"), Node ? Node->GetNodeTitle(ENodeTitleType::ListView).ToString() : FString());
429
- Object->SetNumberField(TEXT("depth"), Depth);
430
- return Object;
431
- }
432
-
433
- UEdGraphNode* UGitNexusBlueprintAnalyzerCommandlet::FindNodeByGuid(UBlueprint* Blueprint, const FString& NodeGuid) const
434
- {
435
- if (!Blueprint)
436
- {
437
- return nullptr;
438
- }
439
-
440
- FGuid Guid;
441
- if (!FGuid::Parse(NodeGuid, Guid))
442
- {
443
- return nullptr;
444
- }
445
-
446
- TArray<UEdGraph*> Graphs;
447
- CollectBlueprintGraphs(Blueprint, Graphs);
448
- for (UEdGraph* Graph : Graphs)
449
- {
450
- if (!Graph)
451
- {
452
- continue;
453
- }
454
-
455
- for (UEdGraphNode* Node : Graph->Nodes)
456
- {
457
- if (Node && Node->NodeGuid == Guid)
458
- {
459
- return Node;
460
- }
461
- }
462
- }
463
-
464
- return nullptr;
465
- }
1
+ #include "GitNexusBlueprintAnalyzerCommandlet.h"
2
+
3
+ #include "AssetRegistry/AssetRegistryModule.h"
4
+ #include "AssetRegistry/IAssetRegistry.h"
5
+ #include "Blueprint/BlueprintSupport.h"
6
+ #include "Blueprint/UserWidget.h"
7
+ #include "Dom/JsonObject.h"
8
+ #include "EdGraph/EdGraph.h"
9
+ #include "EdGraph/EdGraphNode.h"
10
+ #include "EdGraph/EdGraphPin.h"
11
+ #include "Engine/Blueprint.h"
12
+ #include "K2Node_CallFunction.h"
13
+ #include "K2Node_Event.h"
14
+ #include "Kismet2/BlueprintEditorUtils.h"
15
+ #include "Misc/FileHelper.h"
16
+ #include "Misc/Guid.h"
17
+ #include "Misc/PackageName.h"
18
+ #include "Misc/Paths.h"
19
+ #include "Serialization/JsonReader.h"
20
+ #include "Serialization/JsonSerializer.h"
21
+ #include "UObject/SoftObjectPath.h"
22
+
23
+ UGitNexusBlueprintAnalyzerCommandlet::UGitNexusBlueprintAnalyzerCommandlet()
24
+ {
25
+ IsClient = false;
26
+ IsEditor = true;
27
+ LogToConsole = true;
28
+ ShowErrorCount = true;
29
+ }
30
+
31
+ int32 UGitNexusBlueprintAnalyzerCommandlet::Main(const FString& Params)
32
+ {
33
+ FString Operation;
34
+ FString OutputJsonPath;
35
+ FParse::Value(*Params, TEXT("Operation="), Operation);
36
+ FParse::Value(*Params, TEXT("OutputJson="), OutputJsonPath);
37
+
38
+ if (Operation.IsEmpty() || OutputJsonPath.IsEmpty())
39
+ {
40
+ UE_LOG(LogTemp, Error, TEXT("GitNexusBlueprintAnalyzer requires Operation= and OutputJson= parameters."));
41
+ return 1;
42
+ }
43
+
44
+ if (Operation.Equals(TEXT("SyncAssets"), ESearchCase::IgnoreCase))
45
+ {
46
+ return RunSyncAssets(OutputJsonPath);
47
+ }
48
+
49
+ if (Operation.Equals(TEXT("FindNativeBlueprintReferences"), ESearchCase::IgnoreCase))
50
+ {
51
+ FString CandidatesJsonPath;
52
+ FString TargetSymbolKey;
53
+ FString TargetClassName;
54
+ FString TargetFunctionName;
55
+ FParse::Value(*Params, TEXT("CandidatesJson="), CandidatesJsonPath);
56
+ FParse::Value(*Params, TEXT("TargetSymbolKey="), TargetSymbolKey);
57
+ FParse::Value(*Params, TEXT("TargetClass="), TargetClassName);
58
+ FParse::Value(*Params, TEXT("TargetFunction="), TargetFunctionName);
59
+ return RunFindNativeBlueprintReferences(OutputJsonPath, CandidatesJsonPath, TargetSymbolKey, TargetClassName, TargetFunctionName);
60
+ }
61
+
62
+ if (Operation.Equals(TEXT("ExpandBlueprintChain"), ESearchCase::IgnoreCase))
63
+ {
64
+ FString AssetPath;
65
+ FString ChainAnchorId;
66
+ FString Direction = TEXT("downstream");
67
+ int32 MaxDepth = 5;
68
+ FParse::Value(*Params, TEXT("AssetPath="), AssetPath);
69
+ FParse::Value(*Params, TEXT("ChainAnchorId="), ChainAnchorId);
70
+ FParse::Value(*Params, TEXT("Direction="), Direction);
71
+ FParse::Value(*Params, TEXT("MaxDepth="), MaxDepth);
72
+ return RunExpandBlueprintChain(OutputJsonPath, AssetPath, ChainAnchorId, Direction, MaxDepth);
73
+ }
74
+
75
+ UE_LOG(LogTemp, Error, TEXT("Unsupported GitNexusBlueprintAnalyzer operation: %s"), *Operation);
76
+ return 1;
77
+ }
78
+
79
+ int32 UGitNexusBlueprintAnalyzerCommandlet::RunSyncAssets(const FString& OutputJsonPath)
80
+ {
81
+ TArray<TSharedPtr<FJsonValue>> AssetValues;
82
+
83
+ for (const FAssetData& AssetData : GetAllBlueprintAssets())
84
+ {
85
+ UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
86
+ if (!Blueprint)
87
+ {
88
+ continue;
89
+ }
90
+
91
+ TSharedPtr<FJsonObject> AssetObject = MakeShared<FJsonObject>();
92
+ AssetObject->SetStringField(TEXT("asset_path"), AssetData.GetSoftObjectPath().ToString());
93
+
94
+ if (Blueprint->GeneratedClass)
95
+ {
96
+ AssetObject->SetStringField(TEXT("generated_class"), Blueprint->GeneratedClass->GetPathName());
97
+ }
98
+
99
+ if (Blueprint->ParentClass)
100
+ {
101
+ AssetObject->SetStringField(TEXT("parent_class"), Blueprint->ParentClass->GetPathName());
102
+ }
103
+
104
+ TArray<TSharedPtr<FJsonValue>> NativeParents;
105
+ for (UClass* Class = Blueprint->ParentClass; Class; Class = Class->GetSuperClass())
106
+ {
107
+ if (Class->ClassGeneratedBy == nullptr)
108
+ {
109
+ NativeParents.Add(MakeShared<FJsonValueString>(Class->GetName()));
110
+ }
111
+ }
112
+ AssetObject->SetArrayField(TEXT("native_parents"), NativeParents);
113
+
114
+ TArray<FName> Dependencies;
115
+ IAssetRegistry::GetChecked().GetDependencies(AssetData.PackageName, Dependencies, UE::AssetRegistry::EDependencyCategory::Package);
116
+ TArray<TSharedPtr<FJsonValue>> DependencyValues;
117
+ for (const FName& Dependency : Dependencies)
118
+ {
119
+ DependencyValues.Add(MakeShared<FJsonValueString>(Dependency.ToString()));
120
+ }
121
+ AssetObject->SetArrayField(TEXT("dependencies"), DependencyValues);
122
+
123
+ TArray<UEdGraph*> Graphs;
124
+ CollectBlueprintGraphs(Blueprint, Graphs);
125
+ TSet<FString> NativeFunctionRefs;
126
+ for (const UEdGraph* Graph : Graphs)
127
+ {
128
+ if (!Graph)
129
+ {
130
+ continue;
131
+ }
132
+
133
+ for (const UEdGraphNode* Node : Graph->Nodes)
134
+ {
135
+ if (const UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Node))
136
+ {
137
+ if (const UFunction* TargetFunction = CallNode->GetTargetFunction())
138
+ {
139
+ const UClass* OwnerClass = TargetFunction->GetOwnerClass();
140
+ const FString SymbolKey = OwnerClass
141
+ ? FString::Printf(TEXT("%s::%s"), *OwnerClass->GetName(), *TargetFunction->GetName())
142
+ : TargetFunction->GetName();
143
+ NativeFunctionRefs.Add(SymbolKey);
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ TArray<TSharedPtr<FJsonValue>> NativeFunctionRefValues;
150
+ for (const FString& Ref : NativeFunctionRefs)
151
+ {
152
+ NativeFunctionRefValues.Add(MakeShared<FJsonValueString>(Ref));
153
+ }
154
+ AssetObject->SetArrayField(TEXT("native_function_refs"), NativeFunctionRefValues);
155
+
156
+ AssetValues.Add(MakeShared<FJsonValueObject>(AssetObject));
157
+ }
158
+
159
+ TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
160
+ RootObject->SetNumberField(TEXT("version"), 1);
161
+ RootObject->SetStringField(TEXT("generated_at"), FDateTime::UtcNow().ToIso8601());
162
+ RootObject->SetStringField(TEXT("project_path"), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()));
163
+ RootObject->SetArrayField(TEXT("assets"), AssetValues);
164
+
165
+ return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
166
+ }
167
+
168
+ int32 UGitNexusBlueprintAnalyzerCommandlet::RunFindNativeBlueprintReferences(
169
+ const FString& OutputJsonPath,
170
+ const FString& CandidatesJsonPath,
171
+ const FString& TargetSymbolKey,
172
+ const FString& TargetClassName,
173
+ const FString& TargetFunctionName
174
+ )
175
+ {
176
+ TArray<TSharedPtr<FJsonValue>> ConfirmedReferences;
177
+ TArray<FString> CandidateAssets = LoadCandidateAssets(CandidatesJsonPath);
178
+
179
+ for (const FString& AssetPath : CandidateAssets)
180
+ {
181
+ UBlueprint* Blueprint = LoadBlueprintFromAssetPath(AssetPath);
182
+ if (!Blueprint)
183
+ {
184
+ continue;
185
+ }
186
+
187
+ TArray<UEdGraph*> Graphs;
188
+ CollectBlueprintGraphs(Blueprint, Graphs);
189
+ for (const UEdGraph* Graph : Graphs)
190
+ {
191
+ if (!Graph)
192
+ {
193
+ continue;
194
+ }
195
+
196
+ for (const UEdGraphNode* Node : Graph->Nodes)
197
+ {
198
+ if (Node && IsTargetFunctionNode(Node, TargetSymbolKey, TargetClassName, TargetFunctionName))
199
+ {
200
+ ConfirmedReferences.Add(MakeShared<FJsonValueObject>(BuildReferenceJson(Blueprint, Graph, Node)));
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
207
+ TSharedPtr<FJsonObject> TargetObject = MakeShared<FJsonObject>();
208
+ TargetObject->SetStringField(TEXT("symbol_key"), TargetSymbolKey);
209
+ TargetObject->SetStringField(TEXT("class_name"), TargetClassName);
210
+ TargetObject->SetStringField(TEXT("symbol_name"), TargetFunctionName);
211
+ RootObject->SetObjectField(TEXT("target_function"), TargetObject);
212
+ RootObject->SetNumberField(TEXT("candidates_scanned"), CandidateAssets.Num());
213
+ RootObject->SetArrayField(TEXT("confirmed_references"), ConfirmedReferences);
214
+
215
+ return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
216
+ }
217
+
218
+ int32 UGitNexusBlueprintAnalyzerCommandlet::RunExpandBlueprintChain(
219
+ const FString& OutputJsonPath,
220
+ const FString& AssetPath,
221
+ const FString& ChainAnchorId,
222
+ const FString& Direction,
223
+ int32 MaxDepth
224
+ )
225
+ {
226
+ UBlueprint* Blueprint = LoadBlueprintFromAssetPath(AssetPath);
227
+ if (!Blueprint)
228
+ {
229
+ return 1;
230
+ }
231
+
232
+ UEdGraphNode* StartNode = FindNodeByGuid(Blueprint, ChainAnchorId);
233
+ if (!StartNode)
234
+ {
235
+ return 1;
236
+ }
237
+
238
+ TArray<TSharedPtr<FJsonValue>> NodeValues;
239
+ TSet<FGuid> Visited;
240
+ TArray<TPair<UEdGraphNode*, int32>> Frontier;
241
+ Frontier.Add(TPair<UEdGraphNode*, int32>(StartNode, 0));
242
+ Visited.Add(StartNode->NodeGuid);
243
+
244
+ const bool bUpstream = Direction.Equals(TEXT("upstream"), ESearchCase::IgnoreCase);
245
+
246
+ while (Frontier.Num() > 0)
247
+ {
248
+ const TPair<UEdGraphNode*, int32> Current = Frontier[0];
249
+ Frontier.RemoveAt(0);
250
+
251
+ NodeValues.Add(MakeShared<FJsonValueObject>(BuildChainNodeJson(Current.Key->GetGraph(), Current.Key, Current.Value)));
252
+ if (Current.Value >= MaxDepth)
253
+ {
254
+ continue;
255
+ }
256
+
257
+ for (UEdGraphPin* Pin : Current.Key->Pins)
258
+ {
259
+ if (!Pin)
260
+ {
261
+ continue;
262
+ }
263
+
264
+ const bool bMatchesDirection = bUpstream ? Pin->Direction == EGPD_Input : Pin->Direction == EGPD_Output;
265
+ if (!bMatchesDirection)
266
+ {
267
+ continue;
268
+ }
269
+
270
+ for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
271
+ {
272
+ if (!LinkedPin || !LinkedPin->GetOwningNode())
273
+ {
274
+ continue;
275
+ }
276
+
277
+ UEdGraphNode* NextNode = LinkedPin->GetOwningNode();
278
+ if (Visited.Contains(NextNode->NodeGuid))
279
+ {
280
+ continue;
281
+ }
282
+
283
+ Visited.Add(NextNode->NodeGuid);
284
+ Frontier.Add(TPair<UEdGraphNode*, int32>(NextNode, Current.Value + 1));
285
+ }
286
+ }
287
+ }
288
+
289
+ TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
290
+ RootObject->SetArrayField(TEXT("nodes"), NodeValues);
291
+ return WriteJsonToFile(OutputJsonPath, RootObject) ? 0 : 1;
292
+ }
293
+
294
+ bool UGitNexusBlueprintAnalyzerCommandlet::WriteJsonToFile(const FString& OutputJsonPath, const TSharedPtr<FJsonObject>& RootObject) const
295
+ {
296
+ FString JsonText;
297
+ const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
298
+ if (!FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer))
299
+ {
300
+ return false;
301
+ }
302
+
303
+ return FFileHelper::SaveStringToFile(JsonText, *OutputJsonPath);
304
+ }
305
+
306
+ TArray<FString> UGitNexusBlueprintAnalyzerCommandlet::LoadCandidateAssets(const FString& CandidatesJsonPath) const
307
+ {
308
+ TArray<FString> Result;
309
+ if (CandidatesJsonPath.IsEmpty())
310
+ {
311
+ return Result;
312
+ }
313
+
314
+ FString RawJson;
315
+ if (!FFileHelper::LoadFileToString(RawJson, *CandidatesJsonPath))
316
+ {
317
+ return Result;
318
+ }
319
+
320
+ TSharedPtr<FJsonObject> RootObject;
321
+ const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(RawJson);
322
+ if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
323
+ {
324
+ return Result;
325
+ }
326
+
327
+ const TArray<TSharedPtr<FJsonValue>>* CandidateValues = nullptr;
328
+ if (!RootObject->TryGetArrayField(TEXT("candidate_assets"), CandidateValues) || !CandidateValues)
329
+ {
330
+ return Result;
331
+ }
332
+
333
+ for (const TSharedPtr<FJsonValue>& Value : *CandidateValues)
334
+ {
335
+ const TSharedPtr<FJsonObject>* CandidateObject = nullptr;
336
+ if (Value.IsValid() && Value->TryGetObject(CandidateObject) && CandidateObject && CandidateObject->IsValid())
337
+ {
338
+ FString AssetPath;
339
+ if ((*CandidateObject)->TryGetStringField(TEXT("asset_path"), AssetPath))
340
+ {
341
+ Result.Add(AssetPath);
342
+ }
343
+ }
344
+ }
345
+
346
+ return Result;
347
+ }
348
+
349
+ TArray<FAssetData> UGitNexusBlueprintAnalyzerCommandlet::GetAllBlueprintAssets() const
350
+ {
351
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
352
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
353
+ AssetRegistry.SearchAllAssets(true);
354
+
355
+ FARFilter Filter;
356
+ Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
357
+ Filter.bRecursiveClasses = true;
358
+
359
+ TArray<FAssetData> Assets;
360
+ AssetRegistry.GetAssets(Filter, Assets);
361
+ return Assets;
362
+ }
363
+
364
+ UBlueprint* UGitNexusBlueprintAnalyzerCommandlet::LoadBlueprintFromAssetPath(const FString& AssetPath) const
365
+ {
366
+ const FSoftObjectPath SoftObjectPath(AssetPath);
367
+ return Cast<UBlueprint>(SoftObjectPath.TryLoad());
368
+ }
369
+
370
+ void UGitNexusBlueprintAnalyzerCommandlet::CollectBlueprintGraphs(UBlueprint* Blueprint, TArray<UEdGraph*>& OutGraphs) const
371
+ {
372
+ if (!Blueprint)
373
+ {
374
+ return;
375
+ }
376
+
377
+ Blueprint->GetAllGraphs(OutGraphs);
378
+ }
379
+
380
+ bool UGitNexusBlueprintAnalyzerCommandlet::IsTargetFunctionNode(
381
+ const UEdGraphNode* Node,
382
+ const FString& TargetSymbolKey,
383
+ const FString& TargetClassName,
384
+ const FString& TargetFunctionName
385
+ ) const
386
+ {
387
+ if (const UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Node))
388
+ {
389
+ if (const UFunction* TargetFunction = CallNode->GetTargetFunction())
390
+ {
391
+ const UClass* OwnerClass = TargetFunction->GetOwnerClass();
392
+ const FString SymbolKey = OwnerClass
393
+ ? FString::Printf(TEXT("%s::%s"), *OwnerClass->GetName(), *TargetFunction->GetName())
394
+ : TargetFunction->GetName();
395
+ return SymbolKey == TargetSymbolKey
396
+ || TargetFunction->GetName() == TargetFunctionName
397
+ || (OwnerClass && OwnerClass->GetName() == TargetClassName && TargetFunction->GetName() == TargetFunctionName);
398
+ }
399
+ }
400
+
401
+ if (const UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node))
402
+ {
403
+ return EventNode->GetFunctionName().ToString() == TargetFunctionName;
404
+ }
405
+
406
+ return false;
407
+ }
408
+
409
+ TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildReferenceJson(UBlueprint* Blueprint, const UEdGraph* Graph, const UEdGraphNode* Node) const
410
+ {
411
+ TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
412
+ Object->SetStringField(TEXT("asset_path"), Blueprint ? Blueprint->GetPathName() : FString());
413
+ Object->SetStringField(TEXT("graph_name"), Graph ? Graph->GetName() : FString());
414
+ Object->SetStringField(TEXT("node_kind"), Node ? Node->GetClass()->GetName() : FString());
415
+ Object->SetStringField(TEXT("node_title"), Node ? Node->GetNodeTitle(ENodeTitleType::ListView).ToString() : FString());
416
+ Object->SetStringField(TEXT("blueprint_owner_function"), Graph ? Graph->GetName() : FString());
417
+ Object->SetStringField(TEXT("chain_anchor_id"), Node ? Node->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens) : FString());
418
+ Object->SetStringField(TEXT("source"), TEXT("editor_confirmed"));
419
+ return Object;
420
+ }
421
+
422
+ TSharedPtr<FJsonObject> UGitNexusBlueprintAnalyzerCommandlet::BuildChainNodeJson(const UEdGraph* Graph, const UEdGraphNode* Node, int32 Depth) const
423
+ {
424
+ TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
425
+ Object->SetStringField(TEXT("node_id"), Node ? Node->NodeGuid.ToString(EGuidFormats::DigitsWithHyphens) : FString());
426
+ Object->SetStringField(TEXT("graph_name"), Graph ? Graph->GetName() : FString());
427
+ Object->SetStringField(TEXT("node_kind"), Node ? Node->GetClass()->GetName() : FString());
428
+ Object->SetStringField(TEXT("node_title"), Node ? Node->GetNodeTitle(ENodeTitleType::ListView).ToString() : FString());
429
+ Object->SetNumberField(TEXT("depth"), Depth);
430
+ return Object;
431
+ }
432
+
433
+ UEdGraphNode* UGitNexusBlueprintAnalyzerCommandlet::FindNodeByGuid(UBlueprint* Blueprint, const FString& NodeGuid) const
434
+ {
435
+ if (!Blueprint)
436
+ {
437
+ return nullptr;
438
+ }
439
+
440
+ FGuid Guid;
441
+ if (!FGuid::Parse(NodeGuid, Guid))
442
+ {
443
+ return nullptr;
444
+ }
445
+
446
+ TArray<UEdGraph*> Graphs;
447
+ CollectBlueprintGraphs(Blueprint, Graphs);
448
+ for (UEdGraph* Graph : Graphs)
449
+ {
450
+ if (!Graph)
451
+ {
452
+ continue;
453
+ }
454
+
455
+ for (UEdGraphNode* Node : Graph->Nodes)
456
+ {
457
+ if (Node && Node->NodeGuid == Guid)
458
+ {
459
+ return Node;
460
+ }
461
+ }
462
+ }
463
+
464
+ return nullptr;
465
+ }