323 lines
14 KiB
JavaScript
323 lines
14 KiB
JavaScript
|
|
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
|