Initial checkin of Blueprint-MCP plugin
This commit is contained in:
323
tools/blueprint-mcp/dist/graph-describe.js
vendored
Normal file
323
tools/blueprint-mcp/dist/graph-describe.js
vendored
Normal file
@@ -0,0 +1,323 @@
|
||||
import { flagType, formatVarType, formatParams } from "./helpers.js";
|
||||
export function describeNode(node) {
|
||||
const cls = node.class || "";
|
||||
if (node.nodeType === "CallParentFunction") {
|
||||
return `CALL PARENT ${node.functionName || node.title}`;
|
||||
}
|
||||
if (node.nodeType === "OverrideEvent") {
|
||||
return `OVERRIDE ${node.eventName || node.title}`;
|
||||
}
|
||||
if (cls.includes("CallFunction")) {
|
||||
const target = node.targetClass ? `${node.targetClass}.` : "";
|
||||
return `CALL ${target}${node.functionName || node.title}`;
|
||||
}
|
||||
if (node.nodeType === "VariableSet")
|
||||
return `SET ${node.variableName || node.title}`;
|
||||
if (node.nodeType === "VariableGet")
|
||||
return `GET ${node.variableName || node.title}`;
|
||||
if (node.nodeType === "Branch")
|
||||
return "IF";
|
||||
if (node.nodeType === "DynamicCast")
|
||||
return `CAST to ${node.castTarget || "?"}`;
|
||||
if (node.nodeType === "MacroInstance")
|
||||
return `MACRO ${node.macroName || node.title}`;
|
||||
if (cls.includes("AssignmentStatement"))
|
||||
return "ASSIGN";
|
||||
if (cls.includes("K2Node_Select"))
|
||||
return "SELECT";
|
||||
if (cls.includes("SwitchEnum") || cls.includes("SwitchInteger") || cls.includes("SwitchString") || cls.includes("Switch"))
|
||||
return `SWITCH`;
|
||||
if (cls.includes("ForEachLoop") || cls.includes("ForLoop"))
|
||||
return `FOR LOOP`;
|
||||
if (cls.includes("Sequence"))
|
||||
return "SEQUENCE";
|
||||
if (cls.includes("SpawnActor"))
|
||||
return "SPAWN ACTOR";
|
||||
if (cls.includes("CreateWidget"))
|
||||
return "CREATE WIDGET";
|
||||
if (cls.includes("Knot"))
|
||||
return null; // skip reroute nodes
|
||||
return node.title || cls;
|
||||
}
|
||||
/** Annotate a node description with data pin input connections (#10) */
|
||||
export function annotateDataFlow(node, nodeMap) {
|
||||
const dataInputs = (node.pins || []).filter((p) => p.type !== "exec" && p.direction === "Input" && p.connections?.length > 0);
|
||||
if (dataInputs.length === 0)
|
||||
return "";
|
||||
const parts = [];
|
||||
for (const pin of dataInputs) {
|
||||
for (const conn of pin.connections) {
|
||||
const sourceNode = nodeMap[conn.nodeId];
|
||||
if (!sourceNode)
|
||||
continue;
|
||||
const sourceName = sourceNode.variableName || sourceNode.functionName || sourceNode.title || sourceNode.class || "?";
|
||||
const sourcePin = conn.pinName || "?";
|
||||
parts.push(`${pin.name}=${sourceName}.${sourcePin}`);
|
||||
}
|
||||
}
|
||||
if (parts.length === 0)
|
||||
return "";
|
||||
return `(${parts.join(", ")})`;
|
||||
}
|
||||
/** Annotate output data pins that feed into other nodes (#10) */
|
||||
export function annotateDataOutputs(node, nodeMap) {
|
||||
const lines = [];
|
||||
const dataOutputs = (node.pins || []).filter((p) => p.type !== "exec" && p.direction === "Output" && p.connections?.length > 0);
|
||||
for (const pin of dataOutputs) {
|
||||
for (const conn of pin.connections) {
|
||||
const targetNode = nodeMap[conn.nodeId];
|
||||
if (!targetNode)
|
||||
continue;
|
||||
const targetName = targetNode.variableName || targetNode.functionName || targetNode.title || targetNode.class || "?";
|
||||
lines.push(`\u2192 ${pin.name} \u2192 [${targetName}.${conn.pinName || "?"}]`);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
export function walkExecChain(startNodeId, nodeMap, visited, depth = 0) {
|
||||
if (depth > 50 || visited.has(startNodeId))
|
||||
return [];
|
||||
visited.add(startNodeId);
|
||||
const node = nodeMap[startNodeId];
|
||||
if (!node)
|
||||
return [];
|
||||
const lines = [];
|
||||
const indent = " ".repeat(depth + 1);
|
||||
const desc = describeNode(node);
|
||||
const dataFlow = annotateDataFlow(node, nodeMap);
|
||||
// Find exec output pins (pins with type "exec" and direction "Output")
|
||||
const execOutPins = (node.pins || []).filter((p) => p.type === "exec" && p.direction === "Output");
|
||||
if (node.nodeType === "Branch") {
|
||||
// Special handling for branch: show IF with True/False paths
|
||||
lines.push(`${indent}IF:${dataFlow ? ` ${dataFlow}` : ""}`);
|
||||
for (const pin of execOutPins) {
|
||||
const label = pin.name || "?";
|
||||
if (pin.connections?.length) {
|
||||
lines.push(`${indent} [${label}]:`);
|
||||
for (const conn of pin.connections) {
|
||||
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth + 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (node.class?.includes("Sequence")) {
|
||||
lines.push(`${indent}SEQUENCE:`);
|
||||
for (let i = 0; i < execOutPins.length; i++) {
|
||||
const pin = execOutPins[i];
|
||||
if (pin.connections?.length) {
|
||||
lines.push(`${indent} [${i}]:`);
|
||||
for (const conn of pin.connections) {
|
||||
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth + 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (node.class?.includes("ForEachLoop") || node.class?.includes("ForLoop")) {
|
||||
if (desc)
|
||||
lines.push(`${indent}${desc}:${dataFlow ? ` ${dataFlow}` : ""}`);
|
||||
for (const pin of execOutPins) {
|
||||
const label = pin.name || "?";
|
||||
if (pin.connections?.length) {
|
||||
lines.push(`${indent} [${label}]:`);
|
||||
for (const conn of pin.connections) {
|
||||
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth + 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (node.class?.includes("Switch")) {
|
||||
if (desc)
|
||||
lines.push(`${indent}${desc}:${dataFlow ? ` ${dataFlow}` : ""}`);
|
||||
for (const pin of execOutPins) {
|
||||
if (pin.connections?.length) {
|
||||
lines.push(`${indent} [${pin.name}]:`);
|
||||
for (const conn of pin.connections) {
|
||||
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth + 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Normal linear node: describe it and follow the first "then" exec pin
|
||||
if (desc) {
|
||||
lines.push(`${indent}${desc}${dataFlow ? ` ${dataFlow}` : ""}`);
|
||||
// Show data output connections (#10)
|
||||
const dataOuts = annotateDataOutputs(node, nodeMap);
|
||||
for (const dout of dataOuts) {
|
||||
lines.push(`${indent} ${dout}`);
|
||||
}
|
||||
}
|
||||
// Follow exec chain: look for "then" pin or first exec output with connections
|
||||
const thenPin = execOutPins.find((p) => p.name === "then" || p.name === "execute" || p.name === "output") || execOutPins[0];
|
||||
if (thenPin?.connections?.length) {
|
||||
for (const conn of thenPin.connections) {
|
||||
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth));
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
export function describeGraph(graphData) {
|
||||
const lines = [];
|
||||
const nodes = graphData.nodes || [];
|
||||
lines.push(`# ${graphData.name} (${nodes.length} nodes)`);
|
||||
// State machine description mode
|
||||
if (graphData.graphType === "StateMachine") {
|
||||
if (graphData.entryState) {
|
||||
lines.push(`Entry → ${graphData.entryState}`);
|
||||
}
|
||||
lines.push("");
|
||||
// Collect states and transitions
|
||||
const states = nodes.filter((n) => n.nodeType === "AnimState");
|
||||
const transitions = nodes.filter((n) => n.nodeType === "AnimTransition");
|
||||
if (states.length > 0) {
|
||||
lines.push("States:");
|
||||
for (const s of states) {
|
||||
const animInfo = s.animationAsset ? ` [AnimSequence: ${s.animationAsset}]` :
|
||||
s.blendSpaceAsset ? ` [BlendSpace: ${s.blendSpaceAsset}]` : "";
|
||||
lines.push(` ${s.stateName || s.title}${animInfo}`);
|
||||
}
|
||||
}
|
||||
if (transitions.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Transitions:");
|
||||
for (const t of transitions) {
|
||||
const from = t.fromState || "?";
|
||||
const to = t.toState || "?";
|
||||
const dur = t.crossfadeDuration !== undefined ? `${t.crossfadeDuration}s` : "?";
|
||||
const pri = t.priorityOrder !== undefined ? `priority ${t.priorityOrder}` : "";
|
||||
const bidir = t.bBidirectional ? ", bidirectional" : "";
|
||||
lines.push(` ${from} → ${to} (${dur}${pri ? `, ${pri}` : ""}${bidir})`);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
// AnimGraph — identify anim node types
|
||||
if (graphData.graphType === "AnimGraph") {
|
||||
lines.push(`(Animation Graph)`);
|
||||
}
|
||||
else if (graphData.graphType === "TransitionRule") {
|
||||
lines.push(`(Transition Rule)`);
|
||||
}
|
||||
// Build node lookup
|
||||
const nodeMap = {};
|
||||
for (const n of nodes) {
|
||||
nodeMap[n.id] = n;
|
||||
}
|
||||
// Find entry points: Event nodes, CustomEvent nodes, FunctionEntry nodes
|
||||
const entryNodes = nodes.filter((n) => n.nodeType === "Event" ||
|
||||
n.nodeType === "CustomEvent" ||
|
||||
n.class?.includes("FunctionEntry") ||
|
||||
n.class?.includes("K2Node_Tunnel") && n.pins?.some((p) => p.type === "exec" && p.direction === "Output" && p.connections?.length));
|
||||
if (entryNodes.length === 0) {
|
||||
// No entry points found - list all nodes as a fallback
|
||||
lines.push("\n(No event/entry nodes found)");
|
||||
lines.push("Nodes:");
|
||||
for (const n of nodes) {
|
||||
const desc = describeNode(n);
|
||||
if (desc)
|
||||
lines.push(` ${desc}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
for (const entry of entryNodes) {
|
||||
const label = entry.eventName || entry.title || entry.class;
|
||||
lines.push(`\n## on ${label}:`);
|
||||
// Find exec output pins to start walking
|
||||
const execOuts = (entry.pins || []).filter((p) => p.type === "exec" && p.direction === "Output");
|
||||
const visited = new Set();
|
||||
visited.add(entry.id);
|
||||
for (const pin of execOuts) {
|
||||
if (pin.connections?.length) {
|
||||
for (const conn of pin.connections) {
|
||||
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
export function summarizeBlueprint(data) {
|
||||
const lines = [];
|
||||
lines.push(`# ${data.name}`);
|
||||
lines.push(`Parent: ${data.parentClass || "?"} | Path: ${data.path}`);
|
||||
if (data.blueprintType)
|
||||
lines.push(`Type: ${data.blueprintType}`);
|
||||
if (data.isAnimBlueprint) {
|
||||
lines.push(`Animation Blueprint: yes`);
|
||||
if (data.targetSkeleton)
|
||||
lines.push(`Target Skeleton: ${data.targetSkeleton}`);
|
||||
}
|
||||
if (data.interfaces?.length) {
|
||||
lines.push(`\n## Interfaces (${data.interfaces.length})`);
|
||||
for (const iface of data.interfaces)
|
||||
lines.push(` ${iface}`);
|
||||
}
|
||||
if (data.variables?.length) {
|
||||
lines.push(`\n## Variables (${data.variables.length})`);
|
||||
for (const v of data.variables) {
|
||||
const defVal = v.defaultValue ? ` = ${v.defaultValue}` : "";
|
||||
const cat = v.category ? ` [${v.category}]` : "";
|
||||
const typeStr = flagType(formatVarType(v));
|
||||
lines.push(` ${v.name}: ${typeStr}${defVal}${cat}`);
|
||||
}
|
||||
}
|
||||
if (data.graphs?.length) {
|
||||
lines.push(`\n## Graphs (${data.graphs.length})`);
|
||||
for (const g of data.graphs) {
|
||||
const nodes = g.nodes || [];
|
||||
const nodeCount = nodes.length;
|
||||
// Collect events with their parameters (#9)
|
||||
const events = nodes
|
||||
.filter((n) => n.nodeType === "Event" || n.nodeType === "CustomEvent")
|
||||
.map((n) => {
|
||||
const name = n.eventName || n.title;
|
||||
const params = (n.pins || [])
|
||||
.filter((p) => p.direction === "Output" && p.type !== "exec" && p.name !== "")
|
||||
.map((p) => ({ name: p.name, type: p.subtype || p.type || "" }));
|
||||
return `${name}${formatParams(params.length > 0 ? params : n.parameters)}`;
|
||||
});
|
||||
// Collect unique function calls
|
||||
const calls = [
|
||||
...new Set(nodes
|
||||
.filter((n) => n.class?.includes("CallFunction") && n.functionName)
|
||||
.map((n) => n.functionName)),
|
||||
];
|
||||
// Collect variable writes
|
||||
const varSets = [
|
||||
...new Set(nodes
|
||||
.filter((n) => n.nodeType === "VariableSet" && n.variableName)
|
||||
.map((n) => n.variableName)),
|
||||
];
|
||||
// Collect delegates with parameters (#9)
|
||||
const delegates = nodes
|
||||
.filter((n) => n.class?.includes("CreateDelegate") || n.class?.includes("DelegateFunction") || n.nodeType === "EventDispatcher")
|
||||
.map((n) => {
|
||||
const name = n.delegateName || n.functionName || n.title;
|
||||
return `${name}${formatParams(n.parameters)}`;
|
||||
});
|
||||
// Collect function entries with parameters (#9 - for function graphs)
|
||||
const funcEntries = nodes
|
||||
.filter((n) => n.class?.includes("FunctionEntry"))
|
||||
.map((n) => {
|
||||
const params = (n.pins || [])
|
||||
.filter((p) => p.direction === "Output" && p.type !== "exec" && p.name !== "")
|
||||
.map((p) => ({ name: p.name, type: p.subtype || p.type || "" }));
|
||||
return params.length > 0 ? formatParams(params) : "";
|
||||
});
|
||||
const funcParamStr = funcEntries.length === 1 ? funcEntries[0] : "";
|
||||
const graphTypeStr = g.graphType ? ` [${g.graphType}]` : "";
|
||||
lines.push(` ${g.name} (${nodeCount} nodes)${funcParamStr}${graphTypeStr}`);
|
||||
if (events.length)
|
||||
lines.push(` Events: ${events.join(", ")}`);
|
||||
if (delegates.length)
|
||||
lines.push(` Delegates: ${delegates.join(", ")}`);
|
||||
if (calls.length)
|
||||
lines.push(` Calls: ${calls.join(", ")}`);
|
||||
if (varSets.length)
|
||||
lines.push(` Sets: ${varSets.join(", ")}`);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
//# sourceMappingURL=graph-describe.js.map
|
||||
Reference in New Issue
Block a user