From e9492deacf5f4632129e0587421abfa8ace6777e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:47:21 +0000 Subject: [PATCH 01/16] Initial plan From 5d1dc7bc340030fa98b201f5df2eb2b6168811ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:51:31 +0000 Subject: [PATCH 02/16] Add input/output variable support and event mode validation to node system Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../Controls/VisualScripting/NodeEditor.cs | 101 ++++++++++++++++++ .../LevelData/VisualScripting/TriggerNode.cs | 96 +++++++++++++++++ 2 files changed, 197 insertions(+) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index da24befb8..75af9e76c 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -145,6 +145,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 +306,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 +395,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 + input.LinkedOutputNodeName = sourceNode.Name; + 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.LinkedOutputNodeName = string.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 and get its output value + var sourceNode = Nodes.FirstOrDefault(n => n.Name == input.LinkedOutputNodeName); + if (sourceNode != null) + { + // In a real implementation, this would evaluate the output value + return $"[Linked from {input.LinkedOutputNodeName}.{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) diff --git a/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs b/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs index 84bdddc30..019214cd4 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 string LinkedOutputNodeName { get; set; } = string.Empty; // Name of the node providing the output + public string LinkedOutputName { get; set; } = string.Empty; // Name of the specific output variable + public bool IsLinked => !string.IsNullOrEmpty(LinkedOutputNodeName) && !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; private set; } = 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 @@ -35,6 +57,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; } @@ -57,6 +85,36 @@ public virtual TriggerNode Clone() { var node = (TriggerNode)MemberwiseClone(); 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, + LinkedOutputNodeName = input.LinkedOutputNodeName, + 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 +134,17 @@ 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 + Inputs.ForEach(i => { if (!string.IsNullOrEmpty(i.Name)) hash ^= i.Name.GetHashCode(); }); + Outputs.ForEach(o => { if (!string.IsNullOrEmpty(o.Name)) hash ^= o.Name.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(); @@ -156,6 +225,33 @@ public override TriggerNode Clone() }; node.Arguments.AddRange(Arguments); + + // Clone new properties + foreach (var input in Inputs) + { + node.Inputs.Add(new InputVariable + { + Name = input.Name, + LinkedOutputNodeName = input.LinkedOutputNodeName, + 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) { From 743fb40cacccf407b1e676fd15083d7d6b63d53c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:53:41 +0000 Subject: [PATCH 03/16] Address code review feedback: use GUID for node identification and improve hash code calculation Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../Controls/VisualScripting/NodeEditor.cs | 12 +++--- .../LevelData/VisualScripting/TriggerNode.cs | 37 +++++++++++++++---- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index 75af9e76c..25ab57c71 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -417,8 +417,8 @@ public bool LinkInputToOutput(TriggerNode sourceNode, string outputName, Trigger targetNode.Inputs.Add(input); } - // Link the input to the output - input.LinkedOutputNodeName = sourceNode.Name; + // Link the input to the output using unique node ID + input.LinkedOutputNodeId = sourceNode.Id; input.LinkedOutputName = outputName; return true; @@ -435,7 +435,7 @@ public void UnlinkInput(TriggerNode node, string inputName) var input = node.Inputs.FirstOrDefault(i => i.Name == inputName); if (input != null) { - input.LinkedOutputNodeName = string.Empty; + input.LinkedOutputNodeId = Guid.Empty; input.LinkedOutputName = string.Empty; } } @@ -451,12 +451,12 @@ public string GetEffectiveInputValue(TriggerNode node, string inputName) var input = node.Inputs.FirstOrDefault(i => i.Name == inputName); if (input != null && input.IsLinked) { - // Try to find the linked source node and get its output value - var sourceNode = Nodes.FirstOrDefault(n => n.Name == input.LinkedOutputNodeName); + // 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 {input.LinkedOutputNodeName}.{input.LinkedOutputName}]"; + return $"[Linked from {sourceNode.Name}.{input.LinkedOutputName}]"; } } diff --git a/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs b/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs index 019214cd4..1ff11c832 100644 --- a/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs +++ b/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs @@ -14,9 +14,9 @@ public struct TriggerNodeArgument public class InputVariable { public string Name { get; set; } = string.Empty; - public string LinkedOutputNodeName { get; set; } = string.Empty; // Name of the node providing the output + 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 => !string.IsNullOrEmpty(LinkedOutputNodeName) && !string.IsNullOrEmpty(LinkedOutputName); + public bool IsLinked => LinkedOutputNodeId != Guid.Empty && !string.IsNullOrEmpty(LinkedOutputName); } // Output variable represents a slot that can provide data to another node's input @@ -29,7 +29,7 @@ public class OutputVariable // Dynamic arguments that can be set by user or linked from another node public class DynamicData { - public Dictionary UserDefinedArguments { get; private set; } = new Dictionary(); + public Dictionary UserDefinedArguments { get; } = new Dictionary(); } // Every node in visual trigger has this set of parameters. Name and color are @@ -49,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; private set; } = Guid.NewGuid(); + public string Name { get; set; } = string.Empty; public int Size { get; set; } = DefaultSize; public Vector3 Color { get; set; } = Vector3.Zero; @@ -84,6 +87,8 @@ 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 @@ -93,7 +98,7 @@ public virtual TriggerNode Clone() node.Inputs.Add(new InputVariable { Name = input.Name, - LinkedOutputNodeName = input.LinkedOutputNodeName, + LinkedOutputNodeId = input.LinkedOutputNodeId, LinkedOutputName = input.LinkedOutputName }); } @@ -135,9 +140,23 @@ public override int GetHashCode() Arguments.ForEach(a => { if (!string.IsNullOrEmpty(a.Value)) hash ^= a.Value.GetHashCode(); }); - // Include new properties in hash code - Inputs.ForEach(i => { if (!string.IsNullOrEmpty(i.Name)) hash ^= i.Name.GetHashCode(); }); - Outputs.ForEach(o => { if (!string.IsNullOrEmpty(o.Name)) hash ^= o.Name.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)) @@ -224,6 +243,8 @@ 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 @@ -232,7 +253,7 @@ public override TriggerNode Clone() node.Inputs.Add(new InputVariable { Name = input.Name, - LinkedOutputNodeName = input.LinkedOutputNodeName, + LinkedOutputNodeId = input.LinkedOutputNodeId, LinkedOutputName = input.LinkedOutputName }); } From 29e97dcde414e5c1816aaee5ae615f00e2ef43a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:37:42 +0000 Subject: [PATCH 04/16] Add parser support for node inputs, outputs, and event mode metadata Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../VisualScripting/VisibleNodeBase.cs | 27 ++++++++++ .../TriggerNodeEnumerations.cs | 21 ++++++++ TombLib/TombLib/Utils/ScriptingUtils.cs | 54 +++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs index 82f2d9163..02741241c 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs @@ -120,6 +120,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) 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) From 852436b00fefd708a22f0f2f47295834e81dd71c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:39:04 +0000 Subject: [PATCH 05/16] Add sample nodes and documentation for input/output linking and event modes Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../Catalogs/TEN Node Catalogs/Readme.md | 69 ++++++++++ .../Sample Input-Output Nodes.lua | 127 ++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md index a20dcacdf..dc1411bae 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md @@ -75,6 +75,22 @@ 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 + + - **!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 +175,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..10e23f998 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua @@ -0,0 +1,127 @@ +-- Sample node catalog demonstrating input/output variable linking and event mode restrictions +-- These nodes showcase the new !Inputs, !Outputs, and !EventModes metadata tags + +-- !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 From d8411caf74cdc2207c75e4c728b6b1d8b99d893d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:39:43 +0000 Subject: [PATCH 06/16] Add comprehensive how-to guide for input/output nodes Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../HOW_TO_USE_INPUTS_OUTPUTS.md | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 TombLib/TombLib/Catalogs/TEN Node Catalogs/HOW_TO_USE_INPUTS_OUTPUTS.md 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..56c9bb9a2 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/HOW_TO_USE_INPUTS_OUTPUTS.md @@ -0,0 +1,166 @@ +# 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. + +## 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! + +## 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 From 971fa8dc88f7a4341dec4e1390576ee3fe6d4f15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:22:26 +0000 Subject: [PATCH 07/16] Add comprehensive examples for multiple input/output linking Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../HOW_TO_USE_INPUTS_OUTPUTS.md | 60 +++++- .../MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md | 202 ++++++++++++++++++ .../Sample Input-Output Nodes.lua | 84 ++++++++ 3 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 TombLib/TombLib/Catalogs/TEN Node Catalogs/MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md 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 index 56c9bb9a2..ca5ee64a1 100644 --- 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 @@ -157,10 +157,68 @@ 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 +- 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..cc6916573 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md @@ -0,0 +1,202 @@ +# 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. + +## 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/Sample Input-Output Nodes.lua b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua index 10e23f998..725500291 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua @@ -125,3 +125,87 @@ LevelFuncs.Engine.Node.TestDistance = function(operator, threshold) 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) + + -- In a real implementation, these would check if inputs are linked + -- and retrieve values from the linked outputs + -- If not linked, they fall back to the manual argument values + + 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. +-- ============================================================================ From a6bbe6550371513d30f28f4227895fdf82f9bc84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:38:21 +0000 Subject: [PATCH 08/16] Add comprehensive FAQ explaining input vs argument priority Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../TEN Node Catalogs/FAQ_INPUT_LINKING.md | 336 ++++++++++++++++++ .../HOW_TO_USE_INPUTS_OUTPUTS.md | 25 ++ .../MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md | 16 + .../Catalogs/TEN Node Catalogs/Readme.md | 4 + .../Sample Input-Output Nodes.lua | 40 ++- 5 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 TombLib/TombLib/Catalogs/TEN Node Catalogs/FAQ_INPUT_LINKING.md 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..2f11ce7bd --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/FAQ_INPUT_LINKING.md @@ -0,0 +1,336 @@ +# 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 +- `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 index ca5ee64a1..842fc77d3 100644 --- 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 @@ -2,6 +2,31 @@ 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: 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 index cc6916573..d170346d4 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/MULTIPLE_INPUTS_OUTPUTS_EXAMPLE.md @@ -2,6 +2,22 @@ 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: diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md index dc1411bae..452630ce3 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md @@ -83,6 +83,10 @@ Comment metadata entry reference (metadata block is indicated by a keyword which - **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. 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 index 725500291..1e2f10466 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua @@ -1,5 +1,28 @@ -- 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" @@ -161,9 +184,20 @@ end LevelFuncs.Engine.Node.SetMoveableTransform = function(positionValue, rotationValue, moveableName) local moveable = TEN.Objects.GetMoveableByName(moveableName) - -- In a real implementation, these would check if inputs are linked - -- and retrieve values from the linked outputs - -- If not linked, they fall back to the manual argument values + -- 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) From d46087411dc4f480cf7f93e18c808ec1538feffc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:23:45 +0000 Subject: [PATCH 09/16] Fix: Change Id setter to protected for derived class access Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs b/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs index 1ff11c832..2aabddf18 100644 --- a/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs +++ b/TombLib/TombLib/LevelData/VisualScripting/TriggerNode.cs @@ -50,7 +50,7 @@ public abstract class TriggerNode : ICloneable public static int DefaultSize = 400; // Unique identifier for each node instance to support unambiguous linking - public Guid Id { get; private set; } = Guid.NewGuid(); + public Guid Id { get; protected set; } = Guid.NewGuid(); public string Name { get; set; } = string.Empty; public int Size { get; set; } = DefaultSize; From 883b6185e4f3185ae9b5dd286b824a2bcb9e04c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:25:32 +0000 Subject: [PATCH 10/16] Add comprehensive guide for selective parameter linking Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../TEN Node Catalogs/FAQ_INPUT_LINKING.md | 1 + .../TEN Node Catalogs/PARAMETER_MAPPING.md | 262 ++++++++++++++++++ .../Sample Input-Output Nodes.lua | 63 +++++ 3 files changed, 326 insertions(+) create mode 100644 TombLib/TombLib/Catalogs/TEN Node Catalogs/PARAMETER_MAPPING.md diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/FAQ_INPUT_LINKING.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/FAQ_INPUT_LINKING.md index 2f11ce7bd..63258ae9c 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/FAQ_INPUT_LINKING.md +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/FAQ_INPUT_LINKING.md @@ -332,5 +332,6 @@ end 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/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/Sample Input-Output Nodes.lua b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua index 1e2f10466..fec683763 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Sample Input-Output Nodes.lua @@ -243,3 +243,66 @@ end -- 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! +-- ============================================================================ From 9cbf2ea730dcbce09a3ad401431b194942e3f4f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:44:34 +0000 Subject: [PATCH 11/16] Implement UI rendering for input/output connections with validation Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../Controls/VisualScripting/NodeEditor.cs | 150 +++++++++++++++++- .../VisualScripting/VisibleNodeBase.cs | 55 +++++++ 2 files changed, 203 insertions(+), 2 deletions(-) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index 25ab57c71..37b1f75c3 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; @@ -1032,6 +1040,136 @@ 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 of node + if (node.Node.Inputs.Count > 0) + { + int inputCount = node.Node.Inputs.Count; + int nodeWidth = node.Width; + int spacing = nodeWidth / (inputCount + 1); + + for (int i = 0; i < inputCount; i++) + { + var input = node.Node.Inputs[i]; + var size = TextRenderer.MeasureText(input.Name, Font); + + // Position above the node + int xPos = node.Location.X + spacing * (i + 1) - size.Width / 2; + int yPos = node.Location.Y - (int)(size.Height * 1.6f); + + var rect = new Rectangle(xPos, yPos, size.Width, size.Height); + + // Draw shadow + e.Graphics.DrawImage(Properties.Resources.misc_Shadow, + new Rectangle(xPos, yPos, 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 of node + if (node.Node.Outputs.Count > 0) + { + int outputCount = node.Node.Outputs.Count; + int nodeWidth = node.Width; + int spacing = nodeWidth / (outputCount + 1); + + for (int i = 0; i < outputCount; i++) + { + var output = node.Node.Outputs[i]; + var size = TextRenderer.MeasureText(output.Name, Font); + + // Position below the node + int xPos = node.Location.X + spacing * (i + 1) - size.Width / 2; + int yPos = node.Location.Y + node.Height + (int)(size.Height * 0.4f); + + var rect = new Rectangle(xPos, yPos, size.Width, size.Height); + + // Draw shadow + e.Graphics.DrawImage(Properties.Resources.misc_Shadow, + new Rectangle(xPos, yPos, 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++) @@ -1225,10 +1363,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); @@ -1252,6 +1394,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 02741241c..6ae02953d 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; @@ -301,6 +302,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; @@ -312,6 +316,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) @@ -717,6 +754,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; From 969e742bacec6dd119dfdd25f93cbb6236fe358b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:54:03 +0000 Subject: [PATCH 12/16] Implement full interactive mouse handling for input/output connections Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../VisualScripting/VisibleNodeBase.cs | 213 +++++++++++++++--- 1 file changed, 182 insertions(+), 31 deletions(-) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs index 6ae02953d..a6702d579 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs @@ -85,7 +85,34 @@ protected override void Dispose(bool disposing) protected virtual void SpawnGrips() { _grips.Clear(); + + // Grip 0: Previous connection (top center) _grips.Add(new Rectangle(Width / 2 - _gripWidth / 2, 0, _gripWidth, _gripHeight)); + + // Add grips for inputs (at top, starting from index 100) + // These will be accessible via ConnectionMode.InputBase + inputIndex + for (int i = 0; i < Node.Inputs.Count; i++) + { + int slotCount = Node.Inputs.Count; + int spacing = Width / (slotCount + 1); + int xCenter = spacing * (i + 1); + int gripHalfWidth = 30; + + _grips.Add(new Rectangle(xCenter - gripHalfWidth, -_gripHeight, gripHalfWidth * 2, _gripHeight * 2)); + } + + // Add grips for outputs (at bottom, starting from index 200) + // These will be accessible via ConnectionMode.OutputBase + outputIndex + for (int i = 0; i < Node.Outputs.Count; i++) + { + int slotCount = Node.Outputs.Count; + int spacing = Width / (slotCount + 1); + int xCenter = spacing * (i + 1); + int gripHalfWidth = 30; + + _grips.Add(new Rectangle(xCenter - gripHalfWidth, Height - _gripHeight, gripHalfWidth * 2, _gripHeight * 2)); + } + Invalidate(); } @@ -423,10 +450,26 @@ 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)mode; + + // For inputs: mode = InputBase + inputIndex, grip index = 1 + inputIndex + if (gripIndex >= (int)ConnectionMode.InputBase && gripIndex < (int)ConnectionMode.OutputBase) + { + int inputIndex = gripIndex - (int)ConnectionMode.InputBase; + gripIndex = 1 + inputIndex; // Skip Previous grip (index 0) + } + // For outputs: mode = OutputBase + outputIndex, grip index = 1 + inputCount + outputIndex + else if (gripIndex >= (int)ConnectionMode.OutputBase) + { + int outputIndex = gripIndex - (int)ConnectionMode.OutputBase; + gripIndex = 1 + Node.Inputs.Count + outputIndex; + } + + 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; @@ -444,6 +487,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; @@ -531,7 +601,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; @@ -550,7 +620,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)) @@ -559,45 +629,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; @@ -634,6 +731,58 @@ protected override void OnMouseLeave(EventArgs e) } } + /// + /// Converts a grip index to the appropriate ConnectionMode. + /// Handles dynamic input/output slots. + /// + private ConnectionMode GripToConnectionMode(int gripIndex) + { + // Grip 0 is always Previous + if (gripIndex == 0) + return ConnectionMode.Previous; + + // Grips 1 to inputCount are inputs + if (gripIndex >= 1 && gripIndex <= Node.Inputs.Count) + { + int inputIndex = gripIndex - 1; + return (ConnectionMode)((int)ConnectionMode.InputBase + inputIndex); + } + + // Grips after inputs are outputs + if (gripIndex > Node.Inputs.Count && gripIndex <= Node.Inputs.Count + Node.Outputs.Count) + { + int outputIndex = gripIndex - 1 - Node.Inputs.Count; + return (ConnectionMode)((int)ConnectionMode.OutputBase + outputIndex); + } + + // For conditional nodes, Next and Else are added by VisibleNodeCondition + // Grip indices continue after outputs + int nextElseOffset = 1 + Node.Inputs.Count + Node.Outputs.Count; + 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) @@ -655,7 +804,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); } From 105e6b2c6e5aeb34f8421778d4bfdce04d473c65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:59:57 +0000 Subject: [PATCH 13/16] Fix NullReferenceException in SpawnGrips and GripToConnectionMode Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../VisualScripting/VisibleNodeBase.cs | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs index a6702d579..f58bddc79 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs @@ -89,28 +89,33 @@ protected virtual void SpawnGrips() // Grip 0: Previous connection (top center) _grips.Add(new Rectangle(Width / 2 - _gripWidth / 2, 0, _gripWidth, _gripHeight)); - // Add grips for inputs (at top, starting from index 100) - // These will be accessible via ConnectionMode.InputBase + inputIndex - for (int i = 0; i < Node.Inputs.Count; i++) - { - int slotCount = Node.Inputs.Count; - int spacing = Width / (slotCount + 1); - int xCenter = spacing * (i + 1); - int gripHalfWidth = 30; - - _grips.Add(new Rectangle(xCenter - gripHalfWidth, -_gripHeight, gripHalfWidth * 2, _gripHeight * 2)); - } - - // Add grips for outputs (at bottom, starting from index 200) - // These will be accessible via ConnectionMode.OutputBase + outputIndex - for (int i = 0; i < Node.Outputs.Count; i++) - { - int slotCount = Node.Outputs.Count; - int spacing = Width / (slotCount + 1); - int xCenter = spacing * (i + 1); - int gripHalfWidth = 30; + // Only add input/output grips if Node is initialized + // (Node is null during initial control construction) + if (Node != null) + { + // Add grips for inputs (at top, starting from index 100) + // These will be accessible via ConnectionMode.InputBase + inputIndex + for (int i = 0; i < Node.Inputs.Count; i++) + { + int slotCount = Node.Inputs.Count; + int spacing = Width / (slotCount + 1); + int xCenter = spacing * (i + 1); + int gripHalfWidth = 30; + + _grips.Add(new Rectangle(xCenter - gripHalfWidth, -_gripHeight, gripHalfWidth * 2, _gripHeight * 2)); + } - _grips.Add(new Rectangle(xCenter - gripHalfWidth, Height - _gripHeight, gripHalfWidth * 2, _gripHeight * 2)); + // Add grips for outputs (at bottom, starting from index 200) + // These will be accessible via ConnectionMode.OutputBase + outputIndex + for (int i = 0; i < Node.Outputs.Count; i++) + { + int slotCount = Node.Outputs.Count; + int spacing = Width / (slotCount + 1); + int xCenter = spacing * (i + 1); + int gripHalfWidth = 30; + + _grips.Add(new Rectangle(xCenter - gripHalfWidth, Height - _gripHeight, gripHalfWidth * 2, _gripHeight * 2)); + } } Invalidate(); @@ -741,6 +746,10 @@ private ConnectionMode GripToConnectionMode(int gripIndex) if (gripIndex == 0) return ConnectionMode.Previous; + // If Node is not initialized yet, return Previous as default + if (Node == null) + return ConnectionMode.Previous; + // Grips 1 to inputCount are inputs if (gripIndex >= 1 && gripIndex <= Node.Inputs.Count) { From d20df5c78ebb0a3bb802358f9c15c34f0b6417f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:05:57 +0000 Subject: [PATCH 14/16] Fix overlapping grips by separating inputs and outputs vertically Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../Controls/VisualScripting/NodeEditor.cs | 12 ++++++------ .../Controls/VisualScripting/VisibleNodeBase.cs | 9 ++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index 37b1f75c3..a505b2299 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -1047,7 +1047,7 @@ private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node) using (var brush = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(0.3f))) { - // Draw INPUT labels at TOP of node + // Draw INPUT labels at TOP of node (higher up to match grip position) if (node.Node.Inputs.Count > 0) { int inputCount = node.Node.Inputs.Count; @@ -1059,9 +1059,9 @@ private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node) var input = node.Node.Inputs[i]; var size = TextRenderer.MeasureText(input.Name, Font); - // Position above the node + // Position higher up to match the new grip position int xPos = node.Location.X + spacing * (i + 1) - size.Width / 2; - int yPos = node.Location.Y - (int)(size.Height * 1.6f); + int yPos = node.Location.Y - (int)(size.Height * 2.5f); var rect = new Rectangle(xPos, yPos, size.Width, size.Height); @@ -1082,7 +1082,7 @@ private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node) } } - // Draw OUTPUT labels at BOTTOM of node + // Draw OUTPUT labels at BOTTOM of node (lower down to match grip position) if (node.Node.Outputs.Count > 0) { int outputCount = node.Node.Outputs.Count; @@ -1094,9 +1094,9 @@ private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node) var output = node.Node.Outputs[i]; var size = TextRenderer.MeasureText(output.Name, Font); - // Position below the node + // Position lower down to match the new grip position int xPos = node.Location.X + spacing * (i + 1) - size.Width / 2; - int yPos = node.Location.Y + node.Height + (int)(size.Height * 0.4f); + int yPos = node.Location.Y + node.Height + (int)(size.Height * 1.2f); var rect = new Rectangle(xPos, yPos, size.Width, size.Height); diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs index f58bddc79..ed98501e8 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs @@ -93,7 +93,7 @@ protected virtual void SpawnGrips() // (Node is null during initial control construction) if (Node != null) { - // Add grips for inputs (at top, starting from index 100) + // Add grips for inputs (at top, ABOVE Previous to avoid overlap) // These will be accessible via ConnectionMode.InputBase + inputIndex for (int i = 0; i < Node.Inputs.Count; i++) { @@ -102,7 +102,9 @@ protected virtual void SpawnGrips() int xCenter = spacing * (i + 1); int gripHalfWidth = 30; - _grips.Add(new Rectangle(xCenter - gripHalfWidth, -_gripHeight, gripHalfWidth * 2, _gripHeight * 2)); + // Position input grips higher up to avoid overlapping with Previous grip + // Previous is at y=0, so we put inputs at y=-(_gripHeight*3) with height=_gripHeight*2 + _grips.Add(new Rectangle(xCenter - gripHalfWidth, -(_gripHeight * 3), gripHalfWidth * 2, _gripHeight * 2)); } // Add grips for outputs (at bottom, starting from index 200) @@ -114,7 +116,8 @@ protected virtual void SpawnGrips() int xCenter = spacing * (i + 1); int gripHalfWidth = 30; - _grips.Add(new Rectangle(xCenter - gripHalfWidth, Height - _gripHeight, gripHalfWidth * 2, _gripHeight * 2)); + // Position output grips below the node with some spacing + _grips.Add(new Rectangle(xCenter - gripHalfWidth, Height + _gripHeight, gripHalfWidth * 2, _gripHeight * 2)); } } From 4824eb0e4043e72acbeccaf02b1acb30c9bb0f71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:11:41 +0000 Subject: [PATCH 15/16] Redesign grip layout to match then/else pattern - horizontal spreading Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../Controls/VisualScripting/NodeEditor.cs | 46 +++--- .../VisualScripting/VisibleNodeBase.cs | 155 ++++++++++++------ 2 files changed, 132 insertions(+), 69 deletions(-) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index a505b2299..a9560ea43 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -1047,27 +1047,28 @@ private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node) using (var brush = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(0.3f))) { - // Draw INPUT labels at TOP of node (higher up to match grip position) + // Draw INPUT labels at TOP, positioned like then/else labels if (node.Node.Inputs.Count > 0) { - int inputCount = node.Node.Inputs.Count; - int nodeWidth = node.Width; - int spacing = nodeWidth / (inputCount + 1); - - for (int i = 0; i < inputCount; i++) + for (int i = 0; i < node.Node.Inputs.Count; i++) { var input = node.Node.Inputs[i]; var size = TextRenderer.MeasureText(input.Name, Font); - // Position higher up to match the new grip position - int xPos = node.Location.X + spacing * (i + 1) - size.Width / 2; - int yPos = node.Location.Y - (int)(size.Height * 2.5f); + // Get the grip position for this input + var inputMode = (ConnectionMode)((int)ConnectionMode.InputBase + i); + var inputPoint = node.GetNodeScreenPosition(inputMode); + + // Position label below the grip, like then/else + int rectX = (int)inputPoint[0].X; + int rectWidth = (int)(inputPoint[1].X - inputPoint[0].X); + int rectY = node.Location.Y + (int)(size.Height * 0.4f); - var rect = new Rectangle(xPos, yPos, size.Width, size.Height); + var rect = new Rectangle(rectX, rectY, rectWidth, size.Height); // Draw shadow e.Graphics.DrawImage(Properties.Resources.misc_Shadow, - new Rectangle(xPos, yPos, size.Width, size.Height)); + 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 @@ -1082,27 +1083,28 @@ private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node) } } - // Draw OUTPUT labels at BOTTOM of node (lower down to match grip position) + // Draw OUTPUT labels at BOTTOM, positioned like then/else labels if (node.Node.Outputs.Count > 0) { - int outputCount = node.Node.Outputs.Count; - int nodeWidth = node.Width; - int spacing = nodeWidth / (outputCount + 1); - - for (int i = 0; i < outputCount; i++) + for (int i = 0; i < node.Node.Outputs.Count; i++) { var output = node.Node.Outputs[i]; var size = TextRenderer.MeasureText(output.Name, Font); - // Position lower down to match the new grip position - int xPos = node.Location.X + spacing * (i + 1) - size.Width / 2; - int yPos = node.Location.Y + node.Height + (int)(size.Height * 1.2f); + // 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(xPos, yPos, size.Width, size.Height); + var rect = new Rectangle(rectX, rectY, rectWidth, size.Height); // Draw shadow e.Graphics.DrawImage(Properties.Resources.misc_Shadow, - new Rectangle(xPos, yPos, size.Width, size.Height)); + 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, diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs index ed98501e8..dec276288 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/VisibleNodeBase.cs @@ -86,38 +86,76 @@ protected virtual void SpawnGrips() { _grips.Clear(); - // Grip 0: Previous connection (top center) - _grips.Add(new Rectangle(Width / 2 - _gripWidth / 2, 0, _gripWidth, _gripHeight)); - // Only add input/output grips if Node is initialized // (Node is null during initial control construction) - if (Node != null) + if (Node != null && Node.Inputs.Count > 0) { - // Add grips for inputs (at top, ABOVE Previous to avoid overlap) - // These will be accessible via ConnectionMode.InputBase + inputIndex - for (int i = 0; i < Node.Inputs.Count; i++) + // 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++) { - int slotCount = Node.Inputs.Count; - int spacing = Width / (slotCount + 1); - int xCenter = spacing * (i + 1); - int gripHalfWidth = 30; + // 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 - // Position input grips higher up to avoid overlapping with Previous grip - // Previous is at y=0, so we put inputs at y=-(_gripHeight*3) with height=_gripHeight*2 - _grips.Add(new Rectangle(xCenter - gripHalfWidth, -(_gripHeight * 3), gripHalfWidth * 2, _gripHeight * 2)); + 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; - // Add grips for outputs (at bottom, starting from index 200) - // These will be accessible via ConnectionMode.OutputBase + outputIndex - for (int i = 0; i < Node.Outputs.Count; i++) + // Spread outputs across width, avoiding center + for (int i = 0; i < outputCount; i++) { - int slotCount = Node.Outputs.Count; - int spacing = Width / (slotCount + 1); - int xCenter = spacing * (i + 1); - int gripHalfWidth = 30; + int section; + int sectionsPerSide = (outputCount + 1) / 2; - // Position output grips below the node with some spacing - _grips.Add(new Rectangle(xCenter - gripHalfWidth, Height + _gripHeight, gripHalfWidth * 2, _gripHeight * 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)); + } } } @@ -459,19 +497,38 @@ public PointF[] GetNodeScreenPosition(ConnectionMode mode, bool force = false) return result; // Handle dynamic input/output modes - int gripIndex = (int)mode; + int gripIndex; + int modeInt = (int)mode; - // For inputs: mode = InputBase + inputIndex, grip index = 1 + inputIndex - if (gripIndex >= (int)ConnectionMode.InputBase && gripIndex < (int)ConnectionMode.OutputBase) + // 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) { - int inputIndex = gripIndex - (int)ConnectionMode.InputBase; - gripIndex = 1 + inputIndex; // Skip Previous grip (index 0) + gripIndex = Node.Inputs.Count + 1 + Node.Outputs.Count; } - // For outputs: mode = OutputBase + outputIndex, grip index = 1 + inputCount + outputIndex - else if (gripIndex >= (int)ConnectionMode.OutputBase) + else if (mode == ConnectionMode.Else) { - int outputIndex = gripIndex - (int)ConnectionMode.OutputBase; - gripIndex = 1 + Node.Inputs.Count + outputIndex; + gripIndex = Node.Inputs.Count + 1 + Node.Outputs.Count + 1; + } + else + { + gripIndex = Node.Inputs.Count; // Default to Previous } if (_grips.Count < gripIndex + 1) @@ -745,31 +802,35 @@ protected override void OnMouseLeave(EventArgs e) /// private ConnectionMode GripToConnectionMode(int gripIndex) { - // Grip 0 is always Previous - if (gripIndex == 0) - return ConnectionMode.Previous; - - // If Node is not initialized yet, return Previous as default + // If Node is not initialized yet, default to Previous if (Node == null) return ConnectionMode.Previous; - // Grips 1 to inputCount are inputs - if (gripIndex >= 1 && gripIndex <= Node.Inputs.Count) + 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) { - int inputIndex = gripIndex - 1; - return (ConnectionMode)((int)ConnectionMode.InputBase + inputIndex); + return (ConnectionMode)((int)ConnectionMode.InputBase + gripIndex); } - // Grips after inputs are outputs - if (gripIndex > Node.Inputs.Count && gripIndex <= Node.Inputs.Count + Node.Outputs.Count) + // 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 - 1 - Node.Inputs.Count; + int outputIndex = gripIndex - outputGripStart; return (ConnectionMode)((int)ConnectionMode.OutputBase + outputIndex); } - // For conditional nodes, Next and Else are added by VisibleNodeCondition - // Grip indices continue after outputs - int nextElseOffset = 1 + Node.Inputs.Count + Node.Outputs.Count; + // 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) From fa276c785a682278f4d1f8dd224a4e71323e17c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:16:13 +0000 Subject: [PATCH 16/16] Fix input labels not visible - position above node instead of below Co-authored-by: TrainWrack <120750885+TrainWrack@users.noreply.github.com> --- .../TombLib.Forms/Controls/VisualScripting/NodeEditor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index a9560ea43..79b3ded21 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -1047,7 +1047,7 @@ private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node) using (var brush = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(0.3f))) { - // Draw INPUT labels at TOP, positioned like then/else labels + // 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++) @@ -1059,10 +1059,10 @@ private void DrawInputOutputLabels(PaintEventArgs e, VisibleNodeBase node) var inputMode = (ConnectionMode)((int)ConnectionMode.InputBase + i); var inputPoint = node.GetNodeScreenPosition(inputMode); - // Position label below the grip, like then/else + // 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 * 0.4f); + int rectY = node.Location.Y - (int)(size.Height * 1.2f); var rect = new Rectangle(rectX, rectY, rectWidth, size.Height);