Flow Control for Kismet

#Kismet
Modified @ 2025-09-15 17:02:08

A few nodes to make flow control easier within Kismet.


Technical Details

Whenever a flow control node is activated for the first time, it recursively searches Loop Body to find the end of the chain (or another flow control node, which it will then instruct to do the same). When a node has multiple outputs, both branches have an end, which causes both ends to be connected back to the flow control node, which leads to undefined behaviour. (You should use the Sequencer node instead if you want to achieve something like this.)

Disclaimer

Open the attached map in hatcord's Modding Resources thread for documentation and examples of all nodes!

LK_Redirector.uc

[RAW] [Download]

class LK_Redirector extends SequenceOp;
 
defaultproperties
{
    ObjName = "=>"
    ObjCategory = "Organization"
    DrawHeight = 1
    DrawWidth = 1
    VariableLinks.Empty
 
    //bCallHandler = false;
    ObjColor = (R=0,G=0,B=0,A=255);
    InputLinks.Empty
    InputLinks(0) = (LinkDesc=" ")
    OutputLinks.Empty
    OutputLinks(0) = (LinkDesc=" ")
}
LK_SeqAct_CallFunction.uc

[RAW] [Download]

class LK_SeqAct_CallFunction extends SequenceAction;
 
var() Name TargetFunctionName; // Target Function must be within this sequence and must exist.
 
var LK_SequenceFunction Func;
 
public event Activated()
{
    if(Func == None)
    {
        FindFunctionDefinition();
 
        if(Func == None)
        {
            ScriptLog("No Function with name \""$String(TargetFunctionName)$"\" could be found! Skipping.", true);
            ForceActivateOutput(0);
            return;
        }
 
        if(VariableLinks.Length != Func.FunctionParams.Length || Func.VariableLinks.Length != Func.FunctionParams.Length)
        {
            ScriptLog("Manual update required for \""$String(Func.FunctionName)$"\"! (Use Tools > Check Map For Errors) Skipping.");
            ForceActivateOutput(0);
            return;
        }
    }
 
    PublishLinkedVariableValues();
 
    Func.Execute(self);
}
 
public function OnFunctionCompleted(LK_SequenceFunction _Func)
{
    AcceptVars(_Func);
    ForceActivateOutput(0);
}
 
private function FindFunctionDefinition()
{
    local Array<SequenceObject> objs;
    local int i;
    GetWorldInfo().GetGameSequence().FindSeqObjectsByClass(class'LK_SequenceFunction', true, objs);
 
    for(i = 0; i < objs.Length; i++)
    {
        if(LK_SequenceFunction(objs[i]).FunctionName == TargetFunctionName)
        {
            Func = LK_SequenceFunction(objs[i]);
            break;
        }
    }
}
 
private function AcceptVars(LK_SequenceFunction Src)
{
    local int i;
 
    for(i = 0; i < VariableLinks.Length; i++)
    {
        if(Func.FunctionParams[i].bIsOutput) class'LK_SequenceFunction'.static.CopySeqVar(Src.VariableLinks[i].LinkedVariables[0], VariableLinks[i].LinkedVariables[0]);
    }
 
    PopulateLinkedVariableValues();
}
 
event CheckForErrors(out Array<string> ErrorMessages)
{
    FindFunctionDefinition();
    if(Func == None)
    {
        ErrorMessages.AddItem("Target function doesn't exist in sequence!");
        return;
    }
 
    class'LK_SequenceFunction'.static.UpdateVariableLinksFor(self, Func.FunctionParams, false);
 
 
    ObjName = "Call Function: "$String(TargetFunctionName);
}
 
defaultproperties
{
    ObjName = "Call Function"
    ObjCategory = "Functions"
 
    ObjColor = (R=138, G=29, B=29, A=255);
 
    VariableLinks.Empty;
 
    bAutoActivateOutputLinks = false; // important!
 
    ObjComment = "A bunch of things are handled automatically when you use Tools > Check Map For Errors."
}
LK_SeqAct_FlowControl.uc

[RAW] [Download]

class LK_SeqAct_FlowControl extends SequenceAction
    abstract;
 
var LK_SeqFlow_Loop UpperNode;
 
public function NotifiedByUpper(LK_SeqFlow_Loop Op)
{
    UpperNode = Op;
}
 
defaultproperties
{
    ObjColor = (R=46,G=46,B=46,A=255)
    ObjCategory = "Flow Control"
    bAutoActivateOutputLinks=false
 
    VariableLinks.Empty;
}
LK_SeqFlow_Break.uc

[RAW] [Download]

class LK_SeqFlow_Break extends LK_SeqAct_FlowControl;
 
public event Activated()
{
    if(UpperNode == None) return;
    UpperNode.ForceActivateInput(UpperNode.BreakInputIdx);
}
 
defaultproperties
{
   // ObjColor = (R=227,G=20,B=20,A=255)
    ObjName = "Break";
    OutputLinks.Empty;
}
LK_SeqFlow_Continue.uc

[RAW] [Download]

class LK_SeqFlow_Continue extends LK_SeqAct_FlowControl;
 
public event Activated()
{
    if(UpperNode == None) return;
    UpperNode.ForceActivateInput(UpperNode.InternalLoopInputIdx);
}
 
defaultproperties
{
    //ObjColor = (R=227,G=20,B=20,A=255)
    ObjCategory = "Flow Control"
    bAutoActivateOutputLinks=false
    OutputLinks.Empty; // Has no outputs, this node signifies the end of the chain
 
    ObjName = "Continue"
 
    VariableLinks.Empty;
}
LK_SeqFlow_For.uc

[RAW] [Download]

class LK_SeqFlow_For extends LK_SeqFlow_Loop;
 
var int Index;
var() int FirstIndex;
var() int LastIndex;
 
public event Activated()
{
    Super.Activated();
 
    // Break / abort is prioritized
    if(InputLinks[BreakInputIdx].bHasImpulse)
    {
        ForceActivateOutput(CompletedOutputIdx);
    }
    else if(InputLinks[MainInputIdx].bHasImpulse)
    {
        if(FirstIndex > LastIndex || LastIndex < FirstIndex || OutputLinks[0].Links.Length == 0)
        {
            ForceActivateOutput(CompletedOutputIdx);
            return;
        }
 
        Index = FirstIndex;
 
        PopulateLinkedVariableValues();
 
        ForceActivateOutput(LoopBodyIdx);
    }
    else if(InputLinks[InternalLoopInputIdx].bHasImpulse)
    {
        if(Index < LastIndex)
        {
            Index++;
            PopulateLinkedVariableValues();
            ForceActivateOutput(LoopBodyIdx);
        }
        else
        {
            ForceActivateOutput(CompletedOutputIdx);
        }
    }
}
 
defaultproperties
{
    ObjName = "For Loop"
    VariableLinks.Empty
    VariableLinks(0) = (LinkDesc="Index", ExpectedType=class'SeqVar_Int', bWriteable=true, PropertyName=Index, MaxVars=1)
    VariableLinks(1) = (LinkDesc="First Index", ExpectedType=class'SeqVar_Int', bWriteable=false, PropertyName=FirstIndex, bHidden=true)
    VariableLinks(2) = (LinkDesc="Last Index", ExpectedType=class'SeqVar_Int', bWriteable=false, PropertyName=LastIndex, bHidden=true)
}
LK_SeqFlow_Foreach.uc

[RAW] [Download]

class LK_SeqFlow_Foreach extends LK_SeqFlow_Loop;
 
var Array<Object> InList;
var int Index;
var Object OutObject;
 
public event Activated()
{
    Super.Activated();
 
    // Break / abort is prioritized
    if(InputLinks[BreakInputIdx].bHasImpulse)
    {
        ForceActivateOutput(CompletedOutputIdx);
    }
    else if(InputLinks[MainInputIdx].bHasImpulse)
    {
        if(InList.Length == 0)
        {
            ForceActivateOutput(CompletedOutputIdx);
            return;
        }
 
        PublishLinkedVariableValues();
 
        Index = 0;
        OutObject = InList[Index];
 
        PopulateLinkedVariableValues();
 
        ForceActivateOutput(LoopBodyIdx);
    }
    else if(InputLinks[InternalLoopInputIdx].bHasImpulse)
    {
        if(Index < InList.Length-1)
        {
            Index++;
            OutObject = InList[Index];
 
            PopulateLinkedVariableValues();
 
            ForceActivateOutput(LoopBodyIdx);
        }
        else
        {
            Index = 0;
            OutObject = None;
 
            PopulateLinkedVariableValues();
 
            ForceActivateOutput(CompletedOutputIdx);
        }
    }
}
 
defaultproperties
{
    ObjName = "Foreach Loop"
    VariableLinks.Empty;
    VariableLinks(1) = (LinkDesc="Index", ExpectedType=class'SeqVar_Int', bWriteable=true, PropertyName=Index, MaxVars=1, bHidden=true)
    VariableLinks(0) = (LinkDesc="List", ExpectedType=class'SeqVar_ObjectList', bWriteable=false, PropertyName=InList, MaxVars=1)
    VariableLinks(2) = (LinkDesc="Object", ExpectedType=class'SeqVar_Object',bWriteable=true, PropertyName=OutObject, MaxVars=1)
}
LK_SeqFlow_Loop.uc

[RAW] [Download]

class LK_SeqFlow_Loop extends LK_SeqAct_FlowControl
    abstract;
 
var const int InternalLoopInputIdx;
var const int MainInputIdx;
var const int CompletedOutputIdx;
var const int LoopBodyIdx;
var const int BreakInputIdx;
var bool bHasProcessedChain;
 
public event Activated()
{
    if(InputLinks[MainInputIdx].bHasImpulse && !bHasProcessedChain)
    {
        ProcessLoopBody();
        bHasProcessedChain = true;
    }
}
 
public function ProcessLoopBody()
{
    local int i;
 
    for(i = 0; i < OutputLinks[LoopBodyIdx].Links.Length; i++)
    {
        RecursiveProcessChain(OutputLinks[LoopBodyIdx].Links[i].LinkedOp, self);
    }
}
 
public function RecursiveProcessChain(SequenceOp Op, LK_SeqFlow_Loop UpperCaller)
{
    local int i, j;
    local SeqOpOutputInputLink l;
 
    if(LK_SeqAct_FlowControl(Op) != None && Op != self)
    {
        LK_SeqAct_FlowControl(Op).NotifiedByUpper(self);
        return;
    }
 
    for(i = 0; i < Op.OutputLinks.Length; i++)
    {
        if(Op.OutputLinks[i].Links.Length == 0)
        {
            l.LinkedOp = UpperCaller;
            l.InputLinkIdx = UpperCaller.InternalLoopInputIdx;
            Op.OutputLinks[i].Links.AddItem(l);
            continue;
        }
 
        // Prevent endless recursion by checking if the links we are going to process link to the upper node
        if(IsOutputLinked(Op, i, UpperCaller)) continue;
 
        for(j = 0; j < Op.OutputLinks[i].Links.Length; j++)
        {
            RecursiveProcessChain(Op.OutputLinks[i].Links[j].LinkedOp, UpperCaller);
        }
    }
}
 
public function bool IsOutputLinked(SequenceOp SrcOp, int OutputLinkIdx, SequenceOp CheckedOp)
{
    local int i;
 
    for(i = 0; i < SrcOp.OutputLinks[OutputLinkIdx].Links.Length; i++)
    {
        if(SrcOp.OutputLinks[OutputLinkIdx].Links[i].LinkedOp == CheckedOp) return true;
    }
 
    return false;
}
 
public function NotifiedByUpper(LK_SeqFlow_Loop Op)
{
    local SeqOpOutputInputLink l;
    local int i;
 
    Super.NotifiedByUpper(Op);
 
    ProcessLoopBody();
    bHasProcessedChain = true;
 
    if(OutputLinks[CompletedOutputIdx].Links.Length == 0)
    {
        l.LinkedOp = Op;
        l.InputLinkIdx = Op.InternalLoopInputIdx;
        OutputLinks[CompletedOutputIdx].Links.AddItem(l);
    }
    else
    {
        for(i = 0; i < OutputLinks[CompletedOutputIdx].Links.Length; i++)
        {
            RecursiveProcessChain(OutputLinks[CompletedOutputIdx].Links[i].LinkedOp, UpperNode);
        }
    }
}
 
defaultproperties
{
    OutputLinks.Empty
    OutputLinks(0) = (LinkDesc="Loop Body")
    OutputLinks(1) = (LinkDesc="Completed")
    LoopBodyIdx = 0;
    CompletedOutputIdx = 1;
 
    InputLinks.Empty;
    InputLinks(0) = (LinkDesc="In")
    InputLinks(1) = (LinkDesc="Loop Input (DO NOT USE)", bHidden=true)
    InputLinks(2) = (LinkDesc="Break", bHidden=true) // Don't connect to this, use the Break node instead. (Or do and experience the pain of kismetti)
    InternalLoopInputIdx = 1;
    MainInputIdx = 0;
    BreakInputIdx = 2;
 
    ObjComment = "Use the Continue and Break nodes for additional control within this scope."
}
LK_SeqFlow_Sequencer.uc

[RAW] [Download]

class LK_SeqFlow_Sequencer extends LK_SeqFlow_Loop;
 
/* Doesnt process the Break input because it would do nothing (there's only one "iteration") */
public event Activated()
{
    Super.Activated();
 
    if(InputLinks[MainInputIdx].bHasImpulse)
    {
        ForceActivateOutput(0 /* First */);
    }
    else if(InputLinks[InternalLoopInputIdx].bHasImpulse)
    {
        ForceActivateOutput(1 /* Second */);
    }
}
 
defaultproperties
{
    ObjName = "Sequencer";
 
    OutputLinks(0) = (LinkDesc="First");
    OutputLinks(1) = (LinkDesc="Second");
    ObjComment = ""
}
LK_SeqFlow_Skip.uc

[RAW] [Download]

// This node immediately ends the upper loop but does NOT call Completed(). Use this node when a node has two outputs that are both activated to prevent a double iteration or early continue from occuring. (For example, the Actor Factory Ex node. You might only want to use Finished and skip Spawned 1.)
class LK_SeqFlow_Skip extends LK_SeqAct_FlowControl;
 
event Activated()
{
    return;
}
 
defaultproperties
{
    ObjCategory = "Flow Control"
    bAutoActivateOutputLinks=false
    OutputLinks.Empty; // Has no outputs, this node signifies the end of the chain
 
    ObjName = "Skip"
 
    VariableLinks.Empty;
}
LK_SeqFlow_While.uc

[RAW] [Download]

class LK_SeqFlow_While extends LK_SeqFlow_Loop;
 
var() bool Condition;
var() int SafetyLimit; // 0 for no safety limit.
var int Counter;
 
public event Activated()
{
    Super.Activated();
 
    // break / abort is prioritized
    if(InputLinks[BreakInputIdx].bHasImpulse)
    {
        ForceActivateOutput(CompletedOutputIdx);
    }
    else if(InputLinks[MainInputIdx].bHasImpulse)
    {
        PublishLinkedVariableValues();
 
        if(!Condition)
        {
            ForceActivateOutput(CompletedOutputIdx);
            return;
        }
 
        Counter = 0;
 
        ForceActivateOutput(LoopBodyIdx);
    }
    else if(InputLinks[InternalLoopInputIdx].bHasImpulse)
    {
        PublishLinkedVariableValues();
        if((Counter < SafetyLimit || SafetyLimit == 0) && Condition)
        {
            Counter++;
            ForceActivateOutput(LoopBodyIdx);
        }
        else
        {
            ForceActivateOutput(CompletedOutputIdx);
        }
    }
}
 
defaultproperties
{
    ObjName = "While Loop"
 
    VariableLinks.Empty
    VariableLinks(0)=(LinkDesc="Condition",ExpectedType=class'SeqVar_Bool',bWriteable=false, PropertyName=Condition)
    SafetyLimit = 100;
}
LK_SequenceFunction.uc

[RAW] [Download]

// Extending from loop has weird, but useful implications for this node.
// Also, be careful with parameters. It essentially boils down to copying values around.
class LK_SequenceFunction extends LK_SeqFlow_Loop;
 
var() Name FunctionName;
 
struct SequenceFunctionParameter
{
    var() const class<SequenceVariable> ParameterType;
    var() const bool bIsOutput;
    var() const string ParameterName;
};
 
var LK_SeqAct_CallFunction Caller;
 
var() Array<SequenceFunctionParameter> FunctionParams;
 
event Activated()
{
    // Function completed
    if(InputLinks[InternalLoopInputIdx].bHasImpulse || InputLinks[BreakInputIdx].bHasImpulse)
    {
        Caller.OnFunctionCompleted(self);
        return;
    }
}
 
public function Execute(LK_SeqAct_CallFunction _Caller)
{
    if(!bHasProcessedChain)
    {
        ProcessLoopBody();
        bHasProcessedChain = true;
    }
 
    Caller = _Caller;
    AcceptVars(Caller);
    ForceActivateOutput(0);
}
 
private function AcceptVars(LK_SeqAct_CallFunction Src)
{
    local int i;
 
    for(i = 0; i < VariableLinks.Length; i++)
    {
        CopySeqVar(Src.VariableLinks[i].LinkedVariables[0], VariableLinks[i].LinkedVariables[0]);
    }
 
    PopulateLinkedVariableValues();
}
 
// shouldn't happen
public function NotifiedByUpper(LK_SeqFlow_Loop Op)
{
    return;
}
 
public event CheckForErrors(out Array<String> ErrorMessages)
{
    ObjName = "Function Definition: "$String(FunctionName);
    ObjComment = String(GetReferenceCount())$" reference(s)";
 
    UpdateVariableLinksFor(self, FunctionParams, true);
}
 
private function int GetReferenceCount()
{
    local int i, count;
    local Array<SequenceObject> objs;
 
    ParentSequence.FindSeqObjectsByClass(class'LK_SeqAct_CallFunction', true, objs);
 
    for(i = 0; i < objs.Length; i++)
    {
        if(LK_SeqAct_CallFunction(objs[i]).TargetFunctionName == FunctionName) count++;
    }
 
    return count;
}
 
// A bit of a stupid solution
public static function bool CopySeqVar(const SequenceVariable A, SequenceVariable B)
{
    if(A == None || B == None) return false;
    if(A.Class != B.Class) return false;
 
    switch(A.Class)
    {
        case class'SeqVar_Int':
            SeqVar_Int(B).IntValue = SeqVar_Int(A).IntValue; break;
        case class'SeqVar_Float':
            SeqVar_Float(B).FloatValue = SeqVar_Float(A).FloatValue; break;
        case class'SeqVar_Bool':
            SeqVar_Bool(B).bValue = SeqVar_Bool(A).bValue; break;
        case class'SeqVar_Object':
        case class'SeqVar_Player':
            SeqVar_Object(B).SetObjectValue(SeqVar_Object(B).GetObjectValue()); break;
        case class'SeqVar_Vector':
            SeqVar_Vector(B).VectValue = SeqVar_Vector(A).VectValue; break;
        case class'SeqVar_ObjectList':
            SeqVar_ObjectList(B).ObjList = SeqVar_ObjectList(A).ObjList; break;
        case class'SeqVar_String':
            SeqVar_String(B).StrValue = SeqVar_String(A).StrValue; break; 
 
        default: break;
    }
 
    return true;
}
 
public static function UpdateVariableLinksFor(SequenceOp Op, const Array<const SequenceFunctionParameter> _FunctionParams, const bool bInvertIsOutput)
{
    local int i;
    local SeqVarLink l;
 
    for(i = 0; i < _FunctionParams.Length; i++)
    {
        if(i < Op.VariableLinks.Length)
        {
            Op.VariableLinks[i].LinkDesc = _FunctionParams[i].ParameterName;
            Op.VariableLinks[i].bWriteable = bInvertIsOutput ? !_FunctionParams[i].bIsOutput : _FunctionParams[i].bIsOutput;
            Op.VariableLinks[i].ExpectedType = _FunctionParams[i].ParameterType;
        }
        else
        {
            l.LinkDesc = _FunctionParams[i].ParameterName;
            l.bWriteable = bInvertIsOutput ? !_FunctionParams[i].bIsOutput : _FunctionParams[i].bIsOutput;
            l.ExpectedType = _FunctionParams[i].ParameterType;
            l.MaxVars = 1;
            l.MinVars = 1;
            Op.VariableLinks.AddItem(l);
        }
    }
 
    // trim to remove any excess
    Op.VariableLinks.Length = _FunctionParams.Length;
 
    Op.bPendingVarConnectorRecalc = true;
    Op.bPendingInputConnectorRecalc = true;
    Op.bPendingOutputConnectorRecalc = true;
}
 
defaultproperties
{
    ObjName = "Function Definition"
    ObjCategory = "Functions"
    ObjColor = (R=138, G=29, B=29, A=255)
 
    InputLinks.Empty;
    InputLinks(0) = (LinkDesc = "INPUT | DO NOT USE", bHidden=true);
    InputLinks(1) = (LinkDesc = "RETURN | DO NOT USE", bHidden=true);
    InternalLoopInputIdx = 0;
    BreakInputIdx = 1; // RETURN
    MainInputIdx = -1; // has none
    LoopBodyIdx = 0;
    CompletedOutputIdx = -1; // has none
    OutputLinks.Empty;
    OutputLinks(0) = (LinkDesc="Function Body")
    VariableLinks.Empty
 
    ObjComment = "Use the Break node to end the function early."
 
    FunctionName = "My Function"
}