
A Hat in Time Script Repository
(v0.5)
Code viewer
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!
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=" ") }
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." }
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; }
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; }
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; }
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) }
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) }
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." }
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 = "" }
// 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; }
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; }
// 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" }