Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6d7ac95
Search improvements vs. flow mainline
LindyHopperGT Jan 12, 2026
cc5b9c8
Flow Debugger Fixes
LindyHopperGT Jan 26, 2026
bd344f0
Merge remote-tracking branch 'upstream/5.x' into FlowDebuggerFixes
LindyHopperGT Jan 26, 2026
8e4fcf6
Reverted this fix for PR
LindyHopperGT Jan 26, 2026
f702cda
Merge remote-tracking branch 'upstream/5.x' into FlowDebuggerFixes
LindyHopperGT Jan 27, 2026
0008bf8
Merge remote-tracking branch 'upstream/5.x' into FlowDebuggerFixes
LindyHopperGT Jan 27, 2026
15fb280
restore changes from other PRs
MothDoctor Jan 27, 2026
ee0bcff
integrated usage of Flow Node as main parameter passed with breakpoin…
MothDoctor Jan 27, 2026
a5df994
Integrations with our version
LindyHopperGT Jan 27, 2026
e28dc7b
Merge branch 'FlowDebuggerFixes' of https://github.com/LindyHopperGT/…
LindyHopperGT Jan 27, 2026
ad4063a
compile fixes
LindyHopperGT Jan 27, 2026
7d4b31f
reverted formatting changes
MothDoctor Jan 28, 2026
1c00344
restored previous class layout
MothDoctor Jan 28, 2026
e94ca04
Merge branch '5.x' into FlowDebuggerFixes
MothDoctor Jan 28, 2026
12bb73c
Merge remote-tracking branch 'upstream/5.x' into FlowDebuggerFixes
LindyHopperGT Jan 28, 2026
a64dc47
Merge branch 'FlowDebuggerFixes' of https://github.com/LindyHopperGT/…
LindyHopperGT Jan 28, 2026
a6cee33
Merge remote-tracking branch 'upstream/5.x' into FlowDebuggerFixes
LindyHopperGT Jan 30, 2026
7fe6198
[Flow] Trigger Outputs deferred while processing an Input Trigger
LindyHopperGT Feb 5, 2026
64140b8
Merge remote-tracking branch 'upstream/5.x' into FlowDeferredTriggerQ…
LindyHopperGT Feb 5, 2026
83368bb
[Flow] DataPin support to AddOns
LindyHopperGT Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors

#include "Asset/FlowDeferredTransitionScope.h"
#include "FlowAsset.h"
#include "Interfaces/FlowExecutionGate.h"

void FFlowDeferredTransitionScope::EnqueueDeferredTrigger(const FFlowDeferredTriggerInput& Entry)
{
check(bIsOpen);

DeferredTriggers.Add(Entry);
}

bool FFlowDeferredTransitionScope::TryFlushDeferredTriggers(UFlowAsset& OwningFlowAsset)
{
// Ensure the scope is closed before beginning flushing
CloseScope();

// Remove and trigger each deferred trigger input
while (!DeferredTriggers.IsEmpty() && !FFlowExecutionGate::IsHalted())
{
const FFlowDeferredTriggerInput Entry = DeferredTriggers[0];
DeferredTriggers.RemoveAt(0, 1, EAllowShrinking::No);

OwningFlowAsset.TriggerInput(Entry.NodeGuid, Entry.PinName, Entry.FromPin);
}

check(DeferredTriggers.IsEmpty() || FFlowExecutionGate::IsHalted());

// Return true if everything flushed without being interrupted by an ExecutionGate
return DeferredTriggers.IsEmpty();
}
211 changes: 195 additions & 16 deletions Source/Flow/Private/FlowAsset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "AddOns/FlowNodeAddOn.h"
#include "Asset/FlowAssetParams.h"
#include "Asset/FlowAssetParamsUtils.h"
#include "Interfaces/FlowExecutionGate.h"
#include "Nodes/FlowNodeBase.h"
#include "Nodes/Graph/FlowNode_CustomInput.h"
#include "Nodes/Graph/FlowNode_CustomOutput.h"
Expand Down Expand Up @@ -645,30 +646,34 @@ bool UFlowAsset::TryUpdateManagedFlowPinsForNode(UFlowNode& FlowNode)
FlowNode.GetAutoInputDataPins(),
FlowNode.GetAutoOutputDataPins());

// Allow the node to auto-generate data pins
FlowNode.AutoGenerateDataPins(WorkingData);
bool bAutoInputDataPinsChanged = false;
bool bAutoOutputDataPinsChanged = false;

// If the auto-generated data pins array changed, it counts as dirty as well
const bool bAutoInputDataPinsChanged = WorkingData.DidAutoInputDataPinsChange();
const bool bAutoOutputDataPinsChanged = WorkingData.DidAutoOutputDataPinsChange();

if (bAutoInputDataPinsChanged || bAutoOutputDataPinsChanged)
if (WorkingData.AutoGenerateDataPinsForFlowNode(FlowNode, bAutoInputDataPinsChanged, bAutoOutputDataPinsChanged))
{
FlowNode.SetFlags(RF_Transactional);
FlowNode.Modify();

// Lock-in the data that changed.
if (bAutoInputDataPinsChanged || bAutoOutputDataPinsChanged)
TArray<FFlowPin> FlowPinsNext;
const int32 LargestPinNum = FMath::Max(WorkingData.AutoInputDataPinsNext.Num(), WorkingData.AutoOutputDataPinsNext.Num());

if (bAutoInputDataPinsChanged)
{
if (bAutoInputDataPinsChanged)
{
FlowNode.SetAutoInputDataPins(WorkingData.AutoInputDataPinsNext);
}
FlowPinsNext.Reserve(LargestPinNum);

if (bAutoOutputDataPinsChanged)
{
FlowNode.SetAutoOutputDataPins(WorkingData.AutoOutputDataPinsNext);
}
WorkingData.BuildNextFlowPinArray(WorkingData.AutoInputDataPinsNext, FlowPinsNext);

FlowNode.SetAutoInputDataPins(FlowPinsNext);
}

if (bAutoOutputDataPinsChanged)
{
FlowPinsNext.Reserve(LargestPinNum);

WorkingData.BuildNextFlowPinArray(WorkingData.AutoOutputDataPinsNext, FlowPinsNext);

FlowNode.SetAutoOutputDataPins(FlowPinsNext);
}

FlowNode.PostEditChange();
Expand Down Expand Up @@ -970,6 +975,9 @@ void UFlowAsset::InitializeInstance(const TWeakObjectPtr<UObject> InOwner, UFlow

void UFlowAsset::DeinitializeInstance()
{
// These should have been flushed in FinishFlow()
check(DeferredTransitionScopes.IsEmpty());

if (IsInstanceInitialized())
{
for (const TPair<FGuid, UFlowNode*>& Node : ObjectPtrDecay(Nodes))
Expand Down Expand Up @@ -1012,6 +1020,11 @@ void UFlowAsset::PreStartFlow()

void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier)
{
if (FFlowExecutionGate::IsHalted())
{
return;
}

PreStartFlow();

if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode())
Expand All @@ -1031,6 +1044,8 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool b
{
FinishPolicy = InFinishPolicy;

CancelAndWarnForUnflushedDeferredTriggers();

// end execution of this asset and all of its nodes
for (UFlowNode* Node : ActiveNodes)
{
Expand All @@ -1052,6 +1067,62 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool b
}
}

void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers()
{
// Aggressively drop any pending deferred triggers — graph is done
// In normal execution these should have been flushed via PopDeferredTransitionScope() in TriggerInputDirect
// In the debugger they should have been flushed by ResumePIE
// Remaining scopes here usually mean:
// - early/abnormal termination (e.g. FinishFlow called from unexpected place)
// - exception/early return before Pop
// - forced deinitialization during active execution (e.g. PIE stop, subsystem cleanup)
if (!DeferredTransitionScopes.IsEmpty())
{
int32 TotalDroppedTriggers = 0;

for (const TSharedPtr<FFlowDeferredTransitionScope>& ScopePtr : DeferredTransitionScopes)
{
if (!ScopePtr.IsValid())
{
continue;
}

const TArray<FFlowDeferredTriggerInput>& Triggers = ScopePtr->GetDeferredTriggers();

if (TotalDroppedTriggers == 0 && !Triggers.IsEmpty())
{
UE_LOG(LogFlow, Warning, TEXT("FlowAsset '%s' is finishing with %d lingering deferred transition scope(s) — dropping them. "
"This is usually unexpected and may indicate a bug or abnormal termination."),
*GetName(), DeferredTransitionScopes.Num());
}

TotalDroppedTriggers += Triggers.Num();

for (const FFlowDeferredTriggerInput& Trigger : Triggers)
{
const UFlowNode* ToNode = GetNode(Trigger.NodeGuid);
const UFlowNode* FromNode = Trigger.FromPin.NodeGuid.IsValid() ? GetNode(Trigger.FromPin.NodeGuid) : nullptr;

UE_LOG(LogFlow, Error,
TEXT(" → Dropped deferred trigger:\n")
TEXT(" To Node: %s (%s)\n")
TEXT(" To Pin: %s\n")
TEXT(" From Node: %s (%s)\n")
TEXT(" From Pin: %s"),
*ToNode->GetName(),
*Trigger.NodeGuid.ToString(),
*Trigger.PinName.ToString(),
*FromNode->GetName(),
*Trigger.FromPin.NodeGuid.ToString(),
*Trigger.FromPin.PinName.ToString()
);
}
}

ClearAllDeferredTriggerScopes();
}
}

bool UFlowAsset::HasStartedFlow() const
{
return RecordedNodes.Num() > 0;
Expand Down Expand Up @@ -1126,6 +1197,34 @@ void UFlowAsset::TriggerCustomOutput(const FName& EventName)
}

void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin)
{
if (ShouldDeferTriggersForDebugger())
{
EnqueueDeferredTrigger(NodeGuid, PinName, FromPin);
}
else if (ShouldUseStandardDeferTriggers())
{
// Defer only if we have an open top scope
if (!DeferredTransitionScopes.IsEmpty() && DeferredTransitionScopes.Top()->IsOpen())
{
EnqueueDeferredTrigger(NodeGuid, PinName, FromPin);
}
else
{
const TSharedPtr<FFlowDeferredTransitionScope> CurScope = PushDeferredTransitionScope();

TriggerInputDirect(NodeGuid, PinName, FromPin);

PopDeferredTransitionScope(CurScope);
}
}
else
{
TriggerInputDirect(NodeGuid, PinName, FromPin);
}
}

void UFlowAsset::TriggerInputDirect(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin)
{
if (UFlowNode* Node = Nodes.FindRef(NodeGuid))
{
Expand All @@ -1139,6 +1238,86 @@ void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const
}
}

bool UFlowAsset::ShouldDeferTriggersForDebugger() const
{
// Halt always takes precedence for debugger correctness
return FFlowExecutionGate::IsHalted();
}

bool UFlowAsset::ShouldUseStandardDeferTriggers() const
{
return UFlowSettings::Get()->bDeferTriggeredOutputsWhileTriggering;
}

TSharedPtr<FFlowDeferredTransitionScope> UFlowAsset::PushDeferredTransitionScope()
{
// Close the former top scope (if any)
if (!DeferredTransitionScopes.IsEmpty())
{
const TSharedPtr<FFlowDeferredTransitionScope>& FormerTop = DeferredTransitionScopes.Top();
FormerTop->CloseScope();
}

// Push a fresh open scope
return DeferredTransitionScopes.Add_GetRef(MakeShared<FFlowDeferredTransitionScope>());
}

bool UFlowAsset::TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr<FFlowDeferredTransitionScope>& ScopeToFlush)
{
if (ScopeToFlush->TryFlushDeferredTriggers(*this))
{
// Remove the exact instance we were holding (handles nested push/pop cases)
DeferredTransitionScopes.RemoveSingle(ScopeToFlush);
return true;
}
else
{
// Flush was interrupted — should only happen due to execution gate halt
check(FFlowExecutionGate::IsHalted());
return false;
}
}

void UFlowAsset::EnqueueDeferredTrigger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin)
{
if (DeferredTransitionScopes.IsEmpty() || !DeferredTransitionScopes.Top()->IsOpen())
{
// This should only occur when halted at an execution gate
check(FFlowExecutionGate::IsHalted());
PushDeferredTransitionScope();
}

// Always enqueue to the current innermost (top) scope
DeferredTransitionScopes.Top()->EnqueueDeferredTrigger(FFlowDeferredTriggerInput{ NodeGuid, PinName, FromPin });
}

bool UFlowAsset::TryFlushAllDeferredTriggerScopes()
{
while (const TSharedPtr<FFlowDeferredTransitionScope> TopScope = GetTopDeferredTransitionScope())
{
if (!TryFlushAndRemoveDeferredTransitionScope(TopScope))
{
break;
}

// Keep flushing until stack is empty or we hit an ExecutionGate halt
}

check(DeferredTransitionScopes.IsEmpty() || FFlowExecutionGate::IsHalted());

return DeferredTransitionScopes.IsEmpty();
}

void UFlowAsset::ClearAllDeferredTriggerScopes()
{
DeferredTransitionScopes.Reset();
}

TSharedPtr<FFlowDeferredTransitionScope> UFlowAsset::GetTopDeferredTransitionScope() const
{
return !DeferredTransitionScopes.IsEmpty() ? DeferredTransitionScopes.Top() : nullptr;
}

void UFlowAsset::FinishNode(UFlowNode* Node)
{
if (ActiveNodes.Contains(Node))
Expand Down
1 change: 1 addition & 0 deletions Source/Flow/Private/FlowSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ UFlowSettings::UFlowSettings(const FObjectInitializer& ObjectInitializer)
, bWarnAboutMissingIdentityTags(true)
, bLogOnSignalDisabled(true)
, bLogOnSignalPassthrough(true)
, bDeferTriggeredOutputsWhileTriggering(true)
, bUseAdaptiveNodeTitles(false)
, DefaultExpectedOwnerClass(UFlowComponent::StaticClass())
{
Expand Down
58 changes: 58 additions & 0 deletions Source/Flow/Private/FlowSubsystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "FlowLogChannels.h"
#include "FlowSave.h"
#include "FlowSettings.h"
#include "Interfaces/FlowExecutionGate.h"
#include "Nodes/Graph/FlowNode_SubGraph.h"

#include "Engine/GameInstance.h"
Expand Down Expand Up @@ -159,6 +160,63 @@ void UFlowSubsystem::FinishAllRootFlows(UObject* Owner, const EFlowFinishPolicy
}
}

bool UFlowSubsystem::TryFlushAllDeferredTriggerScopes()
{
// Flush deferred triggers on all active runtime instances.
// Flush order follows InstancedTemplates iteration + per-template ActiveInstances.
// This provides reasonable per-asset FIFO but is not a strict global FIFO across assets.
// A more precise global queue could be implemented later if cross-asset ordering becomes critical.
const TArray<UFlowAsset*> CapturedInstancedTemplates = InstancedTemplates;
for (UFlowAsset* Template : CapturedInstancedTemplates)
{
if (!IsValid(Template))
{
continue;
}

for (UFlowAsset* Instance : Template->GetActiveInstances())
{
if (FFlowExecutionGate::IsHalted())
{
break;
}

if (IsValid(Instance))
{
const bool bFlushed = Instance->TryFlushAllDeferredTriggerScopes();

// The only case where we allow a flush to stop before completing
// is if we hit an execution gate halt
check(bFlushed || FFlowExecutionGate::IsHalted());
}
}
}

// The only case where we allow a flush to stop before completing
// is if we hit an execution gate halt
const bool bCompletedFlushAll = !FFlowExecutionGate::IsHalted();
return bCompletedFlushAll;
}

void UFlowSubsystem::ClearAllDeferredTriggerScopes()
{
for (UFlowAsset* Template : InstancedTemplates)
{
if (!IsValid(Template))
{
continue;
}

for (UFlowAsset* Instance : Template->GetActiveInstances())
{
if (IsValid(Instance))
{
Instance->ClearAllDeferredTriggerScopes();
}
}
}
}

UFlowAsset* UFlowSubsystem::CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString& SavedInstanceName, const bool bPreloading /* = false */)
{
UFlowAsset* NewInstance = nullptr;
Expand Down
Loading