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