diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index da24befb8..79b3ded21 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -20,7 +20,11 @@ public enum ConnectionMode { Previous, Next, - Else + Else, + // Dynamic input/output slots start at index 100 to avoid conflicts + // Actual slot index is (mode - InputBase) or (mode - OutputBase) + InputBase = 100, + OutputBase = 200 } public partial class NodeEditor : UserControl @@ -59,6 +63,10 @@ public List Nodes [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ConnectionMode HotNodeMode { get; set; } = ConnectionMode.Previous; + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int HotNodeSlotIndex { get; set; } = -1; // For input/output slot index + [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool LockNodeChanges { get; set; } = false; @@ -145,6 +153,11 @@ public List Nodes public bool LinksAsRopes { get; set; } = false; public bool ShowGrips { get; set; } = false; + // Current event mode for node validation + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public string CurrentEventMode { get; set; } = string.Empty; + private const float _mouseWheelScrollFactor = 0.04f; private const float _hotNodeTransparency = 0.6f; @@ -301,6 +314,28 @@ public void AddNode(bool linkToPrevious, bool linkToElse, bool condition) LayoutVisibleNodes(); } + /// + /// Validates if a node is compatible with the current event mode. + /// Returns true if node is valid for the current event mode, or if no restrictions apply. + /// + public bool ValidateNodeEventMode(TriggerNode node, out string warningMessage) + { + warningMessage = string.Empty; + + // If no event mode is set or node has no restrictions, allow it + if (string.IsNullOrEmpty(CurrentEventMode) || node.AllowedEventModes.Count == 0) + return true; + + // Check if the node explicitly allows this event mode + if (!node.AllowedEventModes.Contains(CurrentEventMode)) + { + warningMessage = $"Node '{node.Name}' is not explicitly designed for '{CurrentEventMode}' events. Proceed with caution."; + return false; + } + + return true; + } + public void AddConditionNode(bool linkToPrevious, bool linkToElse) { AddNode(linkToPrevious, linkToElse, true); @@ -368,6 +403,80 @@ public void LinkSelectedNodes() Invalidate(); } + /// + /// Links an input variable of the target node to an output variable of the source node. + /// This creates a data flow connection between nodes beyond the standard Next/Previous flow. + /// + public bool LinkInputToOutput(TriggerNode sourceNode, string outputName, TriggerNode targetNode, string inputName) + { + if (sourceNode == null || targetNode == null) + return false; + + // Check if the output exists on the source node + var output = sourceNode.Outputs.FirstOrDefault(o => o.Name == outputName); + if (output == null) + return false; + + // Find or create the input on the target node + var input = targetNode.Inputs.FirstOrDefault(i => i.Name == inputName); + if (input == null) + { + input = new InputVariable { Name = inputName }; + targetNode.Inputs.Add(input); + } + + // Link the input to the output using unique node ID + input.LinkedOutputNodeId = sourceNode.Id; + input.LinkedOutputName = outputName; + + return true; + } + + /// + /// Unlinks an input variable from its connected output. + /// + public void UnlinkInput(TriggerNode node, string inputName) + { + if (node == null) + return; + + var input = node.Inputs.FirstOrDefault(i => i.Name == inputName); + if (input != null) + { + input.LinkedOutputNodeId = Guid.Empty; + input.LinkedOutputName = string.Empty; + } + } + + /// + /// Gets the effective value for an input - either from a linked output or from user-defined arguments. + /// + public string GetEffectiveInputValue(TriggerNode node, string inputName) + { + if (node == null) + return string.Empty; + + var input = node.Inputs.FirstOrDefault(i => i.Name == inputName); + if (input != null && input.IsLinked) + { + // Try to find the linked source node by unique ID + var sourceNode = Nodes.FirstOrDefault(n => n.Id == input.LinkedOutputNodeId); + if (sourceNode != null) + { + // In a real implementation, this would evaluate the output value + return $"[Linked from {sourceNode.Name}.{input.LinkedOutputName}]"; + } + } + + // Fall back to user-defined argument + if (node.DynamicArguments.UserDefinedArguments.TryGetValue(inputName, out string value)) + { + return value; + } + + return string.Empty; + } + public void MoveSelectedNodes(TriggerNode rootNode, Vector2 delta) { if (SelectedNodes.Count <= 1) @@ -931,6 +1040,138 @@ private void DrawHeader(PaintEventArgs e, VisibleNodeBase node) } } + private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node) + { + if (!node.Visible) + return; + + using (var brush = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(0.3f))) + { + // Draw INPUT labels at TOP, positioned ABOVE the node (opposite of then/else which are below) + if (node.Node.Inputs.Count > 0) + { + for (int i = 0; i < node.Node.Inputs.Count; i++) + { + var input = node.Node.Inputs[i]; + var size = TextRenderer.MeasureText(input.Name, Font); + + // Get the grip position for this input + var inputMode = (ConnectionMode)((int)ConnectionMode.InputBase + i); + var inputPoint = node.GetNodeScreenPosition(inputMode); + + // Position label ABOVE the node top (grips are at y=0, so labels go above) + int rectX = (int)inputPoint[0].X; + int rectWidth = (int)(inputPoint[1].X - inputPoint[0].X); + int rectY = node.Location.Y - (int)(size.Height * 1.2f); + + var rect = new Rectangle(rectX, rectY, rectWidth, size.Height); + + // Draw shadow + e.Graphics.DrawImage(Properties.Resources.misc_Shadow, + new Rectangle((int)((inputPoint[0].X + inputPoint[1].X) / 2) - size.Width / 2, rectY, size.Width, size.Height)); + + // Draw label with different color if linked + var labelBrush = input.IsLinked + ? new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(0.6f)) + : brush; + + e.Graphics.DrawString(input.Name, Font, labelBrush, rect, + new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }); + + if (input.IsLinked) + labelBrush.Dispose(); + } + } + + // Draw OUTPUT labels at BOTTOM, positioned like then/else labels + if (node.Node.Outputs.Count > 0) + { + for (int i = 0; i < node.Node.Outputs.Count; i++) + { + var output = node.Node.Outputs[i]; + var size = TextRenderer.MeasureText(output.Name, Font); + + // Get the grip position for this output + var outputMode = (ConnectionMode)((int)ConnectionMode.OutputBase + i); + var outputPoint = node.GetNodeScreenPosition(outputMode); + + // Position label below the grip, like then/else + int rectX = (int)outputPoint[0].X; + int rectWidth = (int)(outputPoint[1].X - outputPoint[0].X); + int rectY = node.Location.Y + node.Height + (int)(size.Height * 0.4f); + + var rect = new Rectangle(rectX, rectY, rectWidth, size.Height); + + // Draw shadow + e.Graphics.DrawImage(Properties.Resources.misc_Shadow, + new Rectangle((int)((outputPoint[0].X + outputPoint[1].X) / 2) - size.Width / 2, rectY, size.Width, size.Height)); + + // Draw label + e.Graphics.DrawString(output.Name, Font, brush, rect, + new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }); + } + } + } + } + + private void DrawInputOutputLinks(PaintEventArgs e, List nodes, VisibleNodeBase node) + { + if (!node.Visible) + return; + + // Draw links from this node's inputs to source outputs + for (int i = 0; i < node.Node.Inputs.Count; i++) + { + var input = node.Node.Inputs[i]; + if (!input.IsLinked) + continue; + + // Find the source node + var sourceNode = nodes.FirstOrDefault(n => n.Node.Id == input.LinkedOutputNodeId); + if (sourceNode == null) + continue; + + // Find the output index + int outputIndex = sourceNode.Node.Outputs.FindIndex(o => o.Name == input.LinkedOutputName); + if (outputIndex < 0) + continue; + + // Get connection points + var p1 = GetInputOutputPosition(sourceNode, outputIndex, false); // output + var p2 = GetInputOutputPosition(node, i, true); // input + + // Draw link with node color + DrawLink(e, node.Node.Color, _connectedNodeTransparency, p1, p2); + } + } + + private PointF[] GetInputOutputPosition(VisibleNodeBase node, int slotIndex, bool isInput) + { + int nodeWidth = node.Width; + int slotCount = isInput ? node.Node.Inputs.Count : node.Node.Outputs.Count; + int spacing = nodeWidth / (slotCount + 1); + int xCenter = node.Location.X + spacing * (slotIndex + 1); + + int yPos; + if (isInput) + { + // Input at TOP + yPos = node.Location.Y; + } + else + { + // Output at BOTTOM + yPos = node.Location.Y + node.Height; + } + + int gripHalfWidth = 30; // Width of connection point + return new PointF[] + { + new PointF(xCenter - gripHalfWidth, yPos), + new PointF(xCenter + gripHalfWidth, yPos) + }; + } + private void DrawVisibleNodeLink(PaintEventArgs e, List nodes, VisibleNodeBase node) { for (int i = 0; i < 2; i++) @@ -1124,10 +1365,14 @@ protected override void OnPaint(PaintEventArgs e) // Draw node links antialiased e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; - // Draw connected nodes + // Draw connected nodes (Previous/Next/Else) foreach (var n in nodeList) DrawVisibleNodeLink(e, nodeList, n); + // Draw input/output links + foreach (var n in nodeList) + DrawInputOutputLinks(e, nodeList, n); + // Draw hot node DrawHotNode(e, nodeList); @@ -1151,6 +1396,10 @@ protected override void OnPaint(PaintEventArgs e) // Draw labels (after everything else) foreach (var n in nodeList) DrawHeader(e, n); + + // Draw input/output labels + foreach (var n in nodeList) + DrawInputOutputLabels(e, n); } } diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs index 82f2d9163..dec276288 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs @@ -8,6 +8,7 @@ using DarkUI.Config; using DarkUI.Controls; using DarkUI.Extensions; +using DarkUI.Forms; using DarkUI.Icons; using TombLib.LevelData; using TombLib.LevelData.VisualScripting; @@ -84,7 +85,80 @@ protected override void Dispose(bool disposing) protected virtual void SpawnGrips() { _grips.Clear(); + + // Only add input/output grips if Node is initialized + // (Node is null during initial control construction) + if (Node != null && Node.Inputs.Count > 0) + { + // Add grips for inputs at TOP, spread horizontally, avoiding center + // Following the pattern of then/else grips in conditional nodes + int inputCount = Node.Inputs.Count; + + // Divide width into sections, reserve center for Previous + // Inputs go on the left and right sides + for (int i = 0; i < inputCount; i++) + { + // Spread inputs across left and right portions + // If even number: half on left, half on right + // If odd: favor left side + int section; + int sectionsPerSide = (inputCount + 1) / 2; // Round up for left side + + if (i < sectionsPerSide) + { + // Left side inputs + section = i; + int leftWidth = Width / 3; + int xPos = (leftWidth * (section + 1)) / (sectionsPerSide + 1); + _grips.Add(new Rectangle(xPos - _gripWidth / 2, 0, _gripWidth, _gripHeight)); + } + else + { + // Right side inputs + section = i - sectionsPerSide; + int rightStart = Width * 2 / 3; + int rightWidth = Width / 3; + int xPos = rightStart + (rightWidth * (section + 1)) / (inputCount - sectionsPerSide + 1); + _grips.Add(new Rectangle(xPos - _gripWidth / 2, 0, _gripWidth, _gripHeight)); + } + } + } + + // Grip for Previous connection (top center) - added after inputs _grips.Add(new Rectangle(Width / 2 - _gripWidth / 2, 0, _gripWidth, _gripHeight)); + + if (Node != null && Node.Outputs.Count > 0) + { + // Add grips for outputs at BOTTOM, spread horizontally + // Will be placed to avoid Next grip position (center bottom) + int outputCount = Node.Outputs.Count; + + // Spread outputs across width, avoiding center + for (int i = 0; i < outputCount; i++) + { + int section; + int sectionsPerSide = (outputCount + 1) / 2; + + if (i < sectionsPerSide) + { + // Left side outputs + section = i; + int leftWidth = Width / 3; + int xPos = (leftWidth * (section + 1)) / (sectionsPerSide + 1); + _grips.Add(new Rectangle(xPos - _gripWidth / 2, Height - _gripHeight, _gripWidth, _gripHeight)); + } + else + { + // Right side outputs + section = i - sectionsPerSide; + int rightStart = Width * 2 / 3; + int rightWidth = Width / 3; + int xPos = rightStart + (rightWidth * (section + 1)) / (outputCount - sectionsPerSide + 1); + _grips.Add(new Rectangle(xPos - _gripWidth / 2, Height - _gripHeight, _gripWidth, _gripHeight)); + } + } + } + Invalidate(); } @@ -120,6 +194,33 @@ public void ResetArguments() var funcSetup = cbFunction.SelectedItem as NodeFunction; for (int i = 0; i < funcSetup.Arguments.Count; i++) Node.Arguments.Add(new TriggerNodeArgument() { Name = funcSetup.Arguments[i].Name, Value = funcSetup.Arguments[i].DefaultValue }); + + // Populate inputs from function definition + Node.Inputs.Clear(); + foreach (var inputLayout in funcSetup.Inputs) + { + Node.Inputs.Add(new InputVariable + { + Name = inputLayout.Name, + LinkedOutputNodeId = Guid.Empty, + LinkedOutputName = string.Empty + }); + } + + // Populate outputs from function definition + Node.Outputs.Clear(); + foreach (var outputLayout in funcSetup.Outputs) + { + Node.Outputs.Add(new OutputVariable + { + Name = outputLayout.Name, + Type = outputLayout.Type + }); + } + + // Populate allowed event modes from function definition + Node.AllowedEventModes.Clear(); + Node.AllowedEventModes.AddRange(funcSetup.AllowedEventModes); } public void SpawnFunctionList(List functions) @@ -274,6 +375,9 @@ public void SpawnUIElements() for (int i = 0; i < Node.Arguments.Count; i++) RefreshArgument(i); + // Disable argument controls that have linked inputs + UpdateArgumentControlStates(); + foreach (var sub in WinFormsUtils.AllSubControls(this)) sub.MouseDown += Ctrl_RightClick; @@ -285,6 +389,39 @@ public void SpawnUIElements() Editor?.Invalidate(); } + /// + /// Updates the enabled/disabled state of argument controls based on whether + /// their corresponding inputs are linked + /// + private void UpdateArgumentControlStates() + { + var func = cbFunction.SelectedItem as NodeFunction; + if (func == null) + return; + + // For each argument, check if there's a corresponding input that is linked + for (int i = 0; i < Node.Arguments.Count && i < _argControls.Count; i++) + { + var argName = Node.Arguments[i].Name; + + // Find if this argument has a corresponding input + var input = Node.Inputs.FirstOrDefault(inp => inp.Name == argName); + + if (input != null && input.IsLinked) + { + // Input is linked - disable the argument control + _argControls[i].Enabled = false; + _argControls[i].BackColor = Color.FromArgb(40, 40, 40); // Darker to show disabled + } + else + { + // Input is not linked - enable the argument control + _argControls[i].Enabled = true; + _argControls[i].BackColor = Color.FromArgb(60, 60, 60); // Normal color + } + } + } + private void Ctrl_LocatedItemFound(object sender, EventArgs e) { if (sender is IHasLuaName) @@ -359,10 +496,45 @@ public PointF[] GetNodeScreenPosition(ConnectionMode mode, bool force = false) if (Node == null) return result; - if (_grips.Count < (int)mode + 1) + // Handle dynamic input/output modes + int gripIndex; + int modeInt = (int)mode; + + // For inputs: mode = InputBase + inputIndex, grip index = inputIndex + if (modeInt >= (int)ConnectionMode.InputBase && modeInt < (int)ConnectionMode.OutputBase) + { + int inputIndex = modeInt - (int)ConnectionMode.InputBase; + gripIndex = inputIndex; + } + // For outputs: mode = OutputBase + outputIndex, grip index = inputCount + 1 + outputIndex + else if (modeInt >= (int)ConnectionMode.OutputBase) + { + int outputIndex = modeInt - (int)ConnectionMode.OutputBase; + gripIndex = Node.Inputs.Count + 1 + outputIndex; + } + // For Previous: grip index = inputCount + else if (mode == ConnectionMode.Previous) + { + gripIndex = Node.Inputs.Count; + } + // For Next/Else: after all inputs, Previous, and outputs + else if (mode == ConnectionMode.Next) + { + gripIndex = Node.Inputs.Count + 1 + Node.Outputs.Count; + } + else if (mode == ConnectionMode.Else) + { + gripIndex = Node.Inputs.Count + 1 + Node.Outputs.Count + 1; + } + else + { + gripIndex = Node.Inputs.Count; // Default to Previous + } + + if (_grips.Count < gripIndex + 1) return result; - var grip = _grips[(int)mode]; + var grip = _grips[gripIndex]; var location = Editor.ToVisualCoord(Node.ScreenPosition); var x = location.X + grip.Left; @@ -380,6 +552,33 @@ private bool ValidConnection(ConnectionMode mode, TriggerNode node) if (node == null || node == Node) return false; + // Handle input/output connections + int modeInt = (int)mode; + int hotModeInt = (int)Editor.HotNodeMode; + + // If connecting TO an input + if (modeInt >= (int)ConnectionMode.InputBase && modeInt < (int)ConnectionMode.OutputBase) + { + // Valid only if dragging FROM an output + if (hotModeInt >= (int)ConnectionMode.OutputBase) + { + // Output → Input is valid + int inputIndex = modeInt - (int)ConnectionMode.InputBase; + if (inputIndex >= 0 && inputIndex < Node.Inputs.Count) + { + // Check if input is already linked + return !Node.Inputs[inputIndex].IsLinked; + } + } + return false; + } + // If connecting TO an output (not allowed - outputs don't receive connections) + else if (modeInt >= (int)ConnectionMode.OutputBase) + { + return false; + } + + // Original validation for Previous/Next/Else if (mode == Editor.HotNodeMode) return false; @@ -467,7 +666,7 @@ protected override void OnDragOver(DragEventArgs e) if (grip == -1) return; - var mode = (ConnectionMode)grip; + var mode = GripToConnectionMode(grip); if (ValidConnection(mode, obj) && Editor.AnimateSnap(mode, this)) _lastSnappedGrip = grip; @@ -486,7 +685,7 @@ protected override void OnDragDrop(DragEventArgs e) return; } - var mode = (ConnectionMode)_lastSnappedGrip; + var mode = GripToConnectionMode(_lastSnappedGrip); var obj = e.Data.GetData(e.Data.GetFormats()[0]) as TriggerNode; if (!ValidConnection(mode, obj)) @@ -495,45 +694,72 @@ protected override void OnDragDrop(DragEventArgs e) return; } - switch (mode) + int modeInt = (int)mode; + + // Handle input/output connections + if (modeInt >= (int)ConnectionMode.InputBase && modeInt < (int)ConnectionMode.OutputBase) { - case ConnectionMode.Previous: + // Connecting TO an input + int inputIndex = modeInt - (int)ConnectionMode.InputBase; + int outputIndex = Editor.HotNodeSlotIndex; + + if (outputIndex >= 0 && outputIndex < obj.Outputs.Count) + { + // Create the link from output to input + Editor.LinkInputToOutput( + obj, // Source node with output + obj.Outputs[outputIndex].Name, + Node, // Target node with input + Node.Inputs[inputIndex].Name + ); + + // Update argument control states + UpdateArgumentControlStates(); + } + } + else + { + // Handle Previous/Next/Else connections (existing code) + switch (mode) + { + case ConnectionMode.Previous: - if (obj is TriggerNodeCondition && Editor.HotNodeMode == ConnectionMode.Else) - (obj as TriggerNodeCondition).Else = Node; - else - obj.Next = Node; + if (obj is TriggerNodeCondition && Editor.HotNodeMode == ConnectionMode.Else) + (obj as TriggerNodeCondition).Else = Node; + else + obj.Next = Node; - Node.Previous = obj; + Node.Previous = obj; - if (Editor.Nodes.Contains(Node)) - Editor.Nodes.Remove(Node); + if (Editor.Nodes.Contains(Node)) + Editor.Nodes.Remove(Node); - break; + break; - case ConnectionMode.Next: + case ConnectionMode.Next: - obj.Previous = Node; - Node.Next = obj; + obj.Previous = Node; + Node.Next = obj; - if (Editor.Nodes.Contains(obj)) - Editor.Nodes.Remove(obj); + if (Editor.Nodes.Contains(obj)) + Editor.Nodes.Remove(obj); - break; + break; - case ConnectionMode.Else: + case ConnectionMode.Else: - if (this is VisibleNodeCondition) - { - var condNode = Node as TriggerNodeCondition; + if (this is VisibleNodeCondition) + { + var condNode = Node as TriggerNodeCondition; - obj.Previous = Node; - condNode.Else = obj; + obj.Previous = Node; + condNode.Else = obj; - if (Editor.Nodes.Contains(obj)) - Editor.Nodes.Remove(obj); - } - break; + if (Editor.Nodes.Contains(obj)) + Editor.Nodes.Remove(obj); + } + break; + } } Editor.HotNode = null; @@ -570,6 +796,66 @@ protected override void OnMouseLeave(EventArgs e) } } + /// + /// Converts a grip index to the appropriate ConnectionMode. + /// Handles dynamic input/output slots. + /// + private ConnectionMode GripToConnectionMode(int gripIndex) + { + // If Node is not initialized yet, default to Previous + if (Node == null) + return ConnectionMode.Previous; + + int inputCount = Node.Inputs.Count; + int outputCount = Node.Outputs.Count; + + // Grips are ordered: [Input0...InputN-1] [Previous] [Output0...OutputM-1] [Next] [Else] + + // Check if it's an input grip (first N grips) + if (gripIndex < inputCount) + { + return (ConnectionMode)((int)ConnectionMode.InputBase + gripIndex); + } + + // Previous grip is at index inputCount + if (gripIndex == inputCount) + return ConnectionMode.Previous; + + // Check if it's an output grip (after Previous, before Next/Else) + int outputGripStart = inputCount + 1; + if (gripIndex >= outputGripStart && gripIndex < outputGripStart + outputCount) + { + int outputIndex = gripIndex - outputGripStart; + return (ConnectionMode)((int)ConnectionMode.OutputBase + outputIndex); + } + + // For conditional nodes, Next and Else are added by VisibleNodeCondition after outputs + int nextElseOffset = inputCount + 1 + outputCount; + if (gripIndex == nextElseOffset) + return ConnectionMode.Next; + if (gripIndex == nextElseOffset + 1) + return ConnectionMode.Else; + + return ConnectionMode.Previous; // Default fallback + } + + /// + /// Extracts the slot index from a ConnectionMode (for inputs/outputs). + /// Returns -1 if not an input/output mode. + /// + private int GetSlotIndexFromMode(ConnectionMode mode) + { + int modeInt = (int)mode; + + if (modeInt >= (int)ConnectionMode.InputBase && modeInt < (int)ConnectionMode.OutputBase) + return modeInt - (int)ConnectionMode.InputBase; + + if (modeInt >= (int)ConnectionMode.OutputBase) + return modeInt - (int)ConnectionMode.OutputBase; + + return -1; + } + protected override void OnMouseMove(MouseEventArgs e) { if (Node.Locked) @@ -591,7 +877,9 @@ protected override void OnMouseMove(MouseEventArgs e) var grip = GetGrip(e.Location); if (grip != -1) { - Editor.HotNodeMode = (ConnectionMode)grip; + var connectionMode = GripToConnectionMode(grip); + Editor.HotNodeMode = connectionMode; + Editor.HotNodeSlotIndex = GetSlotIndexFromMode(connectionMode); Editor.FindForm().ActiveControl = null; DoDragDrop(Node, DragDropEffects.Copy); } @@ -690,6 +978,24 @@ private void cbFunction_SelectedIndexChanged(object sender, EventArgs e) SpawnUIElements(); + // Validate event mode compatibility and show warning if needed + if (Editor != null && !string.IsNullOrEmpty(Editor.CurrentEventMode)) + { + if (Editor.ValidateNodeEventMode(Node, out string warningMessage)) + { + // Node is compatible + } + else + { + // Show warning dialog + DarkMessageBox.Show(Editor, + warningMessage, + "Event Mode Compatibility Warning", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } + } + toolTip.SetToolTip(sender as Control, TextExtensions.SingleLineToMultiLine((cbFunction.SelectedItem as NodeFunction)?.Description ?? string.Empty)); _lastSelectedIndex = cbFunction.SelectedIndex; diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/FAQ_INPUT_LINKING.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/FAQ_INPUT_LINKING.md new file mode 100644 index 000000000..63258ae9c --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/FAQ_INPUT_LINKING.md @@ -0,0 +1,337 @@ +# FAQ: Input/Output Linking - How It Works + +## Common Questions About Input Linking + +### Q: How does the game know which value to use - the linked input or the manual argument? + +**A: Linked inputs take PRECEDENCE over manual arguments. The system follows this priority:** + +1. **First**: Check if input is linked → Use linked value +2. **Fallback**: If not linked → Use manual argument value + +This allows you to: +- Define default values via arguments (always visible in UI) +- Override them by linking to another node's output +- See which inputs are linked vs using manual values + +--- + +## Example: Understanding the Priority System + +### The Node Definition + +```lua +-- !Name "Set moveable transform" +-- !Inputs "newPosition, Vector3, Position to set (can be linked)" "newRotation, Vector3, Rotation to set (can be linked)" +-- !Arguments "NewLine, Vector3, [ -1000000 | 1000000 | 0 | 1 | 32 ], 50, Position value" +-- !Arguments "Vector3, [ -360 | 360 | 0 | 1 | 1 ], 50, Rotation value" +-- !Arguments "NewLine, Moveables, 100, Target moveable" + +LevelFuncs.Engine.Node.SetMoveableTransform = function(positionValue, rotationValue, moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + moveable:SetPosition(positionValue) + moveable:SetRotation(rotationValue) +end +``` + +### What This Means + +**When Input "newPosition" IS Linked:** +- ✅ `positionValue` receives data from the linked node's output +- ❌ Manual argument value is IGNORED (but still visible in UI) +- The linked output value flows directly to the parameter + +**When Input "newPosition" is NOT Linked:** +- ❌ No linked value available +- ✅ `positionValue` receives the manual argument from UI +- User can adjust the Vector3 value in the node's UI controls + +### Visual Representation + +``` +Scenario 1: INPUT IS LINKED +┌────────────────────┐ +│ Source Node │ +│ Output: position │──┐ +│ Value: (10,20,30)│ │ +└────────────────────┘ │ + │ Link carries this value + ↓ +┌────────────────────────────────┐ +│ Target Node │ +│ Input: newPosition (LINKED) │ +│ Argument: (0,0,0) [IGNORED] │ +│ │ +│ positionValue = (10,20,30) ✓ │ +└────────────────────────────────┘ + + +Scenario 2: INPUT NOT LINKED +┌────────────────────────────────┐ +│ Target Node │ +│ Input: newPosition (NOT LINKED)│ +│ Argument: (0,0,0) [USED] │ +│ │ +│ positionValue = (0,0,0) ✓ │ +└────────────────────────────────┘ +``` + +--- + +## Runtime Behavior (How the Engine Works) + +### The Node Execution Process + +When the engine executes a node: + +1. **Parse Metadata**: Load `!Inputs` and `!Arguments` definitions +2. **Check Linking State**: For each input, check if it has a linked connection +3. **Resolve Values**: + ``` + For each function parameter: + If corresponding input exists AND is linked: + → Retrieve value from linked node's output + Else: + → Use value from corresponding argument UI control + ``` +4. **Call Function**: Pass resolved values to the Lua function + +### Pseudo-code Implementation + +```lua +-- Conceptual runtime behavior (not actual TEN code) +function ExecuteNode(node) + local resolvedParams = {} + + -- For each function parameter + for i, param in ipairs(node.FunctionParams) do + -- Check if there's a linked input for this parameter + local input = node.Inputs[param.name] + + if input and input.IsLinked then + -- Priority 1: Get value from linked output + local sourceNode = FindNodeById(input.LinkedOutputNodeId) + resolvedParams[i] = sourceNode.Outputs[input.LinkedOutputName].Value + else + -- Priority 2: Fall back to manual argument + resolvedParams[i] = node.Arguments[i].Value + end + end + + -- Call the actual function with resolved parameters + node.Function(unpack(resolvedParams)) +end +``` + +--- + +## UI Behavior + +### What You See in the Editor + +**Node with Unlinked Inputs:** +``` +┌────────────────────────────────┐ +│ Set Moveable Transform │ +├────────────────────────────────┤ +│ Position: [___][___][___] 📝 │ ← Editable +│ Rotation: [___][___][___] 📝 │ ← Editable +│ Moveable: [Dropdown ] 📝 │ ← Editable +├────────────────────────────────┤ +│ Inputs: │ +│ ○ newPosition (not linked) │ +│ ○ newRotation (not linked) │ +└────────────────────────────────┘ +``` + +**Node with Linked Inputs:** +``` +┌────────────────────────────────┐ +│ Set Moveable Transform │ +├────────────────────────────────┤ +│ Position: [___][___][___] 🔗 │ ← Visible but overridden +│ Rotation: [___][___][___] 🔗 │ ← Visible but overridden +│ Moveable: [Dropdown ] 📝 │ ← Still editable +├────────────────────────────────┤ +│ Inputs: │ +│ ● newPosition ← [GetTransform] │ ← Linked! +│ ● newRotation ← [GetTransform] │ ← Linked! +└────────────────────────────────┘ +``` + +**Key Points:** +- Argument controls remain visible even when input is linked +- Visual indicator (e.g., 🔗 icon) shows input is linked +- Linked input values override argument values at runtime +- You can always see the manual fallback values + +--- + +## Why This Design? + +### Benefits of Input + Argument Pattern + +1. **Flexibility**: Node works standalone OR linked +2. **Default Values**: Arguments provide sensible defaults +3. **Discoverability**: Users see what data is needed +4. **Debugging**: Can temporarily unlink and test with manual values +5. **Backward Compatibility**: Existing projects without links still work + +### Example Use Cases + +**Use Case 1: Standalone Operation** +- Create "Set Transform" node +- Set position manually to (100, 200, 300) +- Works immediately without any linking + +**Use Case 2: Dynamic Operation** +- Create "Get Transform" node for source +- Create "Set Transform" node for target +- Link position output → position input +- Position now dynamically follows source +- Manual argument ignored but provides fallback + +--- + +## Technical Implementation Details + +### How TombEditor Manages This + +The `NodeEditor` class provides methods: + +```csharp +// Check if input is linked +public bool IsInputLinked(TriggerNode node, string inputName) +{ + var input = node.Inputs.FirstOrDefault(i => i.Name == inputName); + return input != null && input.IsLinked; +} + +// Get effective value (linked or argument) +public string GetEffectiveInputValue(TriggerNode node, string inputName) +{ + var input = node.Inputs.FirstOrDefault(i => i.Name == inputName); + + if (input != null && input.IsLinked) + { + // Find linked source node and get output value + var sourceNode = Nodes.FirstOrDefault(n => n.Id == input.LinkedOutputNodeId); + if (sourceNode != null) + return $"[Linked from {sourceNode.Name}.{input.LinkedOutputName}]"; + } + + // Fallback to argument value + if (node.DynamicArguments.UserDefinedArguments.TryGetValue(inputName, out string value)) + return value; + + return string.Empty; +} +``` + +--- + +## Best Practices + +### When Designing Nodes + +**✅ DO:** +- Define both `!Inputs` AND `!Arguments` for the same data +- Provide sensible defaults in arguments +- Document that inputs override arguments +- Name inputs clearly (e.g., "newPosition" vs just "position") + +**❌ DON'T:** +- Assume users will always link inputs +- Hide argument controls when input is linked (keep them visible) +- Make nodes that REQUIRE linking to function +- Create ambiguous parameter names + +### Example: Well-Designed Node + +```lua +-- GOOD: Works standalone AND with linking +-- !Inputs "targetPosition, Vector3, Position to move to" +-- !Arguments "NewLine, Vector3, [0|1000|0], 100, Target position (fallback if not linked)" + +LevelFuncs.Engine.Node.MoveToPosition = function(position, moveable) + -- Works with linked position OR manual position + moveable:SetPosition(position) +end +``` + +```lua +-- BAD: Requires linking, no fallback +-- !Inputs "targetPosition, Vector3, Position to move to" +-- (No arguments - what if not linked?) + +LevelFuncs.Engine.Node.MoveToPosition = function(position, moveable) + -- If position input not linked, this breaks! + moveable:SetPosition(position) +end +``` + +--- + +## Summary: Quick Reference + +| Scenario | Input Linked? | Value Source | UI Behavior | +|----------|---------------|--------------|-------------| +| **Linked** | ✅ Yes | Linked output value | Arguments visible but overridden | +| **Not Linked** | ❌ No | Manual argument value | Arguments editable and used | +| **Partial** | Mixed | Per-input basis | Some linked, some manual | + +**Priority Rule:** `Linked Input Value > Manual Argument Value` + +**Fallback Pattern:** Always define arguments so node can work standalone + +**UI Philosophy:** Keep arguments visible to show defaults and allow debugging + +--- + +## Additional Examples + +### Example 1: Color Node + +```lua +-- !Name "Set color" +-- !Inputs "red, Numerical, Red component" "green, Numerical, Green component" "blue, Numerical, Blue component" +-- !Arguments "NewLine, Numerical, [0|255|0], 33, Red" "Numerical, [0|255|0], 33, Green" "Numerical, [0|255|0], 34, Blue" + +LevelFuncs.Engine.Node.SetColor = function(r, g, b, target) + -- Each component: linked value OR manual value + -- Can link all 3, or just 1, or none + target:SetColor(TEN.Color(r, g, b)) +end +``` + +**Scenario A:** All inputs linked → All values from linked outputs +**Scenario B:** Only red linked → Red from link, green/blue from manual +**Scenario C:** Nothing linked → All values from manual arguments + +### Example 2: Conditional Node + +```lua +-- !Name "If distance is..." +-- !Conditional "True" +-- !Inputs "distance, Numerical, Distance value to check" +-- !Arguments "NewLine, Numerical, [0|10000|0], 50, Distance threshold" +-- !Arguments "CompareOperator, 50, Comparison" + +LevelFuncs.Engine.Node.TestDistance = function(threshold, operator) + -- Note: 'threshold' from argument + -- But distance comes from INPUT (if linked) or would need another argument + local distance = 0 -- This should come from linked input or argument + return LevelFuncs.Engine.Node.CompareValue(distance, threshold, operator) +end +``` + +--- + +## Need More Help? + +See also: +- `HOW_TO_USE_INPUTS_OUTPUTS.md` - Basic tutorial +- `MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md` - Advanced multi-IO examples +- `PARAMETER_MAPPING.md` - How to map inputs to specific parameters +- `Sample Input-Output Nodes.lua` - Working code examples +- `Readme.md` - Complete metadata reference diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/HOW_TO_USE_INPUTS_OUTPUTS.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/HOW_TO_USE_INPUTS_OUTPUTS.md new file mode 100644 index 000000000..842fc77d3 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/HOW_TO_USE_INPUTS_OUTPUTS.md @@ -0,0 +1,249 @@ +# How to Define Nodes with Input/Output and Event Mode Features + +This guide shows you how to create nodes with the new input/output linking system and event mode restrictions. + +## ⚠️ CRITICAL: Understanding Input vs Argument Priority + +**When a node has BOTH `!Inputs` and `!Arguments`, the priority is:** + +``` +Linked Input Value > Manual Argument Value +``` + +**What this means:** +- ✅ **IF input is linked**: Function parameter receives value from the linked node's output +- ✅ **IF input NOT linked**: Function parameter receives value from the manual argument UI control +- The argument provides a **fallback** default when no link exists + +**Example:** +```lua +-- !Inputs "newPosition, Vector3, Position to set (can be linked)" +-- !Arguments "NewLine, Vector3, Position value" +function(positionValue) + -- positionValue = linked input (if connected) OR manual argument (if not) +``` + +👉 **See `FAQ_INPUT_LINKING.md` for complete details on this behavior!** + +--- + +## Quick Example: Position Nodes + +Here's exactly how to define the **GetPosition** and **ModifyPosition** nodes you requested, restricted to **OnLoop** events only: + +### 1. Get Position Node (with Output) + +```lua +-- !Name "Get moveable position" +-- !Section "Moveable parameters" +-- !Description "Gets the current position of a moveable.\nThis position can be linked to other nodes as input." +-- !Outputs "position, Vector3, Current XYZ position of the moveable" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Moveable to get position from" + +LevelFuncs.Engine.Node.GetMoveablePosition = function(moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + local position = moveable:GetPosition() + + -- Return the position so it can be used by linked nodes + return position +end +``` + +**Key points:** +- `!Outputs "position, Vector3, Current XYZ position of the moveable"` defines an output slot + - Format: `"outputName, dataType, description"` +- `!EventModes "OnLoop"` restricts this node to OnLoop global events only +- The function returns the position value that can be linked to other nodes + +### 2. Modify Position Node (with Input) + +```lua +-- !Name "Modify position of a moveable" +-- !Section "Moveable parameters" +-- !Description "Set or modify given moveable position.\nPosition can be linked from another node's output or manually set." +-- !Inputs "newPosition, Vector3, New position value (can be linked from Get Position node)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Enumeration, [ Change | Set ], 25, Change adds/subtracts given value while Set forces it." +-- !Arguments "Vector3, [ -1000000 | 1000000 | 0 | 1 | 32 ], 75, Position value to define" +-- !Arguments "NewLine, Moveables, 100, Moveable to modify" + +LevelFuncs.Engine.Node.SetMoveablePosition = function(operation, value, moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + + if (operation == 0) then + -- Change mode: add/subtract from current position + local position = moveable:GetPosition() + position.x = position.x + value.x + position.y = position.y + value.y + position.z = position.z + value.z + moveable:SetPosition(position) + else + -- Set mode: force position to the value + moveable:SetPosition(value) + end +end +``` + +**Key points:** +- `!Inputs "newPosition, Vector3, New position value..."` defines an input slot + - Format: `"inputName, dataType, description"` +- `!EventModes "OnLoop"` restricts this node to OnLoop global events only +- The input can receive data from the output of another node (like GetPosition) +- If no input is linked, the node falls back to the manual `value` argument + +## Metadata Reference + +### !Outputs Syntax +```lua +-- !Outputs "outputName1, Type1, Description1" "outputName2, Type2, Description2" +``` + +Valid types: `Vector3`, `Vector2`, `Numerical`, `String`, `Boolean`, `Color` + +### !Inputs Syntax +```lua +-- !Inputs "inputName1, Type1, Description1" "inputName2, Type2, Description2" +``` + +Valid types: Same as outputs + +### !EventModes Syntax +```lua +-- !EventModes "Mode1, Mode2, Mode3" +``` + +Valid modes: +- `OnStart` - Runs once when level starts +- `OnEnd` - Runs once when level ends +- `OnLoad` - Runs when level loads +- `OnSave` - Runs when level saves +- `OnControlPhase` - Runs during control phase +- `OnLoop` - Runs every frame +- `OnUseItem` - Runs when item is used +- `OnFreeze` - Runs when game freezes + +If `!EventModes` is omitted, the node can be used in any event. + +## How Linking Works + +1. **Create both nodes** in an OnLoop global event +2. **In the node editor**, you can link the output of GetPosition to the input of SetPosition +3. **At runtime**, when GetPosition executes, its return value is passed to SetPosition's input +4. **Fallback behavior**: If no input is linked, SetPosition uses its manual argument value + +## Event Mode Validation + +When you try to use these nodes in a non-OnLoop event (like OnStart), Tomb Editor will: +- Show a warning: "Node not explicitly designed for 'OnStart' events. Proceed with caution." +- Still allow you to place the node (not a hard error) +- Help you catch potential logic errors + +## Complete Working Example + +Place this in any `.lua` file in the `TEN Node Catalogs` folder: + +```lua +-- !Name "Get moveable position" +-- !Section "Moveable parameters" +-- !Description "Gets the current position of a moveable.\nThis position can be linked to other nodes as input." +-- !Outputs "position, Vector3, Current XYZ position of the moveable" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Moveable to get position from" + +LevelFuncs.Engine.Node.GetMoveablePosition = function(moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + return moveable:GetPosition() +end + +-- !Name "Modify position of a moveable" +-- !Section "Moveable parameters" +-- !Description "Set or modify given moveable position.\nPosition can be linked from another node's output or manually set." +-- !Inputs "newPosition, Vector3, New position value (can be linked from Get Position node)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Enumeration, [ Change | Set ], 25, Change adds/subtracts given value while Set forces it." +-- !Arguments "Vector3, [ -1000000 | 1000000 | 0 | 1 | 32 ], 75, Position value to define" +-- !Arguments "NewLine, Moveables, 100, Moveable to modify" + +LevelFuncs.Engine.Node.SetMoveablePosition = function(operation, value, moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + + if (operation == 0) then + local position = moveable:GetPosition() + position.x = position.x + value.x + position.y = position.y + value.y + position.z = position.z + value.z + moveable:SetPosition(position) + else + moveable:SetPosition(value) + end +end +``` + +After adding this file, restart Tomb Editor and you'll see both nodes available in the "Moveable parameters" section, and they will only be usable in OnLoop global events! + +## Advanced Example: Multiple Outputs to Multiple Inputs + +For more complex operations, you can have nodes with **multiple outputs** linking to **multiple inputs**: + +### Node with TWO Outputs + +```lua +-- !Name "Get moveable transform" +-- !Section "Moveable parameters" +-- !Description "Gets both position AND rotation of a moveable." +-- !Outputs "position, Vector3, Current XYZ position" "rotation, Vector3, Current XYZ rotation" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Moveable to get transform from" + +LevelFuncs.Engine.Node.GetMoveableTransform = function(moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + local position = moveable:GetPosition() + local rotation = moveable:GetRotation() + + -- Returns BOTH position and rotation as separate outputs + return position, rotation +end +``` + +### Node with TWO Inputs + +```lua +-- !Name "Set moveable transform" +-- !Section "Moveable parameters" +-- !Description "Sets both position AND rotation of a moveable." +-- !Inputs "newPosition, Vector3, Position to set" "newRotation, Vector3, Rotation to set" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Vector3, [ -1000000 | 1000000 | 0 | 1 | 32 ], 50, Position" +-- !Arguments "Vector3, [ -360 | 360 | 0 | 1 | 1 ], 50, Rotation" +-- !Arguments "NewLine, Moveables, 100, Target moveable" + +LevelFuncs.Engine.Node.SetMoveableTransform = function(positionValue, rotationValue, moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + moveable:SetPosition(positionValue) + moveable:SetRotation(rotationValue) +end +``` + +### How to Link Multiple Outputs to Multiple Inputs + +1. **Create source node**: Place "Get moveable transform" node +2. **Create target node**: Place "Set moveable transform" node +3. **Link first pair**: Connect `position` output → `newPosition` input +4. **Link second pair**: Connect `rotation` output → `newRotation` input +5. **Result**: Both position and rotation transfer together! + +**Benefits:** +- Transfers multiple related values atomically +- More efficient than separate nodes +- Ensures values are synchronized in the same frame +- Clean, organized node graph + +## Additional Examples + +See `Sample Input-Output Nodes.lua` for more examples including: +- Rotation nodes (input/output) +- Distance calculation nodes +- Conditional nodes with inputs +- Multiple inputs/outputs on the same node (Transform operations) +- Pass-through nodes that both receive and provide multiple values diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md new file mode 100644 index 000000000..d170346d4 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md @@ -0,0 +1,218 @@ +# Example: Linking Multiple Outputs to Multiple Inputs + +This example demonstrates how to create nodes that have **multiple outputs** and **multiple inputs**, and how to link them together. + +## ⚠️ Important: Input vs Argument Priority + +Before diving into examples, understand this key concept: + +**When a node has BOTH `!Inputs` AND `!Arguments`:** +- 🔗 **Linked input takes priority** - If input is connected, it uses the linked value +- 📝 **Argument is fallback** - If input not connected, it uses the manual argument + +This means parameters like `positionValue` automatically receive either: +1. The linked output value (if input is connected), OR +2. The manual argument value (if input not connected) + +**See `FAQ_INPUT_LINKING.md` for complete explanation of this behavior!** + +--- + +## Overview + +Sometimes you need to transfer multiple related values between nodes. For example: +- Position AND rotation together (transform) +- RGB color values +- Min/max range values +- Multiple calculation results + +Instead of creating separate nodes for each value, you can define a single node with multiple outputs and another node with multiple inputs. + +## Complete Example: Transform Operations + +### 1. Node with Multiple Outputs + +This node has **TWO outputs** - position and rotation: + +```lua +-- !Name "Get moveable transform" +-- !Section "Moveable parameters" +-- !Description "Gets both position AND rotation of a moveable.\nBoth values can be linked to other nodes that need transform data." +-- !Outputs "position, Vector3, Current XYZ position" "rotation, Vector3, Current XYZ rotation in degrees" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Moveable to get transform from" + +LevelFuncs.Engine.Node.GetMoveableTransform = function(moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + local position = moveable:GetPosition() + local rotation = moveable:GetRotation() + + -- Return BOTH values - they become separate output slots + return position, rotation +end +``` + +**Key Points:** +- Define multiple outputs: `!Outputs "name1, Type1, Desc1" "name2, Type2, Desc2"` +- Return multiple values: `return value1, value2` +- Each output can be linked independently + +### 2. Node with Multiple Inputs + +This node has **TWO inputs** - new position and new rotation: + +```lua +-- !Name "Set moveable transform" +-- !Section "Moveable parameters" +-- !Description "Sets both position AND rotation of a moveable.\nBoth values can be linked from other nodes (e.g., Get Transform)." +-- !Inputs "newPosition, Vector3, Position to set (can be linked)" "newRotation, Vector3, Rotation to set (can be linked)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Vector3, [ -1000000 | 1000000 | 0 | 1 | 32 ], 50, Position value" +-- !Arguments "Vector3, [ -360 | 360 | 0 | 1 | 1 ], 50, Rotation value in degrees" +-- !Arguments "NewLine, Moveables, 100, Target moveable" + +LevelFuncs.Engine.Node.SetMoveableTransform = function(positionValue, rotationValue, moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + + -- These values come from: + -- 1. Linked inputs (if connected to another node's outputs) + -- 2. Manual argument values (if not linked) + + moveable:SetPosition(positionValue) + moveable:SetRotation(rotationValue) +end +``` + +**Key Points:** +- Define multiple inputs: `!Inputs "name1, Type1, Desc1" "name2, Type2, Desc2"` +- Each input can receive from a linked output +- Falls back to manual argument if not linked + +### 3. How to Link in Tomb Editor + +**Step-by-step:** + +1. **Create source node**: Add "Get moveable transform" node to your OnLoop event + - Select source moveable (e.g., "enemy_01") + +2. **Create target node**: Add "Set moveable transform" node + - Select target moveable (e.g., "camera_dummy") + +3. **Link first output→input**: + - Connect `position` output from GetTransform + - To `newPosition` input on SetTransform + +4. **Link second output→input**: + - Connect `rotation` output from GetTransform + - To `newRotation` input on SetTransform + +5. **Result**: The target moveable will now follow the source moveable's position AND rotation every frame! + +### Visual Representation + +``` +┌─────────────────────────────┐ +│ Get Moveable Transform │ +│ (Moveable: enemy_01) │ +│ │ +│ Outputs: │ +│ ○ position (Vector3) ──────┼──┐ +│ ○ rotation (Vector3) ──────┼──┼──┐ +└─────────────────────────────┘ │ │ + │ │ + │ │ +┌─────────────────────────────┐ │ │ +│ Set Moveable Transform │ │ │ +│ (Moveable: camera_dummy) │ │ │ +│ │ │ │ +│ Inputs: │ │ │ +│ ● newPosition (Vector3) ◄──┼──┘ │ +│ ● newRotation (Vector3) ◄──┼─────┘ +└─────────────────────────────┘ +``` + +## More Examples + +### Example: Pass-Through Node + +A node that both receives inputs AND provides outputs: + +```lua +-- !Name "Copy moveable transform" +-- !Inputs "sourcePosition, Vector3, Position from source" "sourceRotation, Vector3, Rotation from source" +-- !Outputs "position, Vector3, Output position" "rotation, Vector3, Output rotation" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Target moveable" + +LevelFuncs.Engine.Node.CopyMoveableTransform = function(moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + + -- Get values from linked inputs + local position = -- from linked input + local rotation = -- from linked input + + -- Apply to target + moveable:SetPosition(position) + moveable:SetRotation(rotation) + + -- Also output them for further linking + return position, rotation +end +``` + +This allows you to create transform chains: +- Node A outputs transform +- Node B receives from A, applies to moveable B, outputs again +- Node C receives from B, applies to moveable C +- And so on... + +### Example: RGB Color Node + +```lua +-- !Name "Get light color" +-- !Outputs "red, Numerical, Red component" "green, Numerical, Green component" "blue, Numerical, Blue component" +-- !Arguments "NewLine, Moveables, 100, Light source" + +LevelFuncs.Engine.Node.GetLightColor = function(lightName) + local light = TEN.Objects.GetMoveableByName(lightName) + local color = light:GetColor() + return color.r, color.g, color.b +end + +-- !Name "Set light color" +-- !Inputs "red, Numerical, Red component" "green, Numerical, Green component" "blue, Numerical, Blue component" +-- !Arguments "NewLine, Moveables, 100, Target light" + +LevelFuncs.Engine.Node.SetLightColor = function(r, g, b, lightName) + local light = TEN.Objects.GetMoveableByName(lightName) + light:SetColor(TEN.Color(r, g, b)) +end +``` + +## Best Practices + +1. **Group related values**: Position+rotation, min+max, RGB, etc. +2. **Use consistent naming**: If output is "position", input should be "newPosition" +3. **Document relationships**: Clearly state which outputs link to which inputs +4. **Type matching**: Ensure output type matches input type (Vector3→Vector3, etc.) +5. **Consider atomicity**: Multiple outputs ensure related values transfer in same frame + +## Benefits of Multiple Outputs/Inputs + +✅ **Efficiency**: One node instead of multiple separate nodes +✅ **Synchronization**: All values transfer in same frame +✅ **Cleaner graphs**: Fewer nodes, easier to read +✅ **Related data**: Keeps logically related values together +✅ **Flexible**: Each output can link independently to different targets + +## Summary + +To create nodes with multiple outputs/inputs: + +1. **Define multiple outputs**: `!Outputs "out1, Type1, Desc1" "out2, Type2, Desc2"` +2. **Define multiple inputs**: `!Inputs "in1, Type1, Desc1" "in2, Type2, Desc2"` +3. **Return multiple values**: `return value1, value2, value3` +4. **Link in editor**: Connect each output to its corresponding input +5. **Test**: Verify all values transfer correctly + +See `Sample Input-Output Nodes.lua` for complete working examples! diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/PARAMETER_MAPPING.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/PARAMETER_MAPPING.md new file mode 100644 index 000000000..90f8da207 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/PARAMETER_MAPPING.md @@ -0,0 +1,262 @@ +# Parameter Mapping: Linking Inputs to Specific Arguments + +## The Question + +**"I have a node with 4 arguments, but I only want to allow one input. How do I define which argument/parameter is linked to the input?"** + +## The Answer + +### Input-to-Parameter Mapping by Name + +The system maps inputs to function parameters **by matching the input name to the parameter name** in your function signature. + +**Key Rule:** The input's `Name` field should match the function parameter name you want it to replace. + +### Example: Selective Parameter Linking + +Let's say you have a function with 4 parameters but only want to make one of them linkable: + +```lua +-- !Name "Complex operation" +-- !Section "Example" +-- !Description "Demonstrates selective parameter linking" +-- +-- ONLY the 'targetPosition' parameter is linkable via input +-- !Inputs "targetPosition, Vector3, Position can be linked from other nodes" +-- +-- All 4 parameters have manual argument UI controls +-- !Arguments "NewLine, Vector3, [ -1000 | 1000 ], 25, Target position" +-- !Arguments "Numerical, [ 0 | 100 ], 25, Speed value" +-- !Arguments "Boolean, 25, Enable flag" +-- !Arguments "Moveables, 25, Target object" + +LevelFuncs.Engine.Node.ComplexOperation = function(targetPosition, speed, enabled, targetObject) + -- targetPosition: Can be linked OR manual (has input + argument) + -- speed: Manual only (argument only, no input) + -- enabled: Manual only (argument only, no input) + -- targetObject: Manual only (argument only, no input) + + local moveable = TEN.Objects.GetMoveableByName(targetObject) + if enabled then + moveable:MoveTo(targetPosition, speed) + end +end +``` + +### How It Works + +**Parameter Resolution:** + +1. **targetPosition** (1st parameter): + - Has input: `"targetPosition, Vector3, ..."` + - Has argument: `Vector3` argument + - **Resolution**: If input is linked → uses linked value; if not → uses argument value + +2. **speed** (2nd parameter): + - NO input defined + - Has argument: `Numerical` argument + - **Resolution**: Always uses argument value (not linkable) + +3. **enabled** (3rd parameter): + - NO input defined + - Has argument: `Boolean` argument + - **Resolution**: Always uses argument value (not linkable) + +4. **targetObject** (4th parameter): + - NO input defined + - Has argument: `Moveables` argument + - **Resolution**: Always uses argument value (not linkable) + +### Visual Representation + +``` +Function Signature: +function(targetPosition, speed, enabled, targetObject) + ^ ^ ^ ^ + | | | | + | | | +-- Argument only (not linkable) + | | +----------- Argument only (not linkable) + | +------------------ Argument only (not linkable) + +--------------------------------- Input OR Argument (linkable!) +``` + +### Important Naming Rule + +**The input name MUST match the function parameter name:** + +```lua +-- ✅ CORRECT - Names match +-- !Inputs "targetPosition, Vector3, Description" +function(targetPosition, speed, enabled, targetObject) + ^^^^^^^^^^^^^^^ + Names match! ✓ + +-- ❌ WRONG - Names don't match +-- !Inputs "newPosition, Vector3, Description" +function(targetPosition, speed, enabled, targetObject) + ^^^^^^^^^^^^^^^ + Input name doesn't match parameter name! + System won't know which parameter to link +``` + +## More Examples + +### Example 1: Link Only the Value, Not the Object + +```lua +-- !Name "Modify health" +-- !Inputs "healthAmount, Numerical, Health value to set" +-- !Arguments "NewLine, Numerical, [ 0 | 1000 ], 50, Health value" +-- !Arguments "Moveables, 50, Target object" + +LevelFuncs.Engine.Node.ModifyHealth = function(healthAmount, targetObject) + -- healthAmount: Linkable (can receive from other nodes) + -- targetObject: Manual selection only (not linkable) + local moveable = TEN.Objects.GetMoveableByName(targetObject) + moveable:SetHP(healthAmount) +end +``` + +**Use Case:** You want to link a calculated health value from another node, but always manually select which object to apply it to. + +### Example 2: Link the Target, Not the Value + +```lua +-- !Name "Apply damage" +-- !Inputs "targetObject, Moveables, Target to damage" +-- !Arguments "NewLine, Numerical, [ 0 | 1000 ], 50, Damage amount" +-- !Arguments "Moveables, 50, Target (can be linked)" + +LevelFuncs.Engine.Node.ApplyDamage = function(damageAmount, targetObject) + -- damageAmount: Manual value only (not linkable) + -- targetObject: Linkable (can receive from node that outputs moveable name) + local moveable = TEN.Objects.GetMoveableByName(targetObject) + moveable:DealDamage(damageAmount) +end +``` + +**Use Case:** Fixed damage amount, but target can be determined dynamically by another node. + +### Example 3: Multiple Inputs for Some Parameters + +```lua +-- !Name "Advanced transform" +-- !Inputs "newPosition, Vector3, Position to set" "newRotation, Vector3, Rotation to set" +-- !Arguments "NewLine, Vector3, 33, Position" +-- !Arguments "Vector3, 33, Rotation" +-- !Arguments "Numerical, 33, Scale" +-- !Arguments "NewLine, Moveables, 100, Target" + +LevelFuncs.Engine.Node.AdvancedTransform = function(newPosition, newRotation, scale, target) + -- newPosition: Linkable (has input) + -- newRotation: Linkable (has input) + -- scale: Manual only (no input) + -- target: Manual only (no input) + + local moveable = TEN.Objects.GetMoveableByName(target) + moveable:SetPosition(newPosition) + moveable:SetRotation(newRotation) + moveable:SetScale(scale) +end +``` + +**Use Case:** Position and rotation can be dynamic from other nodes, but scale and target are always manual. + +## Argument Order Matters + +**The order of !Arguments must match the function parameter order:** + +```lua +-- Function parameters (left to right): +function(param1, param2, param3, param4) + +-- Arguments must be in same order: +-- !Arguments "...", "First argument (for param1)" +-- !Arguments "...", "Second argument (for param2)" +-- !Arguments "...", "Third argument (for param3)" +-- !Arguments "...", "Fourth argument (for param4)" +``` + +**Inputs don't need to be in the same order** - they match by name, not position: + +```lua +-- These work the same: +-- !Inputs "param1, Type1, Desc1" "param3, Type3, Desc3" +-- !Inputs "param3, Type3, Desc3" "param1, Type1, Desc1" + +-- Both will correctly map: +-- param1 → linkable +-- param2 → argument only +-- param3 → linkable +-- param4 → argument only +``` + +## Design Patterns + +### Pattern 1: Value Input, Target Manual + +**When to use:** Value should be dynamic, but user always picks the target. + +```lua +-- !Inputs "value, Type, Dynamic value" +-- !Arguments "NewLine, Type, Value" "Target, Target to apply to" +function(value, target) +``` + +**Example:** Damage from a calculation node, applied to manually selected enemy. + +### Pattern 2: Target Input, Value Manual + +**When to use:** Target is dynamic, but value is fixed/configured. + +```lua +-- !Inputs "target, Type, Dynamic target" +-- !Arguments "NewLine, Type, Fixed value" "Type, Target" +function(value, target) +``` + +**Example:** Fixed heal amount, applied to player or ally determined by game state. + +### Pattern 3: Mixed Linkable Parameters + +**When to use:** Some data comes from calculations, some from manual config. + +```lua +-- !Inputs "calculatedValue, Type, From other node" "dynamicTarget, Type, From other node" +-- !Arguments "NewLine, Type, Calc value" "Type, Manual setting" "Type, Dynamic target" +function(calculatedValue, manualSetting, dynamicTarget) +``` + +**Example:** Calculated position + manual speed + dynamic target. + +## Summary + +### Quick Rules + +1. **Match input name to parameter name**: `!Inputs "myParam, ..."` maps to `function(myParam, ...)` +2. **Not all parameters need inputs**: Only define inputs for linkable parameters +3. **All parameters need arguments**: Provide arguments for all parameters (they're the fallback) +4. **Arguments order = parameter order**: Arguments must be in same order as function parameters +5. **Inputs order doesn't matter**: Inputs match by name, not position + +### The Binding Table + +| What You Want | Input Needed? | Argument Needed? | Behavior | +|---------------|---------------|------------------|----------| +| **Always linkable** | ✅ Yes | ❌ No (but recommended) | Must be linked to work | +| **Optionally linkable** | ✅ Yes | ✅ Yes | Linked if connected, else argument | +| **Never linkable** | ❌ No | ✅ Yes | Always uses argument value | + +### Best Practice + +**Always provide arguments for ALL parameters, even linkable ones:** +- Arguments show default/fallback values +- Node works standalone without links +- Better UX: users see what's needed +- Easier debugging: can unlink and test manually + +## See Also + +- `FAQ_INPUT_LINKING.md` - How input vs argument priority works +- `Sample Input-Output Nodes.lua` - Working examples +- `Readme.md` - Complete metadata syntax reference diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md index a20dcacdf..452630ce3 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md @@ -75,6 +75,26 @@ Comment metadata entry reference (metadata block is indicated by a keyword which - **!Ignore** - if this keyword is used, nearest encountered function declaration will be ignored. Useful if you need to place helper functions which must be ignored by parser (however, it is recommended to use `_System.lua` file for those). + + - **!Inputs "NAME, TYPE, DESC"** - defines input variable slots that can receive data from other nodes' outputs. + Multiple inputs can be defined by separating them with quotes, e.g. `!Inputs "position, Vector3, XYZ position"`. + Format: `"InputName, DataType, Description"` where: + - **InputName**: Name of the input slot + - **DataType**: Type hint (Vector3, Numerical, String, Boolean, Color, etc.) + - **Description**: Tooltip description for the input + + **IMPORTANT**: When a node has BOTH !Inputs and !Arguments for the same parameter, the linked input takes + priority over the manual argument. If the input is not linked, the argument value is used as fallback. + See `FAQ_INPUT_LINKING.md` for detailed explanation of this behavior. + + - **!Outputs "NAME, TYPE, DESC"** - defines output variable slots that can provide data to other nodes' inputs. + Multiple outputs can be defined by separating them with quotes. Format is the same as !Inputs. + + - **!EventModes "MODE1, MODE2, ..."** - specifies which event modes this node is allowed to be used in. + Valid event modes include: OnStart, OnEnd, OnLoad, OnSave, OnControlPhase, OnLoop, OnUseItem, OnFreeze. + If not specified, node can be used in any event mode. Example: `!EventModes "OnLoop"` restricts the node + to only be usable in OnLoop global events. When used in incompatible event modes, Tomb Editor will display + a warning but still allow usage. Metadata blocks can appear in any order. @@ -159,3 +179,56 @@ show, as third parameter in square brackets is set to 0. **LevelFuncs.CheckEntityHealth** function declaration should contain same amount of arguments and in the same order as metadata argument entry. Therefore, **moveableName** will be read from "Moveable to check" UI argument, **operator** will be read from "Kind of check", and so on. + +### Example with Inputs, Outputs, and Event Modes + +``` +-- !Name "Get moveable position" +-- !Section "Moveable parameters" +-- !Description "Gets the current position of a moveable.\nThis position can be linked to other nodes as input." +-- !Outputs "position, Vector3, Current XYZ position of the moveable" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Moveable to get position from" + +LevelFuncs.Engine.Node.GetMoveablePosition = function(moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + local position = moveable:GetPosition() + return position +end + +-- !Name "Modify position of a moveable" +-- !Section "Moveable parameters" +-- !Description "Set or modify given moveable position.\nPosition can be linked from another node's output." +-- !Inputs "newPosition, Vector3, New position value (can be linked from Get Position node)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Enumeration, [ Change | Set ], 25, Operation type" +-- !Arguments "Vector3, [ -1000000 | 1000000 | 0 | 1 | 32 ], 75, Position value" +-- !Arguments "NewLine, Moveables, 100, Moveable to modify" + +LevelFuncs.Engine.Node.SetMoveablePosition = function(operation, value, moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + + if (operation == 0) then + local position = moveable:GetPosition() + position.x = position.x + value.x + position.y = position.y + value.y + position.z = position.z + value.z + moveable:SetPosition(position) + else + moveable:SetPosition(value) + end +end +``` + +In this example: +- **GetMoveablePosition** defines an output called "position" of type Vector3. This output can be linked to other nodes' inputs. +- **SetMoveablePosition** defines an input called "newPosition" that can receive a Vector3 from another node's output (like GetMoveablePosition). +- Both nodes specify **!EventModes "OnLoop"** which restricts them to only be used in OnLoop global events. If a user tries to use these nodes in other event modes (like OnStart), Tomb Editor will display a warning. +- When nodes are linked via input/output connections, the data flows from the output node to the input node, allowing for dynamic value passing beyond the standard sequential execution flow. + +To link these nodes in Tomb Editor: +1. Create both nodes in a OnLoop event +2. Use the node editor's linking functionality to connect the "position" output of GetMoveablePosition to the "newPosition" input of SetMoveablePosition +3. The position value will be automatically passed between nodes at runtime + +See **Sample Input-Output Nodes.lua** for more complete examples of nodes using the input/output system. diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua new file mode 100644 index 000000000..fec683763 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua @@ -0,0 +1,308 @@ +-- Sample node catalog demonstrating input/output variable linking and event mode restrictions +-- These nodes showcase the new !Inputs, !Outputs, and !EventModes metadata tags +-- +-- ============================================================================ +-- IMPORTANT: How Inputs and Arguments Work Together +-- ============================================================================ +-- When a node has BOTH !Inputs and !Arguments for the same data: +-- +-- PRIORITY: Linked Input > Manual Argument +-- +-- - IF input is linked → Function parameter receives value from linked output +-- - IF input NOT linked → Function parameter receives manual argument value +-- +-- This allows nodes to work BOTH standalone (using arguments) AND dynamically +-- (using linked inputs). The arguments provide default/fallback values. +-- +-- Example: +-- !Inputs "newPosition, Vector3, Position to set" +-- !Arguments "NewLine, Vector3, Position value" +-- function(positionValue) +-- -- positionValue = linked input OR manual argument (priority to linked) +-- +-- See FAQ_INPUT_LINKING.md for detailed explanation! +-- ============================================================================ + + +-- !Name "Get moveable position" +-- !Section "Moveable parameters" +-- !Description "Gets the current position of a moveable.\nThis position can be linked to other nodes as input." +-- !Outputs "position, Vector3, Current XYZ position of the moveable" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Moveable to get position from" + +LevelFuncs.Engine.Node.GetMoveablePosition = function(moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + local position = moveable:GetPosition() + + -- In a real implementation, this would store the position value + -- so it can be retrieved by linked nodes via the output slot + return position +end + +-- !Name "Modify position of a moveable" +-- !Section "Moveable parameters" +-- !Description "Set or modify given moveable position.\nPosition can be linked from another node's output or manually set." +-- !Inputs "newPosition, Vector3, New position value (can be linked from Get Position node)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Enumeration, [ Change | Set ], 25, Change adds/subtracts given value while Set forces it." +-- !Arguments "Vector3, [ -1000000 | 1000000 | 0 | 1 | 32 ], 75, Position value to define" +-- !Arguments "NewLine, Moveables, 100, Moveable to modify" + +LevelFuncs.Engine.Node.SetMoveablePosition = function(operation, value, moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + + -- Check if position is linked from another node + -- In a real implementation, this would check if the input is linked + -- and retrieve the value from the linked node's output + + if (operation == 0) then + -- Change mode: add/subtract from current position + local position = moveable:GetPosition() + position.x = position.x + value.x + position.y = position.y + value.y + position.z = position.z + value.z + moveable:SetPosition(position) + else + -- Set mode: force position + moveable:SetPosition(value) + end +end + +-- !Name "Get moveable rotation" +-- !Section "Moveable parameters" +-- !Description "Gets the current rotation of a moveable.\nThis rotation can be linked to other nodes as input." +-- !Outputs "rotation, Vector3, Current XYZ rotation of the moveable in degrees" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Moveable to get rotation from" + +LevelFuncs.Engine.Node.GetMoveableRotation = function(moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + local rotation = moveable:GetRotation() + + return rotation +end + +-- !Name "Modify rotation of a moveable" +-- !Section "Moveable parameters" +-- !Description "Set or modify given moveable rotation.\nRotation can be linked from another node's output or manually set." +-- !Inputs "newRotation, Vector3, New rotation value (can be linked from Get Rotation node)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Enumeration, [ Change | Set ], 25, Change adds/subtracts given value while Set forces it." +-- !Arguments "Vector3, [ -360 | 360 | 0 | 1 | 1 ], 75, Rotation value in degrees" +-- !Arguments "NewLine, Moveables, 100, Moveable to modify" + +LevelFuncs.Engine.Node.SetMoveableRotation = function(operation, value, moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + + if (operation == 0) then + -- Change mode: add/subtract from current rotation + local rotation = moveable:GetRotation() + rotation.x = rotation.x + value.x + rotation.y = rotation.y + value.y + rotation.z = rotation.z + value.z + moveable:SetRotation(rotation) + else + -- Set mode: force rotation + moveable:SetRotation(value) + end +end + +-- !Name "Calculate distance between moveables" +-- !Section "Moveable parameters" +-- !Description "Calculates the distance between two moveables.\nThis can be useful for proximity checks or distance-based logic." +-- !Outputs "distance, Numerical, Distance between the two moveables" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 50, First moveable" +-- !Arguments "Moveables, 50, Second moveable" + +LevelFuncs.Engine.Node.CalculateMoveableDistance = function(moveableName1, moveableName2) + local moveable1 = TEN.Objects.GetMoveableByName(moveableName1) + local moveable2 = TEN.Objects.GetMoveableByName(moveableName2) + + local pos1 = moveable1:GetPosition() + local pos2 = moveable2:GetPosition() + + local dx = pos2.x - pos1.x + local dy = pos2.y - pos1.y + local dz = pos2.z - pos1.z + + local distance = math.sqrt(dx*dx + dy*dy + dz*dz) + + return distance +end + +-- !Name "If distance is..." +-- !Section "Moveable parameters" +-- !Description "Compares distance between two moveables with a threshold.\nDistance can be linked from Calculate Distance node." +-- !Conditional "True" +-- !Inputs "distance, Numerical, Distance value to check (can be linked from Calculate Distance)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, CompareOperator, 30, Comparison operator" +-- !Arguments "Numerical, 70, [ 0 | 100000 | 0 ], Threshold distance" + +LevelFuncs.Engine.Node.TestDistance = function(operator, threshold) + -- In a real implementation, this would get the distance from the linked input + -- For now, we'll assume it's passed as a parameter + local distance = 0 -- This would come from the linked input + + return LevelFuncs.Engine.Node.CompareValue(distance, threshold, operator) +end + +-- ============================================================================ +-- ADVANCED EXAMPLE: Multiple Outputs Linked to Multiple Inputs +-- ============================================================================ +-- This demonstrates a node with TWO outputs linking to another node with TWO inputs +-- This is useful for operations that need to transfer multiple related values at once + +-- !Name "Get moveable transform" +-- !Section "Moveable parameters" +-- !Description "Gets both position AND rotation of a moveable.\nBoth values can be linked to other nodes that need transform data." +-- !Outputs "position, Vector3, Current XYZ position" "rotation, Vector3, Current XYZ rotation in degrees" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Moveable to get transform from" + +LevelFuncs.Engine.Node.GetMoveableTransform = function(moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + local position = moveable:GetPosition() + local rotation = moveable:GetRotation() + + -- Both position and rotation are available as outputs + -- that can be independently linked to other nodes' inputs + return position, rotation +end + +-- !Name "Set moveable transform" +-- !Section "Moveable parameters" +-- !Description "Sets both position AND rotation of a moveable.\nBoth values can be linked from other nodes (e.g., Get Transform)." +-- !Inputs "newPosition, Vector3, Position to set (can be linked)" "newRotation, Vector3, Rotation to set (can be linked)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Vector3, [ -1000000 | 1000000 | 0 | 1 | 32 ], 50, Position value" +-- !Arguments "Vector3, [ -360 | 360 | 0 | 1 | 1 ], 50, Rotation value in degrees" +-- !Arguments "NewLine, Moveables, 100, Target moveable" + +LevelFuncs.Engine.Node.SetMoveableTransform = function(positionValue, rotationValue, moveableName) + local moveable = TEN.Objects.GetMoveableByName(moveableName) + + -- RUNTIME BEHAVIOR: + -- The engine automatically resolves parameter values with this priority: + -- 1. IF input "newPosition" is linked → positionValue gets linked output value + -- 2. IF input "newPosition" NOT linked → positionValue gets manual argument value + -- Same logic applies to rotationValue and the "newRotation" input + -- + -- This means: + -- - When linked: Manual arguments are IGNORED (but still visible in UI) + -- - When not linked: Manual arguments are USED as fallback + -- - User can mix: link position but manually set rotation, or vice versa + + moveable:SetPosition(positionValue) + moveable:SetRotation(rotationValue) +end + + moveable:SetPosition(positionValue) + moveable:SetRotation(rotationValue) +end + +-- !Name "Copy moveable transform" +-- !Section "Moveable parameters" +-- !Description "Copies position and rotation from source to target.\nThis demonstrates linking 2 outputs to 2 inputs in a single operation." +-- !Inputs "sourcePosition, Vector3, Position from source" "sourceRotation, Vector3, Rotation from source" +-- !Outputs "position, Vector3, Output position" "rotation, Vector3, Output rotation" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Moveables, 100, Target moveable to apply transform to" + +LevelFuncs.Engine.Node.CopyMoveableTransform = function(moveableName) + -- This node acts as a pass-through, receiving 2 inputs and providing 2 outputs + -- Useful for transform chains or applying the same transform to multiple targets + + -- In a real implementation: + -- 1. Gets position and rotation from linked inputs + -- 2. Applies them to the target moveable + -- 3. Also outputs them so they can be linked to other nodes + + local moveable = TEN.Objects.GetMoveableByName(moveableName) + -- Would get values from linked inputs here + local position = TEN.Vec3(0, 0, 0) -- From linked input + local rotation = TEN.Vec3(0, 0, 0) -- From linked input + + moveable:SetPosition(position) + moveable:SetRotation(rotation) + + return position, rotation +end + +-- ============================================================================ +-- USAGE EXAMPLE for Multiple Input/Output Linking: +-- ============================================================================ +-- 1. Create "Get moveable transform" node for source moveable +-- 2. Create "Set moveable transform" node for target moveable +-- 3. Link "position" output → "newPosition" input +-- 4. Link "rotation" output → "newRotation" input +-- 5. Now position and rotation flow from source to target automatically! +-- +-- This is more efficient than creating separate nodes for position and rotation, +-- and ensures both values are transferred atomically in the same frame. +-- ============================================================================ + +-- ============================================================================ +-- ADVANCED: Selective Parameter Linking (Some Parameters Linkable, Some Not) +-- ============================================================================ +-- This example shows how to make ONLY SPECIFIC parameters linkable +-- while keeping others as manual arguments only + +-- !Name "Move with calculated speed" +-- !Section "Moveable parameters" +-- !Description "Moves a moveable to a position. Position is linkable, but speed is always manual." +-- !Inputs "targetPosition, Vector3, Target position (can be linked from other nodes)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Vector3, [ -1000 | 1000 | 0 ], 50, Target position" +-- !Arguments "Numerical, [ 0 | 100 | 0 ], 25, Movement speed" +-- !Arguments "Moveables, 25, Moveable to move" + +LevelFuncs.Engine.Node.MoveWithCalculatedSpeed = function(targetPosition, speed, moveableName) + -- PARAMETER MAPPING: + -- - targetPosition: Has input "targetPosition" → Can be linked OR use argument + -- - speed: NO input → Always uses argument (not linkable) + -- - moveableName: NO input → Always uses argument (not linkable) + -- + -- This allows position to come from another node (e.g., waypoint calculator) + -- while speed and target object are always manually configured + + local moveable = TEN.Objects.GetMoveableByName(moveableName) + moveable:MoveTowards(targetPosition, speed) +end + +-- !Name "Apply dynamic damage" +-- !Section "Moveable parameters" +-- !Description "Applies damage. Amount is linkable from calculations, target is manual." +-- !Inputs "damageAmount, Numerical, Damage amount (can be linked)" +-- !EventModes "OnLoop" +-- !Arguments "NewLine, Numerical, [ 0 | 1000 | 0 ], 50, Damage amount" +-- !Arguments "Moveables, 50, Target to damage" + +LevelFuncs.Engine.Node.ApplyDynamicDamage = function(damageAmount, targetName) + -- PARAMETER MAPPING: + -- - damageAmount: Has input "damageAmount" → Can be linked to damage calculator + -- - targetName: NO input → Always manual selection + -- + -- Use case: Damage calculated by another node based on game state, + -- but user manually picks which enemy to damage + + local moveable = TEN.Objects.GetMoveableByName(targetName) + moveable:DealDamage(damageAmount) +end + +-- ============================================================================ +-- KEY RULE for Parameter Mapping: +-- ============================================================================ +-- The input NAME must MATCH the function parameter NAME! +-- +-- Example: +-- !Inputs "targetPosition, Vector3, Description" +-- function(targetPosition, ...) ← Names match! ✓ +-- +-- The system maps inputs to parameters by name matching, not by position. +-- Parameters without inputs always use their argument values (not linkable). +-- +-- See PARAMETER_MAPPING.md for complete guide on selective parameter linking! +-- ============================================================================ diff --git a/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs b/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs index 84bdddc30..2aabddf18 100644 --- a/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs +++ b/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs @@ -10,6 +10,28 @@ public struct TriggerNodeArgument public string Value { get; set; } } + // Input variable represents a slot that can accept data from another node's output + public class InputVariable + { + public string Name { get; set; } = string.Empty; + public Guid LinkedOutputNodeId { get; set; } = Guid.Empty; // ID of the node providing the output + public string LinkedOutputName { get; set; } = string.Empty; // Name of the specific output variable + public bool IsLinked => LinkedOutputNodeId != Guid.Empty && !string.IsNullOrEmpty(LinkedOutputName); + } + + // Output variable represents a slot that can provide data to another node's input + public class OutputVariable + { + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; // Data type (e.g., "String", "Numerical", "Boolean") + } + + // Dynamic arguments that can be set by user or linked from another node + public class DynamicData + { + public Dictionary UserDefinedArguments { get; } = new Dictionary(); + } + // Every node in visual trigger has this set of parameters. Name and color are // merely UI properties, while Previous/Next and ScreenPosition determines the // order of compilation. Every node may have or may have no any previous or @@ -27,6 +49,9 @@ public abstract class TriggerNode : ICloneable { public static int DefaultSize = 400; + // Unique identifier for each node instance to support unambiguous linking + public Guid Id { get; protected set; } = Guid.NewGuid(); + public string Name { get; set; } = string.Empty; public int Size { get; set; } = DefaultSize; public Vector3 Color { get; set; } = Vector3.Zero; @@ -35,6 +60,12 @@ public abstract class TriggerNode : ICloneable public string Function { get; set; } = string.Empty; public List Arguments { get; private set; } = new List(); + // New properties for enhanced node linking functionality + public List Inputs { get; private set; } = new List(); + public List Outputs { get; private set; } = new List(); + public DynamicData DynamicArguments { get; private set; } = new DynamicData(); + public List AllowedEventModes { get; private set; } = new List(); + public TriggerNode Previous { get; set; } public TriggerNode Next { get; set; } @@ -56,7 +87,39 @@ public Vector2 ScreenPosition public virtual TriggerNode Clone() { var node = (TriggerNode)MemberwiseClone(); + // Generate new ID for cloned node to ensure uniqueness + node.Id = Guid.NewGuid(); node.Arguments = new List(Arguments); + + // Clone new properties + node.Inputs = new List(); + foreach (var input in Inputs) + { + node.Inputs.Add(new InputVariable + { + Name = input.Name, + LinkedOutputNodeId = input.LinkedOutputNodeId, + LinkedOutputName = input.LinkedOutputName + }); + } + + node.Outputs = new List(); + foreach (var output in Outputs) + { + node.Outputs.Add(new OutputVariable + { + Name = output.Name, + Type = output.Type + }); + } + + node.DynamicArguments = new DynamicData(); + foreach (var kvp in DynamicArguments.UserDefinedArguments) + { + node.DynamicArguments.UserDefinedArguments[kvp.Key] = kvp.Value; + } + + node.AllowedEventModes = new List(AllowedEventModes); if (Next != null) { @@ -76,6 +139,31 @@ public override int GetHashCode() hash ^= Function.GetHashCode(); Arguments.ForEach(a => { if (!string.IsNullOrEmpty(a.Value)) hash ^= a.Value.GetHashCode(); }); + + // Include new properties in hash code with all relevant fields + Inputs.ForEach(i => + { + if (!string.IsNullOrEmpty(i.Name)) + hash ^= i.Name.GetHashCode(); + if (i.LinkedOutputNodeId != Guid.Empty) + hash ^= i.LinkedOutputNodeId.GetHashCode(); + if (!string.IsNullOrEmpty(i.LinkedOutputName)) + hash ^= i.LinkedOutputName.GetHashCode(); + }); + Outputs.ForEach(o => + { + if (!string.IsNullOrEmpty(o.Name)) + hash ^= o.Name.GetHashCode(); + if (!string.IsNullOrEmpty(o.Type)) + hash ^= o.Type.GetHashCode(); + }); + foreach (var kvp in DynamicArguments.UserDefinedArguments) + { + if (!string.IsNullOrEmpty(kvp.Value)) + hash ^= kvp.Value.GetHashCode(); + } + AllowedEventModes.ForEach(e => { if (!string.IsNullOrEmpty(e)) hash ^= e.GetHashCode(); }); + if (Next != null) hash ^= Next.GetHashCode(); @@ -155,7 +243,36 @@ public override TriggerNode Clone() ScreenPosition = ScreenPosition }; + // Generate new ID for cloned node to ensure uniqueness + node.Id = Guid.NewGuid(); node.Arguments.AddRange(Arguments); + + // Clone new properties + foreach (var input in Inputs) + { + node.Inputs.Add(new InputVariable + { + Name = input.Name, + LinkedOutputNodeId = input.LinkedOutputNodeId, + LinkedOutputName = input.LinkedOutputName + }); + } + + foreach (var output in Outputs) + { + node.Outputs.Add(new OutputVariable + { + Name = output.Name, + Type = output.Type + }); + } + + foreach (var kvp in DynamicArguments.UserDefinedArguments) + { + node.DynamicArguments.UserDefinedArguments[kvp.Key] = kvp.Value; + } + + node.AllowedEventModes.AddRange(AllowedEventModes); if (Next != null) { diff --git a/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs b/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs index 08e1e0965..64b783dc5 100644 --- a/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs +++ b/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs @@ -70,6 +70,22 @@ public class ArgumentLayout public float Width = 100.0f; } + // Layout definition for input variables in node catalog + public class InputVariableLayout + { + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; // Type hint (e.g., "Vector3", "Numerical", "String") + public string Description { get; set; } = string.Empty; + } + + // Layout definition for output variables in node catalog + public class OutputVariableLayout + { + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; // Type hint (e.g., "Vector3", "Numerical", "String") + public string Description { get; set; } = string.Empty; + } + public class NodeFunction { public string Name { get; set; } @@ -78,6 +94,11 @@ public class NodeFunction public bool Conditional { get; set; } public string Signature { get; set; } public List Arguments { get; private set; } = new List(); + + // New properties for input/output variables and event mode restrictions + public List Inputs { get; private set; } = new List(); + public List Outputs { get; private set; } = new List(); + public List AllowedEventModes { get; private set; } = new List(); public override string ToString() => Name; public override int GetHashCode() => (Name + Conditional.ToString() + Description + Signature + Arguments.Count.ToString()).GetHashCode(); diff --git a/TombLib/TombLib/Utils/ScriptingUtils.cs b/TombLib/TombLib/Utils/ScriptingUtils.cs index e0b0daf77..9de013133 100644 --- a/TombLib/TombLib/Utils/ScriptingUtils.cs +++ b/TombLib/TombLib/Utils/ScriptingUtils.cs @@ -59,6 +59,9 @@ public static class ScriptingUtils private const string _nodeTypeId = _metadataPrefix + "condition"; private const string _nodeArgumentId = _metadataPrefix + "arguments"; private const string _nodeDescriptionId = _metadataPrefix + "description"; + private const string _nodeInputsId = _metadataPrefix + "inputs"; + private const string _nodeOutputsId = _metadataPrefix + "outputs"; + private const string _nodeEventModesId = _metadataPrefix + "eventmodes"; private const string _nodeLayoutNewLine = "newline"; public static string GameNodeScriptPath = Path.Combine("Scripts", "Engine", "NodeCatalogs"); @@ -156,6 +159,57 @@ private static List GetAllNodeFunctions(string path, List st.Trim()).ToList(); + if (parts.Count >= 2) + { + var inputLayout = new InputVariableLayout + { + Name = parts[0], + Type = parts[1], + Description = parts.Count >= 3 ? parts[2] : string.Empty + }; + nodeFunction.Inputs.Add(inputLayout); + } + } + continue; + } + else if (comment.StartsWith(_nodeOutputsId, StringComparison.InvariantCultureIgnoreCase)) + { + var settings = TextExtensions.ExtractValues(comment.Substring(_nodeOutputsId.Length, comment.Length - _nodeOutputsId.Length)); + + foreach (var s in settings) + { + var parts = s.SplitParenthesis().Select(st => st.Trim()).ToList(); + if (parts.Count >= 2) + { + var outputLayout = new OutputVariableLayout + { + Name = parts[0], + Type = parts[1], + Description = parts.Count >= 3 ? parts[2] : string.Empty + }; + nodeFunction.Outputs.Add(outputLayout); + } + } + continue; + } + else if (comment.StartsWith(_nodeEventModesId, StringComparison.InvariantCultureIgnoreCase)) + { + var modes = TextExtensions.ExtractValues(comment.Substring(_nodeEventModesId.Length, comment.Length - _nodeEventModesId.Length)); + + foreach (var mode in modes) + { + var eventModes = mode.SplitParenthesis().Select(st => st.Trim()); + nodeFunction.AllowedEventModes.AddRange(eventModes); + } + continue; + } } if (cPoint > 0)