@duytransipher/gitnexus 1.2.0 → 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/setup.js +15 -0
- package/dist/cli/tool.js +30 -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 +7 -1
- package/dist/unreal/bridge.js +28 -1
- package/package.json +1 -1
- package/scripts/setup-unreal-gitnexus.ps1 +14 -0
- package/vendor/GitNexusUnreal/Source/GitNexusUnreal/Private/GitNexusBlueprintAnalyzerCommandlet.cpp +465 -465
package/dist/cli/setup.js
CHANGED
|
@@ -366,6 +366,21 @@ function resolveEditorCmd(explicitPath, engineAssociation) {
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
catch { /* registry key not found */ }
|
|
369
|
+
// Try LauncherInstalled.dat (Epic Games Launcher writes this for all engine installs)
|
|
370
|
+
try {
|
|
371
|
+
const launcherDat = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'EpicGames', 'UnrealEngineLauncher', 'LauncherInstalled.dat');
|
|
372
|
+
const launcher = JSON.parse(fsSync.readFileSync(launcherDat, 'utf-8'));
|
|
373
|
+
if (Array.isArray(launcher.InstallationList)) {
|
|
374
|
+
for (const entry of launcher.InstallationList) {
|
|
375
|
+
if (entry.AppName === `UE_${engineAssociation}` || entry.AppName === engineAssociation) {
|
|
376
|
+
const found = tryCandidates(entry.InstallLocation);
|
|
377
|
+
if (found)
|
|
378
|
+
return found;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch { /* LauncherInstalled.dat not found or unparseable */ }
|
|
369
384
|
// Try standard install path
|
|
370
385
|
const found = tryCandidates(path.join('C:\\Program Files\\Epic Games', `UE_${engineAssociation}`));
|
|
371
386
|
if (found)
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/unreal/bridge.js
CHANGED
|
@@ -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
|
|
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
|
@@ -108,6 +108,20 @@ function Resolve-UnrealEditorCmd {
|
|
|
108
108
|
} catch {
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
# Try LauncherInstalled.dat (Epic Games Launcher writes this for all engine installs)
|
|
112
|
+
try {
|
|
113
|
+
$launcherDat = Join-Path $env:LOCALAPPDATA 'EpicGames\UnrealEngineLauncher\LauncherInstalled.dat'
|
|
114
|
+
if (Test-Path -LiteralPath $launcherDat -PathType Leaf) {
|
|
115
|
+
$launcher = Get-Content -LiteralPath $launcherDat -Raw | ConvertFrom-Json
|
|
116
|
+
foreach ($entry in $launcher.InstallationList) {
|
|
117
|
+
if ($entry.AppName -eq "UE_$EngineAssociation" -or $entry.AppName -eq $EngineAssociation) {
|
|
118
|
+
Add-EditorCmdCandidates -Candidates $candidates -RootOrExe $entry.InstallLocation
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
Add-EditorCmdCandidates -Candidates $candidates -RootOrExe "C:\Program Files\Epic Games\UE_$EngineAssociation"
|
|
112
126
|
}
|
|
113
127
|
|
package/vendor/GitNexusUnreal/Source/GitNexusUnreal/Private/GitNexusBlueprintAnalyzerCommandlet.cpp
CHANGED
|
@@ -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
|
+
}
|