blueprint-extractor-mcp 6.3.1 → 7.0.1
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/compactor.js +54 -0
- package/dist/helpers/blueprint-dsl-parser.d.ts +52 -0
- package/dist/helpers/blueprint-dsl-parser.js +513 -0
- package/dist/helpers/material-dsl-parser.d.ts +42 -0
- package/dist/helpers/material-dsl-parser.js +416 -0
- package/dist/helpers/next-step-hints.js +68 -0
- package/dist/helpers/property-shorthand.d.ts +1 -0
- package/dist/helpers/property-shorthand.js +22 -0
- package/dist/helpers/slot-presets.d.ts +3 -0
- package/dist/helpers/slot-presets.js +26 -0
- package/dist/helpers/tool-results.d.ts +1 -1
- package/dist/helpers/tool-results.js +2 -55
- package/dist/helpers/widget-class-aliases.d.ts +2 -0
- package/dist/helpers/widget-class-aliases.js +15 -0
- package/dist/helpers/widget-diff-parser.d.ts +22 -0
- package/dist/helpers/widget-diff-parser.js +194 -0
- package/dist/helpers/widget-dsl-parser.d.ts +18 -0
- package/dist/helpers/widget-dsl-parser.js +474 -0
- package/dist/helpers/widget-recipe-formatter.d.ts +5 -0
- package/dist/helpers/widget-recipe-formatter.js +167 -0
- package/dist/helpers/widget-recipe-parser.d.ts +12 -0
- package/dist/helpers/widget-recipe-parser.js +212 -0
- package/dist/helpers/widget-utils.d.ts +15 -0
- package/dist/helpers/widget-utils.js +69 -0
- package/dist/register-server-tools.js +12 -0
- package/dist/schemas/tool-inputs.d.ts +20 -20
- package/dist/schemas/tool-inputs.js +13 -13
- package/dist/schemas/tool-results.d.ts +84 -84
- package/dist/server-config.js +7 -1
- package/dist/tool-surface-manager.js +4 -1
- package/dist/tools/analysis-tools.js +5 -5
- package/dist/tools/animation-authoring.js +18 -18
- package/dist/tools/automation-runs.js +9 -9
- package/dist/tools/blueprint-authoring.js +46 -17
- package/dist/tools/commonui-button-style.js +11 -11
- package/dist/tools/composite-tools.js +3 -3
- package/dist/tools/composite-workflows.d.ts +10 -0
- package/dist/tools/composite-workflows.js +426 -0
- package/dist/tools/data-and-input.js +24 -24
- package/dist/tools/extraction.js +41 -30
- package/dist/tools/import-jobs.js +5 -5
- package/dist/tools/material-authoring.js +53 -20
- package/dist/tools/material-instance.js +14 -14
- package/dist/tools/project-control.js +39 -39
- package/dist/tools/project-intelligence.js +6 -6
- package/dist/tools/recipe-tools.d.ts +10 -0
- package/dist/tools/recipe-tools.js +205 -0
- package/dist/tools/schema-and-ai-authoring.js +40 -40
- package/dist/tools/tables-and-curves.js +27 -27
- package/dist/tools/utility-tools.js +2 -2
- package/dist/tools/widget-animation-authoring.js +9 -9
- package/dist/tools/widget-extraction.js +31 -7
- package/dist/tools/widget-structure.js +266 -106
- package/dist/tools/widget-verification.js +19 -19
- package/dist/tools/window-ui.js +10 -10
- package/package.json +2 -2
package/dist/compactor.js
CHANGED
|
@@ -21,6 +21,47 @@ function deleteEmptyArrayField(object, key) {
|
|
|
21
21
|
function shouldStripField(key, patterns) {
|
|
22
22
|
return patterns.some((pattern) => pattern.test(key));
|
|
23
23
|
}
|
|
24
|
+
function stripNullsInPlace(value) {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
for (const entry of value)
|
|
27
|
+
stripNullsInPlace(entry);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!isPlainObject(value))
|
|
31
|
+
return;
|
|
32
|
+
for (const key of Object.keys(value)) {
|
|
33
|
+
if (value[key] === null) {
|
|
34
|
+
delete value[key];
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
stripNullsInPlace(value[key]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const WIDGET_DEFAULT_STRIP_PATTERNS = [
|
|
41
|
+
['bIsVariable', false],
|
|
42
|
+
['Visibility', 'Visible'],
|
|
43
|
+
['Visibility', 'SelfHitTestInvisible'],
|
|
44
|
+
['RenderOpacity', 1.0],
|
|
45
|
+
['RenderOpacity', 1],
|
|
46
|
+
['IsEnabled', true],
|
|
47
|
+
];
|
|
48
|
+
function stripWidgetDefaultsInPlace(value) {
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
for (const entry of value)
|
|
51
|
+
stripWidgetDefaultsInPlace(entry);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!isPlainObject(value))
|
|
55
|
+
return;
|
|
56
|
+
for (const [field, defaultValue] of WIDGET_DEFAULT_STRIP_PATTERNS) {
|
|
57
|
+
if (field in value && value[field] === defaultValue) {
|
|
58
|
+
delete value[field];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const key of Object.keys(value)) {
|
|
62
|
+
stripWidgetDefaultsInPlace(value[key]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
24
65
|
function stripFieldsInPlace(value, fieldPatterns) {
|
|
25
66
|
if (Array.isArray(value)) {
|
|
26
67
|
for (const entry of value) {
|
|
@@ -46,6 +87,7 @@ export function stripFields(data, fieldPatterns) {
|
|
|
46
87
|
return data;
|
|
47
88
|
}
|
|
48
89
|
export function compactGenericExtraction(data) {
|
|
90
|
+
stripNullsInPlace(data);
|
|
49
91
|
return stripFields(data, GUID_AND_POSITION_PATTERNS);
|
|
50
92
|
}
|
|
51
93
|
/**
|
|
@@ -58,6 +100,7 @@ export function compactGenericExtraction(data) {
|
|
|
58
100
|
export function compactBlueprint(data) {
|
|
59
101
|
if (!isPlainObject(data))
|
|
60
102
|
return data;
|
|
103
|
+
stripNullsInPlace(data);
|
|
61
104
|
const root = data;
|
|
62
105
|
const bp = root.blueprint;
|
|
63
106
|
if (!bp)
|
|
@@ -79,6 +122,14 @@ export function compactBlueprint(data) {
|
|
|
79
122
|
export function compactWidgetBlueprint(data) {
|
|
80
123
|
if (!isPlainObject(data))
|
|
81
124
|
return data;
|
|
125
|
+
// Strip nulls and widget defaults inside sub-trees, not at data root
|
|
126
|
+
// (top-level nulls like rootWidget: null carry semantic meaning)
|
|
127
|
+
for (const key of Object.keys(data)) {
|
|
128
|
+
if (data[key] !== null) {
|
|
129
|
+
stripNullsInPlace(data[key]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
stripWidgetDefaultsInPlace(data);
|
|
82
133
|
const rootWidget = data.rootWidget;
|
|
83
134
|
if (isPlainObject(rootWidget)) {
|
|
84
135
|
compactWidgetNode(rootWidget);
|
|
@@ -104,6 +155,7 @@ export function compactWidgetBlueprint(data) {
|
|
|
104
155
|
export function compactBehaviorTree(data) {
|
|
105
156
|
if (!isPlainObject(data))
|
|
106
157
|
return data;
|
|
158
|
+
stripNullsInPlace(data);
|
|
107
159
|
delete data.schemaVersion;
|
|
108
160
|
const behaviorTree = data.behaviorTree;
|
|
109
161
|
if (!isPlainObject(behaviorTree)) {
|
|
@@ -118,6 +170,7 @@ export function compactBehaviorTree(data) {
|
|
|
118
170
|
export function compactStateTree(data) {
|
|
119
171
|
if (!isPlainObject(data))
|
|
120
172
|
return data;
|
|
173
|
+
stripNullsInPlace(data);
|
|
121
174
|
delete data.schemaVersion;
|
|
122
175
|
const stateTree = data.stateTree;
|
|
123
176
|
if (!isPlainObject(stateTree)) {
|
|
@@ -140,6 +193,7 @@ export function compactMaterial(data) {
|
|
|
140
193
|
if (!isPlainObject(data)) {
|
|
141
194
|
return data;
|
|
142
195
|
}
|
|
196
|
+
stripNullsInPlace(data);
|
|
143
197
|
const graph = findMaterialGraphRoot(data);
|
|
144
198
|
if (!graph) {
|
|
145
199
|
return compactGenericExtraction(data);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface BlueprintDslNode {
|
|
2
|
+
/** Node type: function call, event, variable get/set, branch, cast, macro, literal. */
|
|
3
|
+
type: 'event' | 'call' | 'variable_get' | 'variable_set' | 'branch' | 'cast' | 'macro' | 'literal';
|
|
4
|
+
/** The function/event/variable name. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Target object (e.g., "PC" in "PC.GetPawn"). */
|
|
7
|
+
target?: string;
|
|
8
|
+
/** Cast target class. */
|
|
9
|
+
castTo?: string;
|
|
10
|
+
/** Alias for the output (e.g., "as PC"). */
|
|
11
|
+
alias?: string;
|
|
12
|
+
/** Arguments (for function calls). */
|
|
13
|
+
args?: Record<string, unknown>;
|
|
14
|
+
/** Connections to next nodes (exec pins). */
|
|
15
|
+
then?: BlueprintDslNode[];
|
|
16
|
+
/** Named output branches (for Branch, Switch, etc.). */
|
|
17
|
+
branches?: Record<string, BlueprintDslNode[]>;
|
|
18
|
+
/** Comparison condition (for `condition ? Branch`). */
|
|
19
|
+
condition?: {
|
|
20
|
+
left: string;
|
|
21
|
+
operator: string;
|
|
22
|
+
right: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface BlueprintDslGraph {
|
|
26
|
+
graphName: string;
|
|
27
|
+
nodes: BlueprintDslNode[];
|
|
28
|
+
}
|
|
29
|
+
export interface BlueprintDslResult {
|
|
30
|
+
graphs: BlueprintDslGraph[];
|
|
31
|
+
warnings: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface PayloadNode {
|
|
34
|
+
nodeClass: string;
|
|
35
|
+
tempId: string;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
export interface PayloadConnection {
|
|
39
|
+
fromNode: string;
|
|
40
|
+
fromPin: string;
|
|
41
|
+
toNode: string;
|
|
42
|
+
toPin: string;
|
|
43
|
+
}
|
|
44
|
+
export interface PayloadGraph {
|
|
45
|
+
graphName: string;
|
|
46
|
+
nodes: PayloadNode[];
|
|
47
|
+
connections: PayloadConnection[];
|
|
48
|
+
}
|
|
49
|
+
export declare function parseBlueprintDsl(dsl: string): BlueprintDslResult;
|
|
50
|
+
export declare function blueprintDslToPayload(graphs: BlueprintDslGraph[]): {
|
|
51
|
+
functionGraphs: PayloadGraph[];
|
|
52
|
+
};
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Blueprint Graph DSL Parser
|
|
3
|
+
//
|
|
4
|
+
// Converts a pseudocode-style DSL into an intermediate representation that
|
|
5
|
+
// maps to the `upsert_function_graphs` payload for `modify_blueprint_graphs`.
|
|
6
|
+
//
|
|
7
|
+
// The DSL is a high-level intent description — the UE subsystem handles
|
|
8
|
+
// actual node creation, UUID assignment, and pin resolution.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Main entry point — Parser
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export function parseBlueprintDsl(dsl) {
|
|
14
|
+
const warnings = [];
|
|
15
|
+
const rawLines = dsl.split(/\r?\n/);
|
|
16
|
+
const graphs = [];
|
|
17
|
+
let currentGraph = null;
|
|
18
|
+
let branchContext = null;
|
|
19
|
+
for (let lineIdx = 0; lineIdx < rawLines.length; lineIdx++) {
|
|
20
|
+
const lineNum = lineIdx + 1;
|
|
21
|
+
const raw = rawLines[lineIdx];
|
|
22
|
+
const trimmed = raw.trimEnd();
|
|
23
|
+
// Skip blank lines and comments
|
|
24
|
+
if (trimmed.length === 0)
|
|
25
|
+
continue;
|
|
26
|
+
const stripped = trimmed.trimStart();
|
|
27
|
+
if (stripped.startsWith('#') || stripped.startsWith('//'))
|
|
28
|
+
continue;
|
|
29
|
+
// Measure indentation
|
|
30
|
+
const normalized = trimmed.replace(/\t/g, ' ');
|
|
31
|
+
const strippedNorm = normalized.trimStart();
|
|
32
|
+
const leadingSpaces = normalized.length - strippedNorm.length;
|
|
33
|
+
// Graph header: line ending with `:` at indent level 0
|
|
34
|
+
if (leadingSpaces === 0 && stripped.endsWith(':') && !stripped.includes('->')) {
|
|
35
|
+
const graphName = stripped.slice(0, -1).trim();
|
|
36
|
+
if (graphName.length === 0) {
|
|
37
|
+
warnings.push(`Line ${lineNum}: empty graph name`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
currentGraph = { graphName, nodes: [] };
|
|
41
|
+
graphs.push(currentGraph);
|
|
42
|
+
branchContext = null;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// If no graph header yet, create a default EventGraph
|
|
46
|
+
if (!currentGraph) {
|
|
47
|
+
currentGraph = { graphName: 'EventGraph', nodes: [] };
|
|
48
|
+
graphs.push(currentGraph);
|
|
49
|
+
}
|
|
50
|
+
// Check for branch label lines: `True ->`, `False ->`, `Default ->`
|
|
51
|
+
const branchLabelMatch = stripped.match(/^(\w+)\s*->\s*$/);
|
|
52
|
+
if (branchLabelMatch && branchContext) {
|
|
53
|
+
branchContext.currentLabel = branchLabelMatch[1];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// Check for inline branch labels: `True -> NodeChain`
|
|
57
|
+
const inlineBranchMatch = stripped.match(/^(\w+)\s*->\s+(.+)$/);
|
|
58
|
+
if (inlineBranchMatch && branchContext && leadingSpaces > branchContext.indent) {
|
|
59
|
+
const label = inlineBranchMatch[1];
|
|
60
|
+
const chainStr = inlineBranchMatch[2];
|
|
61
|
+
const chain = parseChain(chainStr, lineNum, warnings);
|
|
62
|
+
if (chain.length > 0) {
|
|
63
|
+
const branchNode = branchContext.node;
|
|
64
|
+
if (!branchNode.branches)
|
|
65
|
+
branchNode.branches = {};
|
|
66
|
+
if (!branchNode.branches[label])
|
|
67
|
+
branchNode.branches[label] = [];
|
|
68
|
+
branchNode.branches[label].push(...chain);
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// Check for branch continuation (indented lines after a branch label)
|
|
73
|
+
if (branchContext && branchContext.currentLabel && leadingSpaces > branchContext.indent) {
|
|
74
|
+
const chain = parseChain(stripped, lineNum, warnings);
|
|
75
|
+
if (chain.length > 0) {
|
|
76
|
+
const branchNode = branchContext.node;
|
|
77
|
+
if (!branchNode.branches)
|
|
78
|
+
branchNode.branches = {};
|
|
79
|
+
const label = branchContext.currentLabel;
|
|
80
|
+
if (!branchNode.branches[label])
|
|
81
|
+
branchNode.branches[label] = [];
|
|
82
|
+
branchNode.branches[label].push(...chain);
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Reset branch context if we're back at or above the branch indent level
|
|
87
|
+
if (branchContext && leadingSpaces <= branchContext.indent) {
|
|
88
|
+
branchContext = null;
|
|
89
|
+
}
|
|
90
|
+
// Parse the line as a chain of nodes
|
|
91
|
+
const chain = parseChain(stripped, lineNum, warnings);
|
|
92
|
+
if (chain.length === 0)
|
|
93
|
+
continue;
|
|
94
|
+
// Check if the last node in the chain is a branch
|
|
95
|
+
const lastNode = chain[chain.length - 1];
|
|
96
|
+
if (lastNode.type === 'branch') {
|
|
97
|
+
branchContext = {
|
|
98
|
+
node: lastNode,
|
|
99
|
+
indent: leadingSpaces,
|
|
100
|
+
currentLabel: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Wire chain: each node's `then` points to the next
|
|
104
|
+
for (let i = 0; i < chain.length - 1; i++) {
|
|
105
|
+
chain[i].then = [chain[i + 1]];
|
|
106
|
+
}
|
|
107
|
+
// Add root of chain to current graph
|
|
108
|
+
currentGraph.nodes.push(chain[0]);
|
|
109
|
+
}
|
|
110
|
+
// If no graphs were produced at all (empty or comment-only input), return empty
|
|
111
|
+
return { graphs, warnings };
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Chain parser — handles `A -> B -> C` chains within a single line
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
function parseChain(line, lineNum, warnings) {
|
|
117
|
+
// Strip trailing ` ->` — this signals continuation on the next indented line
|
|
118
|
+
// but has no effect on the current chain (the connection is implicit).
|
|
119
|
+
const cleaned = line.replace(/\s*->\s*$/, '');
|
|
120
|
+
// Split on ` -> ` (with surrounding spaces) but preserve content inside parens
|
|
121
|
+
const segments = splitChainArrow(cleaned);
|
|
122
|
+
const nodes = [];
|
|
123
|
+
for (const segment of segments) {
|
|
124
|
+
const trimmed = segment.trim();
|
|
125
|
+
if (trimmed.length === 0)
|
|
126
|
+
continue;
|
|
127
|
+
const node = parseNodeExpression(trimmed, lineNum, warnings);
|
|
128
|
+
if (node) {
|
|
129
|
+
nodes.push(node);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return nodes;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Split a line on ` -> ` tokens, but NOT inside parentheses.
|
|
136
|
+
* E.g. `A(x, y) -> B -> C(a -> b)` splits into [`A(x, y)`, `B`, `C(a -> b)`].
|
|
137
|
+
*/
|
|
138
|
+
function splitChainArrow(line) {
|
|
139
|
+
const segments = [];
|
|
140
|
+
let depth = 0;
|
|
141
|
+
let start = 0;
|
|
142
|
+
for (let i = 0; i < line.length; i++) {
|
|
143
|
+
const ch = line[i];
|
|
144
|
+
if (ch === '(') {
|
|
145
|
+
depth++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (ch === ')') {
|
|
149
|
+
depth--;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (depth === 0 && i + 3 < line.length
|
|
153
|
+
&& line[i] === ' ' && line[i + 1] === '-' && line[i + 2] === '>' && line[i + 3] === ' ') {
|
|
154
|
+
segments.push(line.slice(start, i));
|
|
155
|
+
start = i + 4; // skip ` -> `
|
|
156
|
+
i += 3;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
segments.push(line.slice(start));
|
|
160
|
+
return segments;
|
|
161
|
+
}
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Node expression parser — single node token
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
function parseNodeExpression(expr, lineNum, warnings) {
|
|
166
|
+
let remaining = expr;
|
|
167
|
+
// Extract trailing `as Alias`
|
|
168
|
+
let alias;
|
|
169
|
+
const asMatch = remaining.match(/\s+as\s+(\w+)\s*$/);
|
|
170
|
+
if (asMatch) {
|
|
171
|
+
alias = asMatch[1];
|
|
172
|
+
remaining = remaining.slice(0, asMatch.index).trim();
|
|
173
|
+
}
|
|
174
|
+
// Check for condition pattern: `left operator right ? Branch`
|
|
175
|
+
const conditionMatch = remaining.match(/^(\S+)\s*(>|<|>=|<=|==|!=)\s*(\S+)\s*\?\s*Branch$/);
|
|
176
|
+
if (conditionMatch) {
|
|
177
|
+
return {
|
|
178
|
+
type: 'branch',
|
|
179
|
+
name: 'Branch',
|
|
180
|
+
condition: {
|
|
181
|
+
left: conditionMatch[1],
|
|
182
|
+
operator: conditionMatch[2],
|
|
183
|
+
right: conditionMatch[3],
|
|
184
|
+
},
|
|
185
|
+
alias,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// Check for Event prefix: `Event BeginPlay`
|
|
189
|
+
const eventMatch = remaining.match(/^Event\s+(\w+)$/);
|
|
190
|
+
if (eventMatch) {
|
|
191
|
+
return {
|
|
192
|
+
type: 'event',
|
|
193
|
+
name: eventMatch[1],
|
|
194
|
+
alias,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Check for CastTo pattern: `CastTo(ClassName)`
|
|
198
|
+
const castMatch = remaining.match(/^CastTo\(([^)]+)\)$/);
|
|
199
|
+
if (castMatch) {
|
|
200
|
+
return {
|
|
201
|
+
type: 'cast',
|
|
202
|
+
name: 'CastTo',
|
|
203
|
+
castTo: castMatch[1].trim(),
|
|
204
|
+
alias,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// Check for Set variable: `Set VarName = value`
|
|
208
|
+
const setMatch = remaining.match(/^Set\s+(\w+)\s*=\s*(.+)$/);
|
|
209
|
+
if (setMatch) {
|
|
210
|
+
return {
|
|
211
|
+
type: 'variable_set',
|
|
212
|
+
name: setMatch[1],
|
|
213
|
+
args: { value: parseArgValue(setMatch[2].trim()) },
|
|
214
|
+
alias,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Check for function call with target: `Target.FunctionName(args)` or `Target.FunctionName`
|
|
218
|
+
const targetCallMatch = remaining.match(/^(\w+)\.(\w+)(?:\(([^)]*)\))?$/);
|
|
219
|
+
if (targetCallMatch) {
|
|
220
|
+
const target = targetCallMatch[1];
|
|
221
|
+
const funcName = targetCallMatch[2];
|
|
222
|
+
const argsStr = targetCallMatch[3];
|
|
223
|
+
const node = {
|
|
224
|
+
type: 'call',
|
|
225
|
+
name: funcName,
|
|
226
|
+
target,
|
|
227
|
+
alias,
|
|
228
|
+
};
|
|
229
|
+
if (argsStr !== undefined && argsStr.trim().length > 0) {
|
|
230
|
+
node.args = parseArgs(argsStr, lineNum, warnings);
|
|
231
|
+
}
|
|
232
|
+
return node;
|
|
233
|
+
}
|
|
234
|
+
// Check for plain function call: `FunctionName(args)`
|
|
235
|
+
const callMatch = remaining.match(/^(\w+)\(([^)]*)\)$/);
|
|
236
|
+
if (callMatch) {
|
|
237
|
+
const funcName = callMatch[1];
|
|
238
|
+
const argsStr = callMatch[2];
|
|
239
|
+
const node = {
|
|
240
|
+
type: 'call',
|
|
241
|
+
name: funcName,
|
|
242
|
+
alias,
|
|
243
|
+
};
|
|
244
|
+
if (argsStr.trim().length > 0) {
|
|
245
|
+
node.args = parseArgs(argsStr, lineNum, warnings);
|
|
246
|
+
}
|
|
247
|
+
return node;
|
|
248
|
+
}
|
|
249
|
+
// Check for plain function call with no args: `FunctionName()` (already handled above via empty args)
|
|
250
|
+
// or plain identifier (variable get or parameterless function)
|
|
251
|
+
const identMatch = remaining.match(/^(\w+)$/);
|
|
252
|
+
if (identMatch) {
|
|
253
|
+
return {
|
|
254
|
+
type: 'variable_get',
|
|
255
|
+
name: identMatch[1],
|
|
256
|
+
alias,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// Unrecognized pattern
|
|
260
|
+
warnings.push(`Line ${lineNum}: unrecognized node expression: "${expr}"`);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Argument parsing — `key=value, key=value` or positional `value, value`
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
function parseArgs(argsStr, lineNum, warnings) {
|
|
267
|
+
const result = {};
|
|
268
|
+
const parts = argsStr.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
269
|
+
for (let i = 0; i < parts.length; i++) {
|
|
270
|
+
const part = parts[i];
|
|
271
|
+
const eqIdx = part.indexOf('=');
|
|
272
|
+
if (eqIdx >= 0) {
|
|
273
|
+
const key = part.slice(0, eqIdx).trim();
|
|
274
|
+
const value = part.slice(eqIdx + 1).trim();
|
|
275
|
+
result[key] = parseArgValue(value);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// Positional argument — use index as key
|
|
279
|
+
result[`arg${i}`] = parseArgValue(part);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
function parseArgValue(raw) {
|
|
285
|
+
// Quoted string
|
|
286
|
+
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
287
|
+
return raw.slice(1, -1);
|
|
288
|
+
}
|
|
289
|
+
// Boolean
|
|
290
|
+
if (raw === 'true')
|
|
291
|
+
return true;
|
|
292
|
+
if (raw === 'false')
|
|
293
|
+
return false;
|
|
294
|
+
// Null
|
|
295
|
+
if (raw === 'null')
|
|
296
|
+
return null;
|
|
297
|
+
// Number
|
|
298
|
+
const num = Number(raw);
|
|
299
|
+
if (!isNaN(num) && raw.length > 0)
|
|
300
|
+
return num;
|
|
301
|
+
// Unquoted identifier / string
|
|
302
|
+
return raw;
|
|
303
|
+
}
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// DSL-to-Payload Converter
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
export function blueprintDslToPayload(graphs) {
|
|
308
|
+
const functionGraphs = [];
|
|
309
|
+
for (const graph of graphs) {
|
|
310
|
+
const ctx = {
|
|
311
|
+
nodes: [],
|
|
312
|
+
connections: [],
|
|
313
|
+
aliasMap: new Map(),
|
|
314
|
+
nextId: 0,
|
|
315
|
+
};
|
|
316
|
+
for (const rootNode of graph.nodes) {
|
|
317
|
+
convertNodeTree(rootNode, ctx);
|
|
318
|
+
}
|
|
319
|
+
functionGraphs.push({
|
|
320
|
+
graphName: graph.graphName,
|
|
321
|
+
nodes: ctx.nodes,
|
|
322
|
+
connections: ctx.connections,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return { functionGraphs };
|
|
326
|
+
}
|
|
327
|
+
function allocTempId(ctx) {
|
|
328
|
+
return `n${ctx.nextId++}`;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Convert a DSL node (and its `then` / `branches` children) into payload nodes
|
|
332
|
+
* and connections. Returns the tempId of the created node.
|
|
333
|
+
*/
|
|
334
|
+
function convertNodeTree(node, ctx) {
|
|
335
|
+
const tempId = allocTempId(ctx);
|
|
336
|
+
const payloadNode = buildPayloadNode(node, tempId, ctx);
|
|
337
|
+
ctx.nodes.push(payloadNode);
|
|
338
|
+
// Register alias
|
|
339
|
+
if (node.alias) {
|
|
340
|
+
ctx.aliasMap.set(node.alias, tempId);
|
|
341
|
+
}
|
|
342
|
+
// Process `then` chain (exec output -> next node)
|
|
343
|
+
// Connection is recorded first, then recurse — this keeps connections in
|
|
344
|
+
// declaration order (parent -> child before child -> grandchild).
|
|
345
|
+
if (node.then && node.then.length > 0) {
|
|
346
|
+
for (const nextNode of node.then) {
|
|
347
|
+
const nextId = allocTempId(ctx);
|
|
348
|
+
ctx.connections.push({
|
|
349
|
+
fromNode: tempId,
|
|
350
|
+
fromPin: 'then',
|
|
351
|
+
toNode: nextId,
|
|
352
|
+
toPin: 'execute',
|
|
353
|
+
});
|
|
354
|
+
convertNodeTreeWithId(nextNode, nextId, ctx);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Process branches
|
|
358
|
+
if (node.branches) {
|
|
359
|
+
for (const [label, branchNodes] of Object.entries(node.branches)) {
|
|
360
|
+
const pinName = mapBranchLabelToPin(label);
|
|
361
|
+
for (const branchNode of branchNodes) {
|
|
362
|
+
const branchId = allocTempId(ctx);
|
|
363
|
+
ctx.connections.push({
|
|
364
|
+
fromNode: tempId,
|
|
365
|
+
fromPin: pinName,
|
|
366
|
+
toNode: branchId,
|
|
367
|
+
toPin: 'execute',
|
|
368
|
+
});
|
|
369
|
+
convertNodeTreeWithId(branchNode, branchId, ctx);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Process condition — add comparison node and wire it to the branch
|
|
374
|
+
if (node.condition) {
|
|
375
|
+
const condNode = buildConditionNode(node.condition, ctx);
|
|
376
|
+
ctx.connections.push({
|
|
377
|
+
fromNode: condNode.tempId,
|
|
378
|
+
fromPin: 'ReturnValue',
|
|
379
|
+
toNode: tempId,
|
|
380
|
+
toPin: 'Condition',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return tempId;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Convert a DSL node using a pre-allocated tempId.
|
|
387
|
+
* Used when the caller has already reserved the id for connection ordering.
|
|
388
|
+
*/
|
|
389
|
+
function convertNodeTreeWithId(node, tempId, ctx) {
|
|
390
|
+
const payloadNode = buildPayloadNode(node, tempId, ctx);
|
|
391
|
+
ctx.nodes.push(payloadNode);
|
|
392
|
+
if (node.alias) {
|
|
393
|
+
ctx.aliasMap.set(node.alias, tempId);
|
|
394
|
+
}
|
|
395
|
+
if (node.then && node.then.length > 0) {
|
|
396
|
+
for (const nextNode of node.then) {
|
|
397
|
+
const nextId = allocTempId(ctx);
|
|
398
|
+
ctx.connections.push({
|
|
399
|
+
fromNode: tempId,
|
|
400
|
+
fromPin: 'then',
|
|
401
|
+
toNode: nextId,
|
|
402
|
+
toPin: 'execute',
|
|
403
|
+
});
|
|
404
|
+
convertNodeTreeWithId(nextNode, nextId, ctx);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (node.branches) {
|
|
408
|
+
for (const [label, branchNodes] of Object.entries(node.branches)) {
|
|
409
|
+
const pinName = mapBranchLabelToPin(label);
|
|
410
|
+
for (const branchNode of branchNodes) {
|
|
411
|
+
const branchId = allocTempId(ctx);
|
|
412
|
+
ctx.connections.push({
|
|
413
|
+
fromNode: tempId,
|
|
414
|
+
fromPin: pinName,
|
|
415
|
+
toNode: branchId,
|
|
416
|
+
toPin: 'execute',
|
|
417
|
+
});
|
|
418
|
+
convertNodeTreeWithId(branchNode, branchId, ctx);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (node.condition) {
|
|
423
|
+
const condNode = buildConditionNode(node.condition, ctx);
|
|
424
|
+
ctx.connections.push({
|
|
425
|
+
fromNode: condNode.tempId,
|
|
426
|
+
fromPin: 'ReturnValue',
|
|
427
|
+
toNode: tempId,
|
|
428
|
+
toPin: 'Condition',
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function buildPayloadNode(node, tempId, ctx) {
|
|
433
|
+
switch (node.type) {
|
|
434
|
+
case 'event':
|
|
435
|
+
return { nodeClass: 'K2Node_Event', tempId, eventName: node.name };
|
|
436
|
+
case 'call': {
|
|
437
|
+
const pn = {
|
|
438
|
+
nodeClass: 'K2Node_CallFunction',
|
|
439
|
+
tempId,
|
|
440
|
+
functionName: node.name,
|
|
441
|
+
};
|
|
442
|
+
if (node.target) {
|
|
443
|
+
// Resolve alias to tempId if known, otherwise pass through as target name
|
|
444
|
+
const resolvedTarget = ctx.aliasMap.get(node.target);
|
|
445
|
+
pn.target = resolvedTarget ?? node.target;
|
|
446
|
+
}
|
|
447
|
+
if (node.args) {
|
|
448
|
+
pn.args = node.args;
|
|
449
|
+
}
|
|
450
|
+
return pn;
|
|
451
|
+
}
|
|
452
|
+
case 'cast':
|
|
453
|
+
return { nodeClass: 'K2Node_DynamicCast', tempId, targetClass: node.castTo };
|
|
454
|
+
case 'branch':
|
|
455
|
+
return { nodeClass: 'K2Node_IfThenElse', tempId };
|
|
456
|
+
case 'variable_get':
|
|
457
|
+
return { nodeClass: 'K2Node_VariableGet', tempId, variableName: node.name };
|
|
458
|
+
case 'variable_set': {
|
|
459
|
+
const pn = { nodeClass: 'K2Node_VariableSet', tempId, variableName: node.name };
|
|
460
|
+
if (node.args) {
|
|
461
|
+
pn.value = node.args.value;
|
|
462
|
+
}
|
|
463
|
+
return pn;
|
|
464
|
+
}
|
|
465
|
+
case 'macro':
|
|
466
|
+
return { nodeClass: 'K2Node_MacroInstance', tempId, macroName: node.name };
|
|
467
|
+
case 'literal':
|
|
468
|
+
return { nodeClass: 'K2Node_Literal', tempId, value: node.name };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function buildConditionNode(condition, ctx) {
|
|
472
|
+
const tempId = allocTempId(ctx);
|
|
473
|
+
const node = {
|
|
474
|
+
nodeClass: 'K2Node_CallFunction',
|
|
475
|
+
tempId,
|
|
476
|
+
functionName: operatorToFunctionName(condition.operator),
|
|
477
|
+
args: { A: resolveConditionOperand(condition.left, ctx), B: resolveConditionOperand(condition.right, ctx) },
|
|
478
|
+
};
|
|
479
|
+
ctx.nodes.push(node);
|
|
480
|
+
return node;
|
|
481
|
+
}
|
|
482
|
+
function resolveConditionOperand(operand, ctx) {
|
|
483
|
+
// Check alias map
|
|
484
|
+
const resolved = ctx.aliasMap.get(operand);
|
|
485
|
+
if (resolved)
|
|
486
|
+
return resolved;
|
|
487
|
+
// Try as number
|
|
488
|
+
const num = Number(operand);
|
|
489
|
+
if (!isNaN(num) && operand.length > 0)
|
|
490
|
+
return num;
|
|
491
|
+
// Return as-is (variable or literal reference)
|
|
492
|
+
return operand;
|
|
493
|
+
}
|
|
494
|
+
function operatorToFunctionName(operator) {
|
|
495
|
+
switch (operator) {
|
|
496
|
+
case '>': return 'Greater';
|
|
497
|
+
case '<': return 'Less';
|
|
498
|
+
case '>=': return 'GreaterEqual';
|
|
499
|
+
case '<=': return 'LessEqual';
|
|
500
|
+
case '==': return 'EqualEqual';
|
|
501
|
+
case '!=': return 'NotEqual';
|
|
502
|
+
default: return operator;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function mapBranchLabelToPin(label) {
|
|
506
|
+
// Standard if-then-else branch labels
|
|
507
|
+
const map = {
|
|
508
|
+
True: 'True',
|
|
509
|
+
False: 'False',
|
|
510
|
+
Default: 'Default',
|
|
511
|
+
};
|
|
512
|
+
return map[label] ?? label;
|
|
513
|
+
}
|