From 6d7ac95b2fdce4b304527876afd224ffdae624b0 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:57:16 -0800 Subject: [PATCH 01/10] Search improvements vs. flow mainline --- .../Nodes/Graph/FlowNode_FormatText.cpp | 6 +- .../Private/Asset/FlowObjectDiff.cpp | 2 +- Source/FlowEditor/Private/Asset/SFlowDiff.cpp | 51 +- Source/FlowEditor/Private/Find/FindInFlow.cpp | 1007 +++++++++++++---- .../Private/Find/SFindInFlowFilterPopup.cpp | 145 +++ .../FlowEditor/Private/FlowEditorModule.cpp | 41 + .../Private/Graph/Widgets/SFlowGraphNode.cpp | 4 +- Source/FlowEditor/Public/Find/FindInFlow.h | 150 ++- .../FlowEditor/Public/Find/FindInFlowEnums.h | 48 + .../Public/Find/SFindInFlowFilterPopup.h | 42 + Source/FlowEditor/Public/FlowEditorModule.h | 9 +- .../Public/Graph/FlowGraphEditorSettings.h | 10 + .../FlowEditor/Public/Graph/FlowGraphSchema.h | 3 + 13 files changed, 1210 insertions(+), 308 deletions(-) create mode 100644 Source/FlowEditor/Private/Find/SFindInFlowFilterPopup.cpp create mode 100644 Source/FlowEditor/Public/Find/FindInFlowEnums.h create mode 100644 Source/FlowEditor/Public/Find/SFindInFlowFilterPopup.h diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp index e6564105d..13c055433 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp @@ -7,6 +7,8 @@ #define LOCTEXT_NAMESPACE "FlowNode_FormatText" +const FName UFlowNode_FormatText::OUTPIN_TextOutput("Formatted Text"); + UFlowNode_FormatText::UFlowNode_FormatText(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { @@ -15,12 +17,12 @@ UFlowNode_FormatText::UFlowNode_FormatText(const FObjectInitializer& ObjectIniti NodeDisplayStyle = FlowNodeStyle::Terminal; #endif - OutputPins.Add(FFlowPin(TEXT("Formatted Text"), FFlowPinType_Text::GetPinTypeNameStatic())); + OutputPins.Add(FFlowPin(OUTPIN_TextOutput, FFlowPinType_Text::GetPinTypeNameStatic())); } FFlowDataPinResult UFlowNode_FormatText::TrySupplyDataPin_Implementation(FName PinName) const { - if (PinName == TEXT("Formatted Text")) + if (PinName == OUTPIN_TextOutput) { FText FormattedText; const EFlowDataPinResolveResult FormatResult = TryResolveFormatText(PinName, FormattedText); diff --git a/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp b/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp index 270eab9ac..dd94960d2 100644 --- a/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp +++ b/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp @@ -72,7 +72,7 @@ void FFlowObjectDiff::DiffProperties(TArray& OutProperty if (OldDetailsView.IsValid() && NewDetailsView.IsValid()) { static constexpr bool bSortByDisplayOrder = true; - //OldDetailsView->DiffAgainst(*NewDetailsView.Get(), OutPropertyDiffsArray, bSortByDisplayOrder); + OldDetailsView->DiffAgainst(*NewDetailsView.Get(), OutPropertyDiffsArray, bSortByDisplayOrder); } } diff --git a/Source/FlowEditor/Private/Asset/SFlowDiff.cpp b/Source/FlowEditor/Private/Asset/SFlowDiff.cpp index 96540b6dc..2b1be2976 100644 --- a/Source/FlowEditor/Private/Asset/SFlowDiff.cpp +++ b/Source/FlowEditor/Private/Asset/SFlowDiff.cpp @@ -1,9 +1,10 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "Asset/SFlowDiff.h" -#include "Asset/FlowDiffControl.h" +#include "Asset/FlowDiffControl.h" #include "FlowAsset.h" +#include "Graph/Nodes/FlowGraphNode.h" #include "EdGraphUtilities.h" #include "Editor.h" @@ -38,11 +39,11 @@ static int32 GetCurrentIndex(SListView> const& Lis const TArray>& Selected = ListView.GetSelectedItems(); if (Selected.Num() == 1) { - for (const TSharedPtr& Diff : ListViewSource) + for (int32 Index = 0; Index < ListViewSource.Num(); ++Index) { - if (Diff == Selected[0]) + if (ListViewSource[Index] == Selected[0]) { - return 0; + return Index; } } } @@ -52,23 +53,21 @@ static int32 GetCurrentIndex(SListView> const& Lis void FlowDiffUtils::SelectNextRow(SListView>& ListView, const TArray>& ListViewSource) { const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); - if (CurrentIndex == ListViewSource.Num() - 1) + const int32 NextIndex = CurrentIndex + 1; + if (ListViewSource.IsValidIndex(NextIndex)) { - return; + ListView.SetSelection(ListViewSource[NextIndex]); } - - ListView.SetSelection(ListViewSource[CurrentIndex + 1]); } void FlowDiffUtils::SelectPrevRow(SListView>& ListView, const TArray>& ListViewSource) { const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); - if (CurrentIndex == 0) + const int32 PrevIndex = CurrentIndex - 1; + if (ListViewSource.IsValidIndex(PrevIndex)) { - return; + ListView.SetSelection(ListViewSource[PrevIndex]); } - - ListView.SetSelection(ListViewSource[CurrentIndex - 1]); } bool FlowDiffUtils::HasNextDifference(const SListView>& ListView, const TArray>& ListViewSource) @@ -538,16 +537,28 @@ void FFlowDiffPanel::GeneratePanel(UEdGraph* Graph, TSharedPtr(); - GraphEditorCommands->MapAction(FGenericCommands::Get().Copy, - FExecuteAction::CreateRaw(this, &FFlowDiffPanel::CopySelectedNodes), - FCanExecuteAction::CreateRaw(this, &FFlowDiffPanel::CanCopyNodes) + GraphEditorCommands->MapAction( + FGenericCommands::Get().Copy, + FExecuteAction::CreateRaw(this, &FFlowDiffPanel::CopySelectedNodes), + FCanExecuteAction::CreateRaw(this, &FFlowDiffPanel::CanCopyNodes) ); } @@ -683,14 +694,14 @@ void SFlowDiff::HandleGraphChanged(const FString& GraphPath) const TAttribute FocusedDiffResult = TAttribute::CreateLambda( [this, RealDifferencesStartIndex]() { - int32 FocusedDiffResult = INDEX_NONE; + int32 FocusedIndex = INDEX_NONE; if (RealDifferencesStartIndex != INDEX_NONE) { - FocusedDiffResult = DiffTreeView::CurrentDifference(DifferencesTreeView.ToSharedRef(), RealDifferences) - RealDifferencesStartIndex; + FocusedIndex = DiffTreeView::CurrentDifference(DifferencesTreeView.ToSharedRef(), RealDifferences) - RealDifferencesStartIndex; } // find selected index in all the graphs, and subtract the index of the first entry in this graph - return FocusedDiffResult; + return FocusedIndex; }); // only regenerate PanelOld if the old graph has changed @@ -934,4 +945,4 @@ void SFlowDiff::OnModeChanged(const FName& InNewViewMode) const UpdateTopSectionVisibility(InNewViewMode); } -#undef LOCTEXT_NAMESPACE +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Find/FindInFlow.cpp b/Source/FlowEditor/Private/Find/FindInFlow.cpp index d098da8d5..648728c78 100644 --- a/Source/FlowEditor/Private/Find/FindInFlow.cpp +++ b/Source/FlowEditor/Private/Find/FindInFlow.cpp @@ -2,14 +2,20 @@ #include "Find/FindInFlow.h" #include "Asset/FlowAssetEditor.h" +#include "Find/SFindInFlowFilterPopup.h" #include "Graph/FlowGraphEditor.h" +#include "Graph/FlowGraphEditorSettings.h" #include "Graph/FlowGraphUtils.h" #include "Graph/Nodes/FlowGraphNode.h" - #include "FlowAsset.h" +#include "FlowEditorModule.h" #include "Nodes/FlowNode.h" +#include "Nodes/FlowNodeBase.h" +#include "AddOns/FlowNodeAddOn.h" #include "Nodes/Graph/FlowNode_SubGraph.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/ARFilter.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" #include "Framework/Application/SlateApplication.h" @@ -22,16 +28,22 @@ #include "Layout/WidgetPath.h" #include "Math/Color.h" #include "Misc/Attribute.h" +#include "Misc/EnumRange.h" +#include "Misc/ScopedSlowTask.h" #include "SlotBase.h" +#include "Subsystems/AssetEditorSubsystem.h" #include "Styling/AppStyle.h" #include "Styling/SlateColor.h" #include "Templates/Casts.h" #include "Types/SlateStructs.h" #include "UObject/Class.h" #include "UObject/ObjectPtr.h" +#include "UObject/TopLevelAssetPath.h" #include "Widgets/Images/SImage.h" -#include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SSearchBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Input/SSpinBox.h" +#include "Widgets/Input/SButton.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/SBoxPanel.h" @@ -39,22 +51,58 @@ #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/STableRow.h" -class ITableRow; -class SWidget; -struct FSlateBrush; - #define LOCTEXT_NAMESPACE "FindInFlow" +////////////////////////////////////////////////////////////////////////// +// FFindInFlowCache + +TMap, TMap>> FFindInFlowCache::CategoryStringCache; + +void FFindInFlowCache::OnFlowAssetChanged(UFlowAsset& ChangedFlowAsset) +{ + TArray> EntriesToRemove; + + for (const auto& KV : CategoryStringCache) + { + const TWeakObjectPtr& EdNodePtr = KV.Key; + + UEdGraphNode* EdNode = EdNodePtr.Get(); + + if (!IsValid(EdNode)) + { + EntriesToRemove.Add(EdNodePtr); + + continue; + } + + UEdGraph* EdGraph = ChangedFlowAsset.GetGraph(); + if (EdGraph->Nodes.Contains(EdNode)) + { + EntriesToRemove.Add(EdNodePtr); + } + } + + for (const TWeakObjectPtr& EdNodePtr : EntriesToRemove) + { + CategoryStringCache.Remove(EdNodePtr); + } +} + ////////////////////////////////////////////////////////////////////////// // FFindInFlowResult -FFindInFlowResult::FFindInFlowResult(const FString& InValue) - : Value(InValue), GraphNode(nullptr) +FFindInFlowResult::FFindInFlowResult(const FString& InValue, UFlowAsset* InOwningFlowAsset) + : Value(InValue) + , OwningFlowAsset(InOwningFlowAsset) { } -FFindInFlowResult::FFindInFlowResult(const FString& InValue, TSharedPtr& InParent, UEdGraphNode* InNode, bool bInIsSubGraphNode) - : Value(InValue), GraphNode(InNode), Parent(InParent), bIsSubGraphNode(bInIsSubGraphNode) +FFindInFlowResult::FFindInFlowResult(const FString& InValue, TSharedPtr InParent, UEdGraphNode* InNode, bool bInIsSubGraphNode, UFlowAsset* InOwningFlowAsset) + : Value(InValue) + , GraphNode(InNode) + , OwningFlowAsset(InOwningFlowAsset) + , Parent(InParent) + , bIsSubGraphNode(bInIsSubGraphNode) { } @@ -68,50 +116,49 @@ TSharedRef FFindInFlowResult::CreateIcon() const .ColorAndOpacity(IconColor); } -FReply FFindInFlowResult::OnClick(TWeakPtr FlowAssetEditorPtr, TSharedPtr Root) +FReply FFindInFlowResult::OnClick(TWeakPtr FlowAssetEditorPtr) { - if (FlowAssetEditorPtr.IsValid() && GraphNode.IsValid()) + if (GraphNode.IsValid()) { - if (Parent.IsValid() && !bIsSubGraphNode) - { - FlowAssetEditorPtr.Pin()->JumpToNode(GraphNode.Get()); - } - else + if (UEdGraph* Graph = GraphNode->GetGraph()) { - FlowAssetEditorPtr.Pin()->JumpToNode(Parent.Pin()->GraphNode.Get()); + if (UFlowAsset* Asset = Cast(Graph->GetOuter())) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(Asset); + if (TSharedPtr Editor = FFlowGraphUtils::GetFlowAssetEditor(Graph)) + { + Editor->JumpToNode(GraphNode.Get()); + } + } } } - + else if (OwningFlowAsset.IsValid()) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(OwningFlowAsset.Get()); + } return FReply::Handled(); } -FReply FFindInFlowResult::OnDoubleClick(TSharedPtr Root) const +FReply FFindInFlowResult::OnDoubleClick() const { - if (!Parent.IsValid() || !bIsSubGraphNode) + if (bIsSubGraphNode && Parent.IsValid()) { - return FReply::Handled(); - } - const UFlowGraphNode* ParentGraphNode = Cast(Parent.Pin()->GraphNode); - if (!ParentGraphNode || !ParentGraphNode->GetFlowNodeBase()) - { - return FReply::Handled(); - } - - if (UFlowNode* FlowNode = Cast(ParentGraphNode->GetFlowNodeBase())) - { - if (UObject* AssetToEdit = FlowNode->GetAssetToEdit()) + if (const UFlowGraphNode* ParentNode = Cast(Parent.Pin()->GraphNode.Get())) { - UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); - if (AssetEditorSubsystem->OpenEditorForAsset(AssetToEdit)) + if (UFlowNode_SubGraph* SubGraph = Cast(ParentNode->GetFlowNodeBase())) { - if (const TSharedPtr FlowAssetEditor = FFlowGraphUtils::GetFlowAssetEditor(GraphNode->GetGraph())) + if (UObject* Target = SubGraph->GetAssetToEdit()) { - FlowAssetEditor->JumpToNode(GraphNode.Get()); + GEditor->GetEditorSubsystem()->OpenEditorForAsset(Target); + if (TSharedPtr Editor = FFlowGraphUtils::GetFlowAssetEditor(GraphNode->GetGraph())) + { + Editor->JumpToNode(GraphNode.Get()); + } } } } } - + return FReply::Handled(); } @@ -127,304 +174,789 @@ FString FFindInFlowResult::GetDescriptionText() const FString FFindInFlowResult::GetCommentText() const { - if (GraphNode.IsValid()) - { - return GraphNode->NodeComment; - } - - return FString(); + return GraphNode.IsValid() ? GraphNode->NodeComment : FString(); } FString FFindInFlowResult::GetNodeTypeText() const { - if (GraphNode.IsValid()) + if (!GraphNode.IsValid()) { - FString NodeClassName; - const UFlowGraphNode* FlowGraphNode = Cast(GraphNode.Get()); - if (FlowGraphNode && FlowGraphNode->GetFlowNodeBase()) - { - NodeClassName = FlowGraphNode->GetFlowNodeBase()->GetClass()->GetName(); - } - else - { - NodeClassName = GraphNode->GetClass()->GetName(); - } - const int32 Pos = NodeClassName.Find("_"); - if (Pos == INDEX_NONE) - { - return NodeClassName; - } - else + return FString(); + } + + if (const UFlowGraphNode* FlowGraphNode = Cast(GraphNode.Get())) + { + if (UFlowNodeBase* Base = FlowGraphNode->GetFlowNodeBase()) { - return NodeClassName.RightChop(Pos + 1); + return Base->GetClass()->GetDisplayNameText().ToString(); } } - return FString(); + return GraphNode->GetClass()->GetDisplayNameText().ToString(); } FText FFindInFlowResult::GetToolTipText() const { - FString ToolTipStr = TEXT("Click to focus on nodes."); - if (bIsSubGraphNode) + FString Tip = GetNodeTypeText() + TEXT("\n") + GetDescriptionText(); + + if (!GetCommentText().IsEmpty()) { - ToolTipStr += TEXT("\nDouble click to focus on subgraph nodes"); + Tip += TEXT("\n") + GetCommentText(); } - return FText::FromString(ToolTipStr); + + if (!MatchedPropertySnippet.IsEmpty()) + { + Tip += TEXT("\n\nMatched: ") + MatchedPropertySnippet; + } + + return FText::FromString(Tip); +} + +FText FFindInFlowResult::GetMatchedSnippet() const +{ + return FText::FromString(MatchedPropertySnippet); +} + +FText FFindInFlowResult::GetMatchedCategoriesText() const +{ + if (MatchedFlags == EFlowSearchFlags::None) + { + return FText::GetEmpty(); + } + + TArray DisplayNames; + + for (EFlowSearchFlags Flag : MakeFlagsRange(EFlowSearchFlags::All)) + { + if (EnumHasAnyFlags(MatchedFlags, Flag)) + { + FText DisplayName = UEnum::GetDisplayValueAsText(Flag); + if (!DisplayName.IsEmpty()) + { + DisplayNames.Add(DisplayName); + } + } + } + + if (DisplayNames.Num() == 0) + { + return FText::GetEmpty(); + } + + return FText::Join(FText::FromString(TEXT(", ")), DisplayNames); } ////////////////////////////////////////////////////////////////////////// // SFindInFlow -void SFindInFlow::Construct( const FArguments& InArgs, TSharedPtr InFlowAssetEditor) +void SFindInFlow::Construct(const FArguments& InArgs, TSharedPtr InFlowAssetEditor) { FlowAssetEditorPtr = InFlowAssetEditor; + SearchResults.Setup(); + + // Load INI settings + const UFlowGraphEditorSettings* Settings = UFlowGraphEditorSettings::Get(); + if (ensure(Settings)) + { + MaxSearchDepth = Settings->DefaultMaxSearchDepth; + SearchFlags = static_cast(Settings->DefaultSearchFlags); + } - this->ChildSlot + // Populate scope options + FLOW_ASSERT_ENUM_MAX(EFlowSearchScope, 3); + for (EFlowSearchScope Scope : TEnumRange()) + { + if (FlowEnum::IsValidEnumValue(Scope)) + { + ScopeOptionList.Add(MakeShareable(new EFlowSearchScope(Scope))); + } + } + SelectedScopeOption = ScopeOptionList[0]; + + SAssignNew(SearchTextField, SSearchBox) + .OnTextCommitted(this, &SFindInFlow::OnSearchTextCommitted); + + SAssignNew(SearchButton, SButton) + .Text(LOCTEXT("SearchButton", "Search")) + .OnClicked(this, &SFindInFlow::OnSearchButtonClicked); + + SAssignNew(MaxDepthSpinBox, SSpinBox) + .MinValue(0) + .MaxValue(10) + .Value(MaxSearchDepth) + .OnValueChanged(this, &SFindInFlow::OnMaxDepthChanged) + .ToolTipText(LOCTEXT("MaxDepthTooltip", "Maximum recursion depth when searching inside objects")); + + SAssignNew(TreeView, STreeViewType) + .TreeItemsSource(&SearchResults.ItemsFound) + .OnGenerateRow(this, &SFindInFlow::OnGenerateRow) + .OnGetChildren(this, &SFindInFlow::OnGetChildren) + .OnSelectionChanged(this, &SFindInFlow::OnTreeSelectionChanged) + .OnMouseButtonDoubleClick(this, &SFindInFlow::OnTreeSelectionDoubleClicked); + + ChildSlot [ SNew(SVerticalBox) - +SVerticalBox::Slot() - .AutoHeight() - [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .FillWidth(1) - [ - SAssignNew(SearchTextField, SSearchBox) - .HintText(LOCTEXT("FlowEditorSearchHint", "Enter text to find nodes...")) - .OnTextChanged(this, &SFindInFlow::OnSearchTextChanged) - .OnTextCommitted(this, &SFindInFlow::OnSearchTextCommitted) - ] - +SHorizontalBox::Slot() - .Padding(10,0,5,0) - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(LOCTEXT("FlowEditorSubGraphSearchText", "Find In SubGraph ")) - ] - +SHorizontalBox::Slot() - .AutoWidth() + + SVerticalBox::Slot() + .AutoHeight() [ - SNew(SCheckBox) - .OnCheckStateChanged(this, &SFindInFlow::OnFindInSubGraphStateChanged) - .ToolTipText(LOCTEXT("FlowEditorSubGraphSearchHint", "Checkin means search also in sub graph.")) + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .VAlign(VAlign_Center) + [ + SearchTextField.ToSharedRef() + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SearchButton.ToSharedRef() + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) + .Text(LOCTEXT("FiltersLabel", "Filters:")) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(SButton) + .ButtonStyle(FAppStyle::Get(), "HoverHintOnly") + .ToolTipText(LOCTEXT("EditFiltersTooltip", "Edit search filters")) + .OnClicked_Lambda([this]() + { + FFindInFlowApplyDelegate OnSaveAsDefault = FFindInFlowApplyDelegate::CreateLambda([this](EFlowSearchFlags Flags) + { + if (UFlowGraphEditorSettings* Settings = UFlowGraphEditorSettings::Get()) + { + Settings->DefaultSearchFlags = static_cast(Flags); + Settings->SaveConfig(); + } + }); + + TSharedRef FilterPopup = SNew(SFindInFlowFilterPopup) + .OnApply(FFindInFlowApplyDelegate::CreateLambda([this](EFlowSearchFlags NewSearchFlags) + { + SearchFlags = NewSearchFlags; + + InitiateSearch(); + })) + .OnSaveAsDefault(OnSaveAsDefault) + .InitialFlags(SearchFlags); + + FSlateApplication::Get().PushMenu( + AsShared(), + FWidgetPath(), + FilterPopup, + FSlateApplication::Get().GetCursorPos(), + FPopupTransitionEffect::ContextMenu); + + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text_Lambda([this]() + { + int32 ActiveCount = FMath::CountBits(static_cast(SearchFlags)); + return FText::Format(LOCTEXT("ActiveFilters", "{0} Active"), FText::AsNumber(ActiveCount)); + }) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(SComboBox>) + .OptionsSource(&ScopeOptionList) + .OnGenerateWidget(this, &SFindInFlow::GenerateScopeWidget) + .OnSelectionChanged(this, &SFindInFlow::OnScopeChanged) + [ + SNew(STextBlock).Text(this, &SFindInFlow::GetCurrentScopeText) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) + .Text(LOCTEXT("MaxDepthLabel", "Max Depth:")) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + MaxDepthSpinBox.ToSharedRef() + ] ] - ] - +SVerticalBox::Slot() - .FillHeight(1.0f) - .Padding(0.f, 4.f, 0.f, 0.f) - [ - SNew(SBorder) - .BorderImage(FAppStyle::GetBrush("Menu.Background")) + + SVerticalBox::Slot() + .FillHeight(1.0f) [ - SAssignNew(TreeView, STreeViewType) - .TreeItemsSource(&ItemsFound) - .OnGenerateRow(this, &SFindInFlow::OnGenerateRow) - .OnGetChildren(this, &SFindInFlow::OnGetChildren) - .OnSelectionChanged(this, &SFindInFlow::OnTreeSelectionChanged) - .OnMouseButtonDoubleClick(this, &SFindInFlow::OnTreeSelectionDoubleClicked) - .SelectionMode(ESelectionMode::Multi) + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [ + TreeView.ToSharedRef() + ] ] - ] ]; } void SFindInFlow::FocusForUse() const { - // NOTE: Careful, GeneratePathToWidget can be reentrant in that it can call visibility delegates and such - FWidgetPath FilterTextBoxWidgetPath; - FSlateApplication::Get().GeneratePathToWidgetUnchecked(SearchTextField.ToSharedRef(), FilterTextBoxWidgetPath); - - // Set keyboard focus directly - FSlateApplication::Get().SetKeyboardFocus(FilterTextBoxWidgetPath, EFocusCause::SetDirectly); + if (SearchTextField.IsValid()) + { + FSlateApplication::Get().SetKeyboardFocus(SearchTextField.ToSharedRef()); + SearchTextField->SelectAllText(); + } } void SFindInFlow::OnSearchTextChanged(const FText& Text) { SearchValue = Text.ToString(); - +} + +void SFindInFlow::OnSearchTextCommitted(const FText& Text, ETextCommit::Type) +{ + SearchValue = Text.ToString(); + + InitiateSearch(); +} + +FReply SFindInFlow::OnSearchButtonClicked() +{ InitiateSearch(); + + return FReply::Handled(); +} + +void SFindInFlow::OnScopeChanged(TSharedPtr NewSelection, ESelectInfo::Type) +{ + SelectedScopeOption = NewSelection; + SearchScope = *NewSelection; } -void SFindInFlow::OnSearchTextCommitted(const FText& Text, ETextCommit::Type CommitType) +void SFindInFlow::OnMaxDepthChanged(int32 NewDepth) { - OnSearchTextChanged(Text); + MaxSearchDepth = NewDepth; + + // Save to INI + if (UFlowGraphEditorSettings* Settings = UFlowGraphEditorSettings::Get()) + { + Settings->DefaultMaxSearchDepth = NewDepth; + Settings->SaveConfig(); + } +} + +TSharedRef SFindInFlow::GenerateScopeWidget(TSharedPtr Item) const +{ + return SNew(STextBlock) + .Text(UEnum::GetDisplayValueAsText(*Item.Get())); +} + +FText SFindInFlow::GetCurrentScopeText() const +{ + return UEnum::GetDisplayValueAsText(*SelectedScopeOption.Get()); } void SFindInFlow::InitiateSearch() { + FFlowEditorModule* FlowEditorModule = &FModuleManager::LoadModuleChecked("FlowEditor"); + if (ensure(FlowEditorModule)) + { + FlowEditorModule->RegisterForAssetChanges(); + } + + SearchResults.Reset(); + + HighlightText = FText::FromString(SearchValue); + TreeView->RequestTreeRefresh(); + + if (SearchValue.IsEmpty()) + { + return; + } + TArray Tokens; SearchValue.ParseIntoArray(Tokens, TEXT(" "), true); + for (FString& Token : Tokens) + { + Token = Token.ToUpper(); + } + + TSharedPtr Editor = FlowAssetEditorPtr.Pin(); + if (!Editor.IsValid()) + { + return; + } - for (auto It(ItemsFound.CreateIterator()); It; ++It) + UFlowAsset* CurrentAsset = Editor->GetFlowAsset(); + if (!CurrentAsset || !CurrentAsset->GetGraph()) { - TreeView->SetItemExpansion(*It, false); + return; } - ItemsFound.Empty(); - if (Tokens.Num() > 0) + + const TSubclassOf CurrentAssetClass = CurrentAsset->GetClass(); + + constexpr int32 Depth = 0; + + switch (SearchScope) { - HighlightText = FText::FromString(SearchValue); - MatchTokens(Tokens); + case EFlowSearchScope::ThisAssetOnly: + { + FSearchResult AssetRoot = MakeShareable(new FFindInFlowResult(CurrentAsset->GetName(), CurrentAsset)); + ProcessAsset(CurrentAsset, AssetRoot, Tokens, Depth); + + if (AssetRoot->Children.Num() > 0) + { + SearchResults.ItemsFound.Add(AssetRoot); + + // Auto-expand the current asset's results + TreeView->SetItemExpansion(AssetRoot, true); + } + } + break; + + case EFlowSearchScope::AllOfThisType: + case EFlowSearchScope::AllFlowAssets: + { + FAssetRegistryModule& Registry = FModuleManager::LoadModuleChecked("AssetRegistry"); + TArray Assets; + FARFilter Filter; + Filter.bRecursiveClasses = true; + + if (SearchScope == EFlowSearchScope::AllFlowAssets) + { + Filter.ClassPaths.Add(FTopLevelAssetPath(UFlowAsset::StaticClass()->GetClassPathName())); + } + else + { + Filter.ClassPaths.Add(FTopLevelAssetPath(CurrentAsset->GetClass()->GetClassPathName())); + } + + Registry.Get().GetAssets(Filter, Assets); + + FScopedSlowTask Task(Assets.Num(), LOCTEXT("SearchingAssets", "Searching Flow Assets...")); + Task.MakeDialog(); + + int32 CurrentAssetIndex = 0; + + for (const FAssetData& Data : Assets) + { + UFlowAsset* Asset = Cast(Data.GetAsset()); + if (!IsValid(Asset)) + { + continue; + } + + CurrentAssetIndex++; + + Task.EnterProgressFrame(1, FText::Format(LOCTEXT("SearchingAsset", "Searching {0}/{1}: {2}..."), CurrentAssetIndex, Assets.Num(), FText::FromString(Asset->GetName()))); + + FSearchResult AssetRoot = MakeShareable(new FFindInFlowResult(Asset->GetName(), Asset)); + ProcessAsset(Asset, AssetRoot, Tokens, Depth); + + if (AssetRoot->Children.Num() > 0) + { + SearchResults.ItemsFound.Add(AssetRoot); + + // Auto-expand only the current asset + if (Asset == CurrentAsset) + { + TreeView->SetItemExpansion(AssetRoot, true); + } + } + } + } + break; + + default: + checkNoEntry(); + break; } - // Insert a fake result to inform user if none found - if (ItemsFound.Num() == 0) + // Add "No results" placeholder if nothing found + if (SearchResults.ItemsFound.IsEmpty()) { - ItemsFound.Add(MakeShared(LOCTEXT("FlowEditorSearchNoResults", "No Results found").ToString())); + FSearchResult NoResults = MakeShareable(new FFindInFlowResult(TEXT("No results found"))); + SearchResults.ItemsFound.Add(NoResults); } TreeView->RequestTreeRefresh(); +} + +bool SFindInFlow::ProcessAsset(UFlowAsset* Asset, FSearchResult ParentResult, const TArray& Tokens, int32 Depth) +{ + if (!Asset || !Asset->GetGraph() || Depth >= MaxSearchDepth || SearchResults.VisitedAssets.Contains(Asset)) + { + return false; + } + + SearchResults.VisitedAssets.Add(Asset); + + bool bAnyMatches = false; - for (auto It(ItemsFound.CreateIterator()); It; ++It) + for (UEdGraphNode* EdNode : Asset->GetGraph()->Nodes) { - TreeView->SetItemExpansion(*It, true); + const TMap>* CategoryStrings = BuildCategoryStrings(EdNode, Depth); + + if (!CategoryStrings) + { + continue; + } + + EFlowSearchFlags NodeMatchedFlags = EFlowSearchFlags::None; + + for (const TPair>& Pair : *CategoryStrings) + { + const TSet& StringSet = Pair.Value; + if (EnumHasAnyFlags(SearchFlags, Pair.Key) && StringSetMatchesSearchTokens(Tokens, StringSet)) + { + EnumAddFlags(NodeMatchedFlags, Pair.Key); + } + } + + if (NodeMatchedFlags != EFlowSearchFlags::None) + { + FString Title = EdNode->GetNodeTitle(ENodeTitleType::ListView).ToString(); + if (Title.IsEmpty()) + { + Title = EdNode->GetClass()->GetName(); + } + + FSearchResult Result = MakeShareable(new FFindInFlowResult(Title, ParentResult, EdNode, Depth > 0, Asset)); + Result->MatchedFlags = NodeMatchedFlags; + ParentResult->Children.Add(Result); + + bAnyMatches = true; + } + + bAnyMatches |= RecurseIntoSubgraphsIfEnabled(EdNode, ParentResult, Tokens, Depth); } + + return bAnyMatches; } -void SFindInFlow::MatchTokens(const TArray& Tokens) +bool SFindInFlow::RecurseIntoSubgraphsIfEnabled(UEdGraphNode* EdNode, FSearchResult ParentResult, const TArray& Tokens, int32 Depth) { - RootSearchResult.Reset(); - - const UEdGraph* Graph = nullptr; - const TSharedPtr FocusedGraphEditor = FlowAssetEditorPtr.Pin()->GetFlowGraph(); - if (FocusedGraphEditor.IsValid()) + if (!EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Subgraphs)) + { + return false; + } + + UFlowGraphNode* FlowGraphNode = Cast(EdNode); + if (!FlowGraphNode || !FlowGraphNode->GetFlowNodeBase()) + { + return false; + } + + UFlowNode_SubGraph* SubGraph = Cast(FlowGraphNode->GetFlowNodeBase()); + if (!SubGraph) + { + return false; + } + + UFlowAsset* SubAsset = Cast(SubGraph->GetAssetToEdit()); + if (!SubAsset) + { + return false; + } + + const FString SubgraphStr = + SearchResults.VisitedAssets.Contains(SubAsset) ? + TEXT(" (repeat subgraph)") : + TEXT(" (Subgraph)"); + + const FString SubTitle = SubAsset->GetName() + SubgraphStr; + FSearchResult SubResult = MakeShareable(new FFindInFlowResult(SubTitle, ParentResult, EdNode, true, SubAsset)); + + // Subgraphs don't count against depth + if (ProcessAsset(SubAsset, SubResult, Tokens, Depth)) + { + ParentResult->Children.Add(SubResult); + + return true; + } + + return false; +} + +const TMap>* SFindInFlow::BuildCategoryStrings(UEdGraphNode* EdNode, int32 Depth) const +{ + if (!IsValid(EdNode)) + { + return nullptr; + } + + // Check cache first + if (const TMap>* Cached = FFindInFlowCache::CategoryStringCache.Find(EdNode)) + { + return Cached; + } + + TMap> NewResultMap; + + UpdateSearchFlagToStringMapForEdGraphNode(*EdNode, NewResultMap, Depth); + + UFlowGraphNode* FlowGraphNode = Cast(EdNode); + if (IsValid(FlowGraphNode)) + { + UFlowNodeBase* FlowNodeBase = FlowGraphNode->GetFlowNodeBase(); + if (IsValid(FlowNodeBase)) + { + UpdateSearchFlagToStringMapForFlowNodeBase(*FlowNodeBase, NewResultMap, Depth); + } + } + + // Now add the new map to the search cache + const TMap>* AddedResultMap = &FFindInFlowCache::CategoryStringCache.Add(EdNode, NewResultMap); + return AddedResultMap; +} + +void SFindInFlow::UpdateSearchFlagToStringMapForEdGraphNode(const UEdGraphNode& EdGraphNode, TMap>& SearchFlagToStringMap, int32 Depth) const +{ + // Comments + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Comments)) + { + TSet& CommentsSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Comments); + CommentsSet.Add(EdGraphNode.NodeComment); + } +} + +void SFindInFlow::UpdateSearchFlagToStringMapForFlowNodeBase(const UFlowNodeBase& FlowNodeBase, TMap>& SearchFlagToStringMap, int32 Depth) const +{ + // Node Titles + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Titles)) + { + TSet& TitlesSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Titles); + TitlesSet.Add(FlowNodeBase.GetNodeTitle().ToString()); + } + + // Tooltips + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Tooltips)) + { + TSet& TooltipsSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Tooltips); + TooltipsSet.Add(FlowNodeBase.GetNodeToolTip().ToString()); + } + + // Classes + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Classes)) + { + TSet& ClassesSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Classes); + + const FString DisplayName = FlowNodeBase.GetClass()->GetDisplayNameText().ToString(); + ClassesSet.Add(DisplayName); + + const FString NativeName = FlowNodeBase.GetClass()->GetName(); + ClassesSet.Add(NativeName); + } + + // Descriptions + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Descriptions)) + { + TSet& DescriptionsSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Descriptions); + + DescriptionsSet.Add(FlowNodeBase.GetNodeDescription()); + } + + // Config Text + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::ConfigText)) + { + TSet& ConfigSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::ConfigText); + ConfigSet.Add(FlowNodeBase.GetNodeConfigText().ToString()); + } + + // Property-based scouring + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::PropertiesFlags)) + { + AppendPropertyValues(&FlowNodeBase, FlowNodeBase.GetClass(), &FlowNodeBase, SearchFlagToStringMap, Depth); + } + + // AddOns + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::AddOns)) + { + TSet& AddOnsSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::AddOns); + + FlowNodeBase.ForEachAddOnConst([AddOnsSet, this, &SearchFlagToStringMap, &Depth](const UFlowNodeAddOn& AddOn) + { + // No depth penalty for AddOns + UpdateSearchFlagToStringMapForFlowNodeBase(AddOn, SearchFlagToStringMap, Depth); + + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); + } +} + +void SFindInFlow::AppendPropertyValues(const void* Container, const UStruct* Struct, const UObject* ParentObject, TMap>& SearchFlagToStringMap, int32 Depth) const +{ + int32 MaxDepth = 1; + if (const UFlowGraphEditorSettings* Settings = UFlowGraphEditorSettings::Get()) { - Graph = FocusedGraphEditor->GetCurrentGraph(); + MaxDepth = Settings->DefaultMaxSearchDepth; } - if (Graph == nullptr) + if (!Container || !Struct || !ParentObject || Depth >= MaxDepth) { return; } - - RootSearchResult = MakeShared(FString("FlowEditorRoot")); - for (auto It(Graph->Nodes.CreateConstIterator()); It; ++It) + for (TFieldIterator It(Struct, EFieldIteratorFlags::IncludeSuper); It; ++It) { - UEdGraphNode* Node = *It; - - const FString NodeName = Node->GetNodeTitle(ENodeTitleType::ListView).ToString(); - FSearchResult NodeResult(new FFindInFlowResult(NodeName, RootSearchResult, Node)); - FString NodeSearchString = NodeName + Node->GetClass()->GetName() + Node->NodeComment; + FProperty* Prop = *It; + if (!Prop->HasAnyPropertyFlags(CPF_Edit | CPF_SimpleDisplay | CPF_AdvancedDisplay | CPF_BlueprintVisible | CPF_Config)) + { + continue; + } - if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) + const void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); + + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::PropertyNames)) { - FString NodeDescription = FlowGraphNode->GetNodeDescription(); - NodeSearchString += NodeDescription; - - UFlowNode_SubGraph* SubGraphNode = Cast(FlowGraphNode->GetFlowNodeBase()); - if (bFindInSubGraph && SubGraphNode) + TSet& PropertyNamesSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::PropertyNames); + + const FString DisplayName = Prop->GetMetaData(TEXT("DisplayName")); + + if (!DisplayName.IsEmpty()) { - if (const UFlowAsset* FlowAsset = Cast(SubGraphNode->GetAssetToEdit()); FlowAsset && FlowAsset->GetGraph()) - { - for (auto ChildIt(FlowAsset->GetGraph()->Nodes.CreateConstIterator()); ChildIt; ++ChildIt) - { - MatchTokensInChild(Tokens, *ChildIt, NodeResult); - } - } + PropertyNamesSet.Add(DisplayName); } + + PropertyNamesSet.Add(Prop->GetName()); + } + + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::PropertyValues)) + { + TSet& PropertyValuesSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::PropertyValues); + + FString ValueStr; + UObject* MutableParentObject = const_cast(ParentObject); + Prop->ExportText_InContainer(0, ValueStr, Container, nullptr, MutableParentObject, PPF_None); + ValueStr = ValueStr.Replace(TEXT("\""), TEXT("")).TrimStartAndEnd(); + + PropertyValuesSet.Add(ValueStr); } - NodeSearchString = NodeSearchString.Replace(TEXT(" "), TEXT("")); - const bool bNodeMatchesSearch = StringMatchesSearchTokens(Tokens, NodeSearchString); - - if ((NodeResult->Children.Num() > 0) || bNodeMatchesSearch) + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Tooltips)) { - ItemsFound.Add(NodeResult); + TSet& TooltipsSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Tooltips); + TooltipsSet.Add(Prop->GetMetaData(TEXT("ToolTip"))); + } + + if (FStructProperty* StructProp = CastField(Prop)) + { + // Recurse into structs (no depth penalty) + AppendPropertyValues(ValuePtr, StructProp->Struct, ParentObject, SearchFlagToStringMap, Depth); + } + else if (FObjectProperty* ObjProp = CastField(Prop)) + { + // Recurse into inline objects (incurs a depth penalty) + UObject* Obj = ObjProp->GetObjectPropertyValue(ValuePtr); + if (IsValid(Obj) && !Obj->HasAnyFlags(RF_ClassDefaultObject)) + { + AppendPropertyValues(Obj, Obj->GetClass(), Obj, SearchFlagToStringMap, Depth + 1); + } } } } -void SFindInFlow::MatchTokensInChild(const TArray& Tokens, UEdGraphNode* Child, FSearchResult ParentNode) +bool SFindInFlow::StringMatchesSearchTokens(const TArray& Tokens, const FString& ComparisonString) { - if (Child == nullptr) + int32 MatchedTokenCount = 0; + const int32 TotalTokenCount = Tokens.Num(); + + // Must match all tokens + for (const FString& Token : Tokens) { - return; + if (ComparisonString.Contains(Token)) + { + ++MatchedTokenCount; + } + else + { + break; + } } - const FString ChildName = Child->GetNodeTitle(ENodeTitleType::ListView).ToString(); - FString ChildSearchString = ChildName + Child->GetClass()->GetName() + Child->NodeComment; - if (const UFlowGraphNode* FlowGraphNode = Cast(Child)) + if (MatchedTokenCount == TotalTokenCount) { - FString NodeDescription = FlowGraphNode->GetNodeDescription(); - ChildSearchString += NodeDescription; + return true; } - ChildSearchString = ChildSearchString.Replace(TEXT(" "), TEXT("")); - if (StringMatchesSearchTokens(Tokens, ChildSearchString)) + else { - const FSearchResult DecoratorResult(new FFindInFlowResult(ChildName, ParentNode, Child, true)); - ParentNode->Children.Add(DecoratorResult); + return false; + } +} + +bool SFindInFlow::StringSetMatchesSearchTokens(const TArray& Tokens, const TSet& StringSet) +{ + for (const FString& StringFromSet : StringSet) + { + if (StringMatchesSearchTokens(Tokens, StringFromSet)) + { + return true; + } } + + return false; } -TSharedRef SFindInFlow::OnGenerateRow( FSearchResult InItem, const TSharedRef& OwnerTable ) +TSharedRef SFindInFlow::OnGenerateRow(FSearchResult InItem, const TSharedRef& OwnerTable) { - return SNew(STableRow< TSharedPtr >, OwnerTable) + return SNew(STableRow, OwnerTable) .ToolTip(SNew(SToolTip).Text(InItem->GetToolTipText())) [ SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .VAlign(VAlign_Center) - .AutoWidth() - [ - SNew(SBox) - .MinDesiredWidth(300) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(2, 0) [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .AutoWidth() - [ - InItem->CreateIcon() - ] - +SHorizontalBox::Slot() - .VAlign(VAlign_Center) - .AutoWidth() - .Padding(2, 0) - [ - SNew(STextBlock) + InItem->CreateIcon() + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) .Text(FText::FromString(InItem->Value)) .HighlightText(HighlightText) - ] ] - ] - +SHorizontalBox::Slot() - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(FText::FromString(InItem->GetDescriptionText())) - .HighlightText(HighlightText) - ] - +SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(FText::FromString(InItem->GetNodeTypeText())) - .HighlightText(HighlightText) - ] - +SHorizontalBox::Slot() - .HAlign(HAlign_Right) - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(FText::FromString(InItem->GetCommentText())) - .ColorAndOpacity(FLinearColor::Yellow) - .HighlightText(HighlightText) - ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) + .Text(FText::FromString(InItem->GetNodeTypeText())) + .ColorAndOpacity(FSlateColor(FLinearColor(0.6f, 0.8f, 1.0f))) + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) + .Text(InItem->GetMatchedCategoriesText()) + .ColorAndOpacity(FSlateColor(FLinearColor(0.8f, 0.8f, 0.8f))) + ] ]; } -void SFindInFlow::OnGetChildren(FSearchResult InItem, TArray< FSearchResult >& OutChildren) +void SFindInFlow::OnGetChildren(FSearchResult InItem, TArray& OutChildren) { - OutChildren += InItem->Children; + OutChildren = InItem->Children; } -void SFindInFlow::OnTreeSelectionChanged(FSearchResult Item , ESelectInfo::Type) +void SFindInFlow::OnTreeSelectionChanged(FSearchResult Item, ESelectInfo::Type) { if (Item.IsValid()) { - Item->OnClick(FlowAssetEditorPtr, RootSearchResult); + Item->OnClick(FlowAssetEditorPtr); } } @@ -432,33 +964,8 @@ void SFindInFlow::OnTreeSelectionDoubleClicked(FSearchResult Item) { if (Item.IsValid()) { - Item->OnDoubleClick(RootSearchResult); + Item->OnDoubleClick(); } } -void SFindInFlow::OnFindInSubGraphStateChanged(ECheckBoxState CheckBoxState) -{ - bFindInSubGraph = CheckBoxState == ECheckBoxState::Checked; - InitiateSearch(); -} - -bool SFindInFlow::StringMatchesSearchTokens(const TArray& Tokens, const FString& ComparisonString) -{ - bool bFoundAllTokens = true; - - //search the entry for each token, it must have all of them to pass - for (auto TokItr(Tokens.CreateConstIterator()); TokItr; ++TokItr) - { - const FString& Token = *TokItr; - if (!ComparisonString.Contains(Token)) - { - bFoundAllTokens = false; - break; - } - } - return bFoundAllTokens; -} - -///////////////////////////////////////////////////// - #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Find/SFindInFlowFilterPopup.cpp b/Source/FlowEditor/Private/Find/SFindInFlowFilterPopup.cpp new file mode 100644 index 000000000..7735a2d90 --- /dev/null +++ b/Source/FlowEditor/Private/Find/SFindInFlowFilterPopup.cpp @@ -0,0 +1,145 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Find/SFindInFlowFilterPopup.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Framework/Application/SlateApplication.h" + +#define LOCTEXT_NAMESPACE "FindInFlow" + +void SFindInFlowFilterPopup::Construct(const FArguments& InArgs) +{ + OnApplyDelegate = InArgs._OnApply; + OnSaveAsDefaultDelegate = InArgs._OnSaveAsDefault; + ProposedFlags = InArgs._InitialFlags; + + // Build the checkbox container with slots added during construction + SAssignNew(CheckBoxContainer, SVerticalBox); + + for (EFlowSearchFlags Flag : MakeFlagsRange(EFlowSearchFlags::All)) + { + CheckBoxContainer->AddSlot() + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked(this, &SFindInFlowFilterPopup::GetCheckState, Flag) + .OnCheckStateChanged_Lambda([this, Flag](ECheckBoxState NewState) + { + if (NewState == ECheckBoxState::Checked) + { + EnumAddFlags(ProposedFlags, Flag); + } + else + { + EnumRemoveFlags(ProposedFlags, Flag); + } + }) + [ + SNew(STextBlock) + .Text(UEnum::GetDisplayValueAsText(Flag)) + ] + ]; + } + + ChildSlot + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(10) + [ + SNew(STextBlock) + .Text(LOCTEXT("FilterPopupTitle", "Select Search Filters:")) + .Font(FAppStyle::GetFontStyle("NormalFontBold")) + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(10, 5) + [ + SNew(SScrollBox) + + SScrollBox::Slot() + [ + CheckBoxContainer.ToSharedRef() + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(10) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("ToggleAllFilters", "Toggle All")) + .OnClicked(this, &SFindInFlowFilterPopup::OnToggleAllClicked) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("SaveAsDefaultFilters", "Save as Default")) + .OnClicked(this, &SFindInFlowFilterPopup::OnSaveAsDefaultClicked) + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(10) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("CancelFilters", "Cancel")) + .OnClicked(this, &SFindInFlowFilterPopup::OnCancelClicked) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("ApplyFilters", "Apply")) + .OnClicked(this, &SFindInFlowFilterPopup::OnApplyClicked) + ] + ] + ]; +} + +FReply SFindInFlowFilterPopup::OnApplyClicked() +{ + OnApplyDelegate.ExecuteIfBound(ProposedFlags); + FSlateApplication::Get().DismissAllMenus(); + return FReply::Handled(); +} + +FReply SFindInFlowFilterPopup::OnCancelClicked() +{ + FSlateApplication::Get().DismissAllMenus(); + return FReply::Handled(); +} + +FReply SFindInFlowFilterPopup::OnToggleAllClicked() +{ + if (ProposedFlags != EFlowSearchFlags::None) + { + ProposedFlags = EFlowSearchFlags::None; + } + else + { + ProposedFlags = EFlowSearchFlags::All; + } + + CheckBoxContainer->Invalidate(EInvalidateWidgetReason::Layout); + + return FReply::Handled(); +} + +FReply SFindInFlowFilterPopup::OnSaveAsDefaultClicked() +{ + OnSaveAsDefaultDelegate.ExecuteIfBound(ProposedFlags); + return FReply::Handled(); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/FlowEditorModule.cpp b/Source/FlowEditor/Private/FlowEditorModule.cpp index 282fab0d2..74f5e5536 100644 --- a/Source/FlowEditor/Private/FlowEditorModule.cpp +++ b/Source/FlowEditor/Private/FlowEditorModule.cpp @@ -35,6 +35,7 @@ #include "FlowAsset.h" #include "AddOns/FlowNodeAddOn.h" #include "Asset/FlowAssetParamsTypes.h" +#include "Find/FindInFlow.h" #include "Nodes/Actor/FlowNode_ComponentObserver.h" #include "Nodes/Actor/FlowNode_PlayLevelSequence.h" #include "Nodes/Graph/FlowNode_CustomInput.h" @@ -43,6 +44,7 @@ #include "Types/FlowNamedDataPinProperty.h" #include "AssetToolsModule.h" +#include "AssetRegistry/AssetRegistryModule.h" #include "EdGraphUtilities.h" #include "IAssetSearchModule.h" #include "Framework/MultiBox/MultiBoxBuilder.h" @@ -100,6 +102,20 @@ void FFlowEditorModule::StartupModule() ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddRaw(this, &FFlowEditorModule::ModulesChangesCallback); } +void FFlowEditorModule::RegisterForAssetChanges() +{ + if (!bIsRegisteredForAssetChanges) + { + // Register asset change detection for search cache invalidation + FAssetRegistryModule& AssetRegistry = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistry.Get().OnAssetUpdated().AddRaw(this, &FFlowEditorModule::OnAssetUpdated); + AssetRegistry.Get().OnAssetRenamed().AddRaw(this, &FFlowEditorModule::OnAssetRenamed); + AssetRegistry.Get().OnAssetRemoved().AddRaw(this, &FFlowEditorModule::OnAssetUpdated); + + bIsRegisteredForAssetChanges = true; + } +} + void FFlowEditorModule::ShutdownModule() { MenuExtensibilityManager.Reset(); @@ -116,6 +132,18 @@ void FFlowEditorModule::ShutdownModule() SequencerModule.UnRegisterTrackEditor(FlowTrackCreateEditorHandle); FModuleManager::Get().OnModulesChanged().Remove(ModulesChangedHandle); + + if (bIsRegisteredForAssetChanges && FModuleManager::Get().IsModuleLoaded("AssetRegistry")) + { + // Unregister asset change detection + FAssetRegistryModule& AssetRegistry = FModuleManager::Get().GetModuleChecked("AssetRegistry"); + + AssetRegistry.Get().OnAssetUpdated().RemoveAll(this); + AssetRegistry.Get().OnAssetRenamed().RemoveAll(this); + AssetRegistry.Get().OnAssetRemoved().RemoveAll(this); + + bIsRegisteredForAssetChanges = false; + } } void FFlowEditorModule::TrySetFlowNodeDisplayStyleDefaults() const @@ -314,6 +342,19 @@ TSharedRef FFlowEditorModule::CreateFlowAssetEditor(const EToo return NewFlowAssetEditor; } +void FFlowEditorModule::OnAssetUpdated(const FAssetData& AssetData) +{ + if (UFlowAsset* FlowAsset = Cast(AssetData.GetAsset())) + { + FFindInFlowCache::OnFlowAssetChanged(*FlowAsset); + } +} + +void FFlowEditorModule::OnAssetRenamed(const FAssetData& AssetData, const FString& OldObjectPath) +{ + OnAssetUpdated(AssetData); +} + #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FFlowEditorModule, FlowEditor) \ No newline at end of file diff --git a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp index b6d4cfea7..78c54ff23 100644 --- a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp @@ -416,7 +416,7 @@ FSlateColor SFlowGraphNode::GetConfigBoxBackgroundColor() const void SFlowGraphNode::CreateBelowPinControls(const TSharedPtr InnerVerticalBox) { - static const FMargin ConfigBoxPadding = FMargin(2.0f, 0.0f, 1.0f, 0.0); + static const FMargin ConfigBoxPadding = FMargin(2.0f, 0.0f, 1.0f, 0.0f); // Add a box to wrap around the Config Text area to make it a more visually distinct part of the node TSharedPtr BelowPinsBox; @@ -1224,4 +1224,4 @@ void SFlowGraphNode::SetOwner(const TSharedRef& OwnerPanel) } } -#undef LOCTEXT_NAMESPACE +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Public/Find/FindInFlow.h b/Source/FlowEditor/Public/Find/FindInFlow.h index 848bcff4d..9c8321416 100644 --- a/Source/FlowEditor/Public/Find/FindInFlow.h +++ b/Source/FlowEditor/Public/Find/FindInFlow.h @@ -20,32 +20,38 @@ #include "UObject/WeakObjectPtrTemplates.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SCompoundWidget.h" +#include "Widgets/Input/SSpinBox.h" #include "Widgets/Views/STableViewBase.h" #include "Widgets/Views/STreeView.h" +#include "Types/FlowEnumUtils.h" +#include "FindInFlowEnums.h" + class ITableRow; class SWidget; class UFlowGraphNode; class UEdGraphNode; +class UFlowAsset; +class UFlowNodeBase; /** Item that matched the search results */ class FFindInFlowResult { -public: +public: /** Create a root (or only text) result */ - FFindInFlowResult(const FString& InValue); - + FFindInFlowResult(const FString& InValue, UFlowAsset* InOwningFlowAsset = nullptr); + /** Create a flow node result */ - FFindInFlowResult(const FString& InValue, TSharedPtr& InParent, UEdGraphNode* InNode, bool bInIsSubGraphNode = false); + FFindInFlowResult(const FString& InValue, TSharedPtr InParent, UEdGraphNode* InNode, bool bInIsSubGraphNode = false, UFlowAsset* InOwningFlowAsset = nullptr); /** Called when user clicks on the search item */ - FReply OnClick(TWeakPtr FlowAssetEditor, TSharedPtr Root); - + FReply OnClick(TWeakPtr FlowAssetEditorPtr); + /** Called when user double clicks on the search item */ - FReply OnDoubleClick(TSharedPtr Root) const; + FReply OnDoubleClick() const; /** Create an icon to represent the result */ - TSharedRef CreateIcon() const; + TSharedRef CreateIcon() const; /** Gets the description on flow node if any */ FString GetDescriptionText() const; @@ -59,15 +65,30 @@ class FFindInFlowResult /** Gets the node tool tip */ FText GetToolTipText() const; - /** Any children listed under this flow node (decorators and services) */ + /** Returns a snippet of the matched property/value for tooltip */ + FText GetMatchedSnippet() const; + + /** Human-readable list of categories this result matched in */ + FText GetMatchedCategoriesText() const; + + /** Any children listed under this flow node (decorators, services, addons, subnodes) */ TArray< TSharedPtr > Children; /** The string value for this result */ FString Value; + /** Stores a snippet of the matched property/value (e.g. "Damage:50") */ + FString MatchedPropertySnippet; + + /** Which search categories actually produced a hit for this item */ + EFlowSearchFlags MatchedFlags = EFlowSearchFlags::None; + /** The graph node that this search result refers to */ TWeakObjectPtr GraphNode; + /** The owning flow asset for this result */ + TWeakObjectPtr OwningFlowAsset; + /** Search result parent */ TWeakPtr Parent; @@ -75,11 +96,46 @@ class FFindInFlowResult bool bIsSubGraphNode = false; }; +struct FFindInFlowCache +{ + /** Removes all cached data for the changed flow asset */ + static void OnFlowAssetChanged(UFlowAsset& ChangedFlowAsset); + + /** Cache searchable strings per node (for repeat searches) */ + static TMap, TMap>> CategoryStringCache; +}; + +struct FFindInFlowAllResults +{ + typedef TSharedPtr FSearchResult; + + /** we need to keep a handle on the root result, because it won't show up in the tree */ + FSearchResult RootSearchResult; + + /** This buffer stores the currently displayed results */ + TArray ItemsFound; + + /** Visited assets to prevent cycles in subgraph recursion */ + TSet VisitedAssets; + + void Setup() + { + RootSearchResult = MakeShareable(new FFindInFlowResult(TEXT("Root"))); + } + + void Reset() + { + ItemsFound.Empty(); + RootSearchResult->Children.Empty(); + VisitedAssets.Empty(); + } +}; + /** Widget for searching for (Flow nodes) across focused FlowNodes */ class SFindInFlow : public SCompoundWidget { public: - SLATE_BEGIN_ARGS(SFindInFlow){} + SLATE_BEGIN_ARGS(SFindInFlow) {} SLATE_END_ARGS() void Construct(const FArguments& InArgs, TSharedPtr InFlowAssetEditor); @@ -87,7 +143,8 @@ class SFindInFlow : public SCompoundWidget /** Focuses this widget's search box */ void FocusForUse() const; -private: +protected: + typedef TSharedPtr FSearchResult; typedef STreeView STreeViewType; @@ -97,55 +154,84 @@ class SFindInFlow : public SCompoundWidget /** Called when user commits text */ void OnSearchTextCommitted(const FText& Text, ETextCommit::Type CommitType); + /** Called when search button is clicked */ + FReply OnSearchButtonClicked(); + /** Get the children of a row */ void OnGetChildren(FSearchResult InItem, TArray& OutChildren); /** Called when user clicks on a new result */ void OnTreeSelectionChanged(FSearchResult Item, ESelectInfo::Type SelectInfo); - + /* Called when user double clicks on a new result */ - void OnTreeSelectionDoubleClicked( FSearchResult Item ); + void OnTreeSelectionDoubleClicked(FSearchResult Item); - /** Called when whether find in sub graph changed */ - void OnFindInSubGraphStateChanged(ECheckBoxState CheckBoxState); + /** Called when scope selection changed */ + void OnScopeChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo); + + /** Called when max depth changed */ + void OnMaxDepthChanged(int32 NewDepth); /** Called when a new row is being generated */ TSharedRef OnGenerateRow(FSearchResult InItem, const TSharedRef& OwnerTable); /** Begins the search based on the SearchValue */ void InitiateSearch(); - - /** Find any results that contain all of the tokens */ - void MatchTokens(const TArray& Tokens); - /** Find if child contains all of the tokens and add a result accordingly */ - static void MatchTokensInChild(const TArray& Tokens, UEdGraphNode* Child, FSearchResult ParentNode); - + /** Build searchable string from node and its FlowNodeBase + AddOns */ + const TMap>* BuildCategoryStrings(UEdGraphNode* Node, int32 Depth) const; + /** Determines if a string matches the search tokens */ static bool StringMatchesSearchTokens(const TArray& Tokens, const FString& ComparisonString); + static bool StringSetMatchesSearchTokens(const TArray& Tokens, const TSet& StringSet); + + /** Generate widget for scope combo */ + TSharedRef GenerateScopeWidget(TSharedPtr Item) const; + + /** Get current scope display text */ + FText GetCurrentScopeText() const; + + bool ProcessAsset(UFlowAsset* Asset, FSearchResult ParentResult, const TArray& Tokens, int32 Depth); + + bool RecurseIntoSubgraphsIfEnabled(UEdGraphNode* EdNode, FSearchResult ParentResult, const TArray& Tokens, int32 Depth); -private: + void UpdateSearchFlagToStringMapForEdGraphNode(const UEdGraphNode& EdGraphNode, TMap>& SearchFlagToStringMap, int32 Depth) const; + void UpdateSearchFlagToStringMapForFlowNodeBase(const UFlowNodeBase& FlowNodeBase, TMap>& SearchFlagToStringMap, int32 Depth) const; + void AppendPropertyValues(const void* Container, const UStruct* Struct, const UObject* ParentObject, TMap>& SearchFlagToStringMap, int32 Depth) const; + +protected: /** Pointer back to the flow editor that owns us */ TWeakPtr FlowAssetEditorPtr; - + /** The tree view displays the results */ TSharedPtr TreeView; /** The search text box */ TSharedPtr SearchTextField; - - /** This buffer stores the currently displayed results */ - TArray ItemsFound; - /** we need to keep a handle on the root result, because it won't show up in the tree */ - FSearchResult RootSearchResult; + /** The search button */ + TSharedPtr SearchButton; + + /** Struct with all of the search results */ + FFindInFlowAllResults SearchResults; + + /** Repeat Search Caching */ + FFindInFlowCache SearchCache; /** The string to highlight in the results */ FText HighlightText; /** The string to search for */ - FString SearchValue; + FString SearchValue; - /** Using to control whether search in sub graph */ - bool bFindInSubGraph = false; -}; + /** Search configuration */ + EFlowSearchFlags SearchFlags = EFlowSearchFlags::DefaultSearchFlags; + + TSharedPtr> MaxDepthSpinBox; + int32 MaxSearchDepth = 3; + + /** Scope selection */ + TArray> ScopeOptionList; + TSharedPtr SelectedScopeOption; + EFlowSearchScope SearchScope = EFlowSearchScope::ThisAssetOnly; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Find/FindInFlowEnums.h b/Source/FlowEditor/Public/Find/FindInFlowEnums.h new file mode 100644 index 000000000..e0c7a6887 --- /dev/null +++ b/Source/FlowEditor/Public/Find/FindInFlowEnums.h @@ -0,0 +1,48 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Types/FlowEnumUtils.h" + +#include "FindInFlowEnums.generated.h" + +/** Bitflags controlling what parts of a Flow node are included in search */ +UENUM(Meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true")) +enum class EFlowSearchFlags : uint32 +{ + None = 0 UMETA(Hidden), + + Titles = 1 << 0 UMETA(DisplayName = "Titles"), + Tooltips = 1 << 1 UMETA(DisplayName = "Tooltips"), + Classes = 1 << 2 UMETA(DisplayName = "Classes"), + Comments = 1 << 3 UMETA(DisplayName = "Comments"), + Descriptions = 1 << 4 UMETA(DisplayName = "Descriptions"), + ConfigText = 1 << 5 UMETA(DisplayName = "Config Text"), + PropertyNames = 1 << 6 UMETA(DisplayName = "Property Names"), + PropertyValues = 1 << 7 UMETA(DisplayName = "Property Values"), + AddOns = 1 << 8 UMETA(DisplayName = "Add-Ons"), + Subgraphs = 1 << 9 UMETA(DisplayName = "Subgraphs"), + + All = + Titles | Tooltips | Classes | Comments | Descriptions | ConfigText | + PropertyNames | PropertyValues | AddOns | Subgraphs UMETA(Hidden), + + // Default mask — used at startup and for "reset" + DefaultSearchFlags = All UMETA(Hidden), + PropertiesFlags = PropertyNames | PropertyValues | Tooltips UMETA(Hidden), +}; +ENUM_CLASS_FLAGS(EFlowSearchFlags); + +/** Search scope — intentionally minimal */ +UENUM() +enum class EFlowSearchScope : uint8 +{ + ThisAssetOnly UMETA(DisplayName = "This Asset", ToolTip = "Search only the currently open Flow Asset"), + AllOfThisType UMETA(DisplayName = "All Flow Assets of This Type",ToolTip = "Search all Flow Assets of this type (or subclasses)"), + AllFlowAssets UMETA(DisplayName = "All Flow Assets", ToolTip = "Search every Flow Asset in the project"), + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowSearchScope); \ No newline at end of file diff --git a/Source/FlowEditor/Public/Find/SFindInFlowFilterPopup.h b/Source/FlowEditor/Public/Find/SFindInFlowFilterPopup.h new file mode 100644 index 000000000..49843b068 --- /dev/null +++ b/Source/FlowEditor/Public/Find/SFindInFlowFilterPopup.h @@ -0,0 +1,42 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/SBoxPanel.h" + +#include "FindInFlowEnums.h" + +DECLARE_DELEGATE_OneParam(FFindInFlowApplyDelegate, EFlowSearchFlags); + +class SFindInFlowFilterPopup : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SFindInFlowFilterPopup) {} + SLATE_ARGUMENT(FFindInFlowApplyDelegate, OnApply) + SLATE_ARGUMENT(FFindInFlowApplyDelegate, OnSaveAsDefault) + SLATE_ARGUMENT(EFlowSearchFlags, InitialFlags) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + +protected: + + EFlowSearchFlags ProposedFlags = EFlowSearchFlags::DefaultSearchFlags; + + FFindInFlowApplyDelegate OnApplyDelegate; + FFindInFlowApplyDelegate OnSaveAsDefaultDelegate; + + TSharedPtr CheckBoxContainer; + + ECheckBoxState GetCheckState(EFlowSearchFlags Flag) const + { + return EnumHasAnyFlags(ProposedFlags, Flag) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + } + + FReply OnApplyClicked(); + FReply OnCancelClicked(); + FReply OnToggleAllClicked(); + FReply OnSaveAsDefaultClicked(); +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/FlowEditorModule.h b/Source/FlowEditor/Public/FlowEditorModule.h index 8dec688cf..e1266b1b3 100644 --- a/Source/FlowEditor/Public/FlowEditorModule.h +++ b/Source/FlowEditor/Public/FlowEditorModule.h @@ -34,6 +34,8 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface, public IHasMen TSharedPtr MenuExtensibilityManager; TSharedPtr ToolBarExtensibilityManager; + bool bIsRegisteredForAssetChanges = false; + public: virtual void StartupModule() override; virtual void ShutdownModule() override; @@ -41,6 +43,8 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface, public IHasMen virtual TSharedPtr GetMenuExtensibilityManager() override { return MenuExtensibilityManager; } virtual TSharedPtr GetToolBarExtensibilityManager() override { return ToolBarExtensibilityManager; } + void RegisterForAssetChanges(); + private: void TrySetFlowNodeDisplayStyleDefaults() const; @@ -65,4 +69,7 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface, public IHasMen public: static TSharedRef CreateFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UFlowAsset* FlowAsset); -}; + + void OnAssetUpdated(const FAssetData& AssetData); + void OnAssetRenamed(const FAssetData& AssetData, const FString& OldObjectPath); +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h b/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h index f19656734..8695702ca 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h @@ -3,6 +3,8 @@ #pragma once #include "Engine/DeveloperSettings.h" +#include "Find/FindInFlowEnums.h" + #include "FlowGraphEditorSettings.generated.h" UENUM() @@ -67,6 +69,14 @@ class FLOWEDITOR_API UFlowGraphEditorSettings : public UDeveloperSettings UPROPERTY(EditAnywhere, config, Category = "Wires") bool bHighlightOutputWiresOfSelectedNodes; + // Default search filter flags for the Flow Editor + UPROPERTY(VisibleAnywhere, config, Category = "Search", meta = (Bitmask, BitmaskEnum = "/Script/Flow.EFlowSearchFlags")) + uint32 DefaultSearchFlags = uint32(EFlowSearchFlags::DefaultSearchFlags); + + // Max search depth for inline objects in the Flow Editor + UPROPERTY(EditAnywhere, config, Category = "Search", meta = (ClampMin = 1)) + int32 DefaultMaxSearchDepth = 1; + public: virtual FName GetCategoryName() const override { return FName("Flow Graph"); } virtual FText GetSectionText() const override { return INVTEXT("User Settings"); } diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSchema.h b/Source/FlowEditor/Public/Graph/FlowGraphSchema.h index a912b60a4..78918f786 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSchema.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSchema.h @@ -2,6 +2,9 @@ #pragma once +#include "Asset/FlowPinTypeMatchPolicy.h" +#include "Types/FlowPinTypeNamesStandard.h" + #include "EdGraph/EdGraphSchema.h" #include "Runtime/Launch/Resources/Version.h" #include "Templates/SubclassOf.h" From cc5b9c8fe6fe3c8bfa6b3fe9eb5ced7c14469123 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:04:55 -0800 Subject: [PATCH 02/10] Flow Debugger Fixes Data Pin Values displayed in Tooltips Flow debugger stops at breakpoints bugfix --- Flow.uplugin | 2 +- Source/Flow/Private/FlowAsset.cpp | 21 ++ .../Private/Interfaces/FlowExecutionGate.cpp | 134 +++++++++++ Source/Flow/Private/Nodes/FlowNode.cpp | 10 +- Source/Flow/Private/Nodes/FlowPin.cpp | 1 - .../Flow/Private/Types/FlowDataPinValue.cpp | 2 +- .../Types/FlowDataPinValuesStandard.cpp | 38 ++++ Source/Flow/Public/FlowAsset.h | 6 +- .../Public/Interfaces/FlowExecutionGate.h | 45 ++++ Source/Flow/Public/Nodes/FlowPin.h | 1 - .../Public/Types/FlowDataPinValuesStandard.h | 67 +++--- .../Public/Types/FlowNamedDataPinProperty.h | 2 +- .../Debugger/FlowDebuggerSubsystem.cpp | 214 ++++++++++++++---- .../Public/Debugger/FlowDebuggerSubsystem.h | 63 ++++-- .../Private/Asset/FlowAssetToolbar.cpp | 13 +- .../Asset/FlowDebugEditorSubsystem.cpp | 108 ++++++++- .../Private/Graph/FlowGraphEditor.cpp | 161 ++++++++----- .../Private/Graph/Nodes/FlowGraphNode.cpp | 90 ++++++-- .../FlowEditor/Public/Asset/FlowAssetEditor.h | 2 +- .../Public/Asset/FlowDebugEditorSubsystem.h | 9 +- .../FlowEditor/Public/Graph/FlowGraphEditor.h | 8 + 21 files changed, 817 insertions(+), 180 deletions(-) create mode 100644 Source/Flow/Private/Interfaces/FlowExecutionGate.cpp create mode 100644 Source/Flow/Public/Interfaces/FlowExecutionGate.h diff --git a/Flow.uplugin b/Flow.uplugin index ec3fc0671..f4eb2d4c3 100644 --- a/Flow.uplugin +++ b/Flow.uplugin @@ -8,7 +8,7 @@ "DocsURL" : "https://github.com/MothCocoon/FlowGraph/wiki", "MarketplaceURL" : "", "SupportURL": "https://discord.gg/Xmtr6GhbmW", - "EnabledByDefault" : true, + "EnabledByDefault" : false, "CanContainContent" : false, "IsBetaVersion" : false, "Installed" : false, diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index df435c480..44b672dd1 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -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" @@ -965,6 +966,11 @@ void UFlowAsset::PreStartFlow() void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier) { + if (FFlowExecutionGate::IsHalted()) + { + return; + } + PreStartFlow(); if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode()) @@ -1028,6 +1034,11 @@ TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGr void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const { + if (FFlowExecutionGate::IsHalted()) + { + return; + } + // NOTE (gtaylor) Custom Input nodes cannot currently add data pins (like Start or DefineProperties nodes can) // but we may want to allow them to source parameters, so I am providing the subgraph node as the // IFlowDataPinValueSupplierInterface when triggering the node (even though it's not used at this time). @@ -1041,6 +1052,11 @@ void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNod void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier) { + if (FFlowExecutionGate::IsHalted()) + { + return; + } + for (UFlowNode_CustomInput* CustomInputNode : CustomInputNodes) { if (CustomInputNode->EventName == EventName) @@ -1080,6 +1096,11 @@ void UFlowAsset::TriggerCustomOutput(const FName& EventName) void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) { + if (FFlowExecutionGate::EnqueueDeferredTriggerInput(this, NodeGuid, PinName, FromPin)) + { + return; + } + if (UFlowNode* Node = Nodes.FindRef(NodeGuid)) { if (!ActiveNodes.Contains(Node)) diff --git a/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp new file mode 100644 index 000000000..44fd864f2 --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp @@ -0,0 +1,134 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowExecutionGate.h" + +#include "FlowAsset.h" +#include "Nodes/FlowPin.h" + +namespace FlowExecutionGate_Private +{ + struct FDeferredTriggerInput + { + TWeakObjectPtr FlowAssetInstance; + FGuid NodeGuid; + FName PinName; + FConnectedPin FromPin; + }; + + static TArray DeferredTriggerInputs; + static bool bIsFlushing = false; +} + +IFlowExecutionGate* FFlowExecutionGate::Gate = nullptr; + +void FFlowExecutionGate::SetGate(IFlowExecutionGate* InGate) +{ + Gate = InGate; +} + +IFlowExecutionGate* FFlowExecutionGate::GetGate() +{ + return Gate; +} + +bool FFlowExecutionGate::IsHalted() +{ + return (Gate != nullptr) && Gate->IsFlowExecutionHalted(); +} + +bool FFlowExecutionGate::EnqueueDeferredTriggerInput(UFlowAsset* FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) +{ + using namespace FlowExecutionGate_Private; + + // If we're halted, always enqueue (even during flushing). The whole point is to stop propagation. + if (IsHalted()) + { + if (!IsValid(FlowAssetInstance)) + { + return true; // treat as handled while halted + } + + FDeferredTriggerInput& Entry = DeferredTriggerInputs.AddDefaulted_GetRef(); + Entry.FlowAssetInstance = FlowAssetInstance; + Entry.NodeGuid = NodeGuid; + Entry.PinName = PinName; + Entry.FromPin = FromPin; + + return true; + } + + // Not halted: + // During flush we must not enqueue "normal" triggers (we want them to execute now), + // otherwise we can get infinite deferral. + if (bIsFlushing) + { + return false; + } + + return false; +} + +void FFlowExecutionGate::FlushDeferredTriggerInputs() +{ + using namespace FlowExecutionGate_Private; + + if (bIsFlushing) + { + return; + } + + // Do not flush while halted; callers should clear the halt first. + if (IsHalted()) + { + return; + } + + if (DeferredTriggerInputs.IsEmpty()) + { + return; + } + + bIsFlushing = true; + + // Move into a local array so new deferred triggers can be added while we flush. + TArray Local = MoveTemp(DeferredTriggerInputs); + DeferredTriggerInputs.Reset(); + + for (int32 Index = 0; Index < Local.Num(); ++Index) + { + // If a breakpoint was hit during this flush, stop immediately and re-queue remaining work. + if (IsHalted()) + { + const int32 Remaining = Local.Num() - Index; + if (Remaining > 0) + { + TArray RemainingItems; + RemainingItems.Reserve(Remaining); + + for (int32 j = Index; j < Local.Num(); ++j) + { + RemainingItems.Add(Local[j]); + } + + // RemainingItems should run before any items that may already be queued. + if (!DeferredTriggerInputs.IsEmpty()) + { + RemainingItems.Append(MoveTemp(DeferredTriggerInputs)); + } + + DeferredTriggerInputs = MoveTemp(RemainingItems); + } + + bIsFlushing = false; + return; + } + + const FDeferredTriggerInput& Entry = Local[Index]; + if (UFlowAsset* Asset = Entry.FlowAssetInstance.Get()) + { + Asset->TriggerDeferredInputFromDebugger(Entry.NodeGuid, Entry.PinName, Entry.FromPin); + } + } + + bIsFlushing = false; +} \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index be876bee5..0121cb170 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -883,9 +883,10 @@ void UFlowNode::TriggerInput(const FName& PinName, const EFlowPinActivationType TArray& Records = InputRecords.FindOrAdd(PinName); Records.Add(FPinRecord(FApp::GetCurrentTime(), ActivationType)); - if (const UFlowAsset* FlowAssetTemplate = GetFlowAsset()->GetTemplateAsset()) + UFlowAsset* FlowAssetInstance = GetFlowAsset(); + if (const UFlowAsset* FlowAssetTemplate = FlowAssetInstance->GetTemplateAsset()) { - (void)FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(NodeGuid, PinName); + (void) FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(FlowAssetInstance, NodeGuid, PinName); } #endif } @@ -949,9 +950,10 @@ void UFlowNode::TriggerOutput(const FName PinName, const bool bFinish /*= false* TArray& Records = OutputRecords.FindOrAdd(PinName); Records.Add(FPinRecord(FApp::GetCurrentTime(), ActivationType)); - if (const UFlowAsset* FlowAssetTemplate = GetFlowAsset()->GetTemplateAsset()) + UFlowAsset* FlowAssetInstance = GetFlowAsset(); + if (const UFlowAsset* FlowAssetTemplate = FlowAssetInstance->GetTemplateAsset()) { - FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(NodeGuid, PinName); + FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(FlowAssetInstance, NodeGuid, PinName); } } else diff --git a/Source/Flow/Private/Nodes/FlowPin.cpp b/Source/Flow/Private/Nodes/FlowPin.cpp index 84a3351ad..8c2a34cd5 100644 --- a/Source/Flow/Private/Nodes/FlowPin.cpp +++ b/Source/Flow/Private/Nodes/FlowPin.cpp @@ -18,7 +18,6 @@ // Pin Record #if !UE_BUILD_SHIPPING -FString FPinRecord::NoActivations = TEXT("No activations"); FString FPinRecord::PinActivations = TEXT("Pin activations"); FString FPinRecord::ForcedActivation = TEXT(" (forced activation)"); FString FPinRecord::PassThroughActivation = TEXT(" (pass-through activation)"); diff --git a/Source/Flow/Private/Types/FlowDataPinValue.cpp b/Source/Flow/Private/Types/FlowDataPinValue.cpp index b10c3e206..34de3e9f7 100644 --- a/Source/Flow/Private/Types/FlowDataPinValue.cpp +++ b/Source/Flow/Private/Types/FlowDataPinValue.cpp @@ -8,7 +8,7 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDataPinValue) -const FString FFlowDataPinValue::StringArraySeparator = TEXT(","); +const FString FFlowDataPinValue::StringArraySeparator = TEXT(", "); const FFlowPinType* FFlowDataPinValue::LookupPinType() const { diff --git a/Source/Flow/Private/Types/FlowDataPinValuesStandard.cpp b/Source/Flow/Private/Types/FlowDataPinValuesStandard.cpp index 2d8b4db79..b7bec0109 100644 --- a/Source/Flow/Private/Types/FlowDataPinValuesStandard.cpp +++ b/Source/Flow/Private/Types/FlowDataPinValuesStandard.cpp @@ -479,6 +479,44 @@ FFlowDataPinValue_InstancedStruct::FFlowDataPinValue_InstancedStruct(const TArra #endif } +bool FFlowDataPinValue_InstancedStruct::TryConvertValuesToString(FString& OutString) const +{ + const FInstancedStruct DefaultValue; + + OutString = FlowArray::FormatArrayString( + Values, + [&DefaultValue](const FInstancedStruct& InstancedStruct) + { + FString ExportedString; + + constexpr UObject* ParentObject = nullptr; + constexpr UObject* ExportRootScope = nullptr; + + const bool bExported = InstancedStruct.ExportTextItem( + ExportedString, + DefaultValue, + ParentObject, + PPF_None, + ExportRootScope); + + if (!bExported) + { + // Fallback: just show the contained struct type name (or None) + if (const UScriptStruct* ScriptStruct = InstancedStruct.GetScriptStruct()) + { + return ScriptStruct->GetName(); + } + + return FString(); + } + + return ExportedString; + }, + StringArraySeparator); + + return true; +} + //====================================================================== // Object //====================================================================== diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index d562b8d8b..6de5ba0cf 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -26,7 +26,7 @@ class UFlowAssetParams; #if !UE_BUILD_SHIPPING DECLARE_DELEGATE(FFlowGraphEvent); -DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, const FGuid& /*NodeGuid*/, const FName& /*PinName*/); +DECLARE_DELEGATE_ThreeParams(FFlowSignalEvent, UFlowAsset* /*FlowAsset*/, const FGuid& /*NodeGuid*/, const FName& /*PinName*/); #endif /** @@ -353,6 +353,10 @@ class FLOW_API UFlowAsset : public UObject // Get Flow Asset instance created by the given SubGraph node TWeakObjectPtr GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const; + // Public trigger input signature for the FFlowExecutionGate mechanism in the Flow Debugger + FORCEINLINE void TriggerDeferredInputFromDebugger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) + { TriggerInput(NodeGuid, PinName, FromPin); } + protected: void TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* Node, const FName& EventName) const; void TriggerCustomOutput(const FName& EventName); diff --git a/Source/Flow/Public/Interfaces/FlowExecutionGate.h b/Source/Flow/Public/Interfaces/FlowExecutionGate.h new file mode 100644 index 000000000..8e1abdf66 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowExecutionGate.h @@ -0,0 +1,45 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "CoreMinimal.h" + +class UFlowAsset; + +/** + * Implemented by a debugger/runtime system (in another module) that can halt Flow execution. + * Flow runtime queries this through FFlowExecutionGate without depending on the debugger module. + */ +class FLOW_API IFlowExecutionGate +{ +public: + virtual ~IFlowExecutionGate() = default; + + /** Return true when Flow execution should be halted globally. */ + virtual bool IsFlowExecutionHalted() const = 0; +}; + +/** + * Global registry + minimal deferred-execution queue for Flow runtime. + */ +class FLOW_API FFlowExecutionGate +{ +public: + static void SetGate(IFlowExecutionGate* InGate); + static IFlowExecutionGate* GetGate(); + + /** True if a gate exists and it currently wants Flow execution halted. */ + static bool IsHalted(); + + /** If halted, queues the trigger for later. Returns true if queued (caller should early-out). */ + static bool EnqueueDeferredTriggerInput(UFlowAsset* FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName, const struct FConnectedPin& FromPin); + + /** + * Flushes queued trigger inputs (FIFO). + * Safe to call even if nothing is queued. + */ + static void FlushDeferredTriggerInputs(); + +private: + static IFlowExecutionGate* Gate; +}; \ No newline at end of file diff --git a/Source/Flow/Public/Nodes/FlowPin.h b/Source/Flow/Public/Nodes/FlowPin.h index 465a2cf14..9aa88d484 100644 --- a/Source/Flow/Public/Nodes/FlowPin.h +++ b/Source/Flow/Public/Nodes/FlowPin.h @@ -388,7 +388,6 @@ struct FLOW_API FPinRecord FString HumanReadableTime; EFlowPinActivationType ActivationType; - static FString NoActivations; static FString PinActivations; static FString ForcedActivation; static FString PassThroughActivation; diff --git a/Source/Flow/Public/Types/FlowDataPinValuesStandard.h b/Source/Flow/Public/Types/FlowDataPinValuesStandard.h index bfe5a18a2..9757fbb45 100644 --- a/Source/Flow/Public/Types/FlowDataPinValuesStandard.h +++ b/Source/Flow/Public/Types/FlowDataPinValuesStandard.h @@ -35,8 +35,8 @@ struct FFlowDataPinValue_Bool : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Bool(ValueType InValue); FLOW_API FFlowDataPinValue_Bool(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -58,8 +58,8 @@ struct FFlowDataPinValue_Int : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Int(ValueType InValue); FLOW_API FFlowDataPinValue_Int(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -81,8 +81,8 @@ struct FFlowDataPinValue_Int64 : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Int64(ValueType InValue); FLOW_API FFlowDataPinValue_Int64(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -104,8 +104,8 @@ struct FFlowDataPinValue_Float : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Float(ValueType InValue); FLOW_API FFlowDataPinValue_Float(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -127,8 +127,8 @@ struct FFlowDataPinValue_Double : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Double(ValueType InValue); FLOW_API FFlowDataPinValue_Double(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -150,8 +150,8 @@ struct FFlowDataPinValue_Name : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Name(const ValueType& InValue); FLOW_API FFlowDataPinValue_Name(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -173,8 +173,8 @@ struct FFlowDataPinValue_String : public FFlowDataPinValue FLOW_API FFlowDataPinValue_String(const ValueType& InValue); FLOW_API FFlowDataPinValue_String(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -196,8 +196,8 @@ struct FFlowDataPinValue_Text : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Text(const ValueType& InValue); FLOW_API FFlowDataPinValue_Text(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -233,9 +233,9 @@ struct FFlowDataPinValue_Enum : public FFlowDataPinValue FLOW_API void OnEnumNameChanged(); #endif - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } virtual UField* GetFieldType() const override; - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; // Helper templates template @@ -315,8 +315,8 @@ struct FFlowDataPinValue_Vector : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Vector(const ValueType& InValue); FLOW_API FFlowDataPinValue_Vector(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -338,8 +338,8 @@ struct FFlowDataPinValue_Rotator : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Rotator(const ValueType& InValue); FLOW_API FFlowDataPinValue_Rotator(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -361,8 +361,8 @@ struct FFlowDataPinValue_Transform : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Transform(const ValueType& InValue); FLOW_API FFlowDataPinValue_Transform(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -384,8 +384,8 @@ struct FFlowDataPinValue_GameplayTag : public FFlowDataPinValue FLOW_API FFlowDataPinValue_GameplayTag(const ValueType& InValue); FLOW_API FFlowDataPinValue_GameplayTag(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -409,8 +409,8 @@ struct FFlowDataPinValue_GameplayTagContainer : public FFlowDataPinValue FLOW_API FFlowDataPinValue_GameplayTagContainer(const TArray& InValues); FLOW_API FFlowDataPinValue_GameplayTagContainer(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -432,7 +432,8 @@ struct FFlowDataPinValue_InstancedStruct : public FFlowDataPinValue FLOW_API FFlowDataPinValue_InstancedStruct(const ValueType& InValue); FLOW_API FFlowDataPinValue_InstancedStruct(const TArray& InValues); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -461,8 +462,8 @@ struct FFlowDataPinValue_Object : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Object(AActor* InActor, UClass* InClassFilter = nullptr /* nullptr here defaults to AActor::StaticClass() */ ); FLOW_API FFlowDataPinValue_Object(const TArray& InActors, UClass* InClassFilter = nullptr /* nullptr here defaults to AActor::StaticClass() */); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; //====================================================================== @@ -491,6 +492,6 @@ struct FFlowDataPinValue_Class : public FFlowDataPinValue FLOW_API FFlowDataPinValue_Class(const UClass* InClass, UClass* InClassFilter = UObject::StaticClass()); FLOW_API FFlowDataPinValue_Class(const TArray& InClasses, UClass* InClassFilter = UObject::StaticClass()); - virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } - virtual bool TryConvertValuesToString(FString& OutString) const override; + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; }; \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowNamedDataPinProperty.h b/Source/Flow/Public/Types/FlowNamedDataPinProperty.h index 939b67fa1..0b6031186 100644 --- a/Source/Flow/Public/Types/FlowNamedDataPinProperty.h +++ b/Source/Flow/Public/Types/FlowNamedDataPinProperty.h @@ -27,7 +27,7 @@ struct FFlowNamedDataPinProperty private: // DataPinProperty payload - UPROPERTY(VisibleAnywhere, Category = DataPins, meta = (DeprecatedProperty)) + UPROPERTY(meta = (DeprecatedProperty)) TInstancedStruct DataPinProperty; public: diff --git a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp index b9543a4aa..ed54ba520 100644 --- a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp @@ -21,6 +21,23 @@ UFlowDebuggerSubsystem::UFlowDebuggerSubsystem() UFlowSubsystem::OnInstancedTemplateRemoved.BindUObject(this, &ThisClass::OnInstancedTemplateRemoved); } +void UFlowDebuggerSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + FFlowExecutionGate::SetGate(this); +} + +void UFlowDebuggerSubsystem::Deinitialize() +{ + if (FFlowExecutionGate::GetGate() == this) + { + FFlowExecutionGate::SetGate(nullptr); + } + + Super::Deinitialize(); +} + bool UFlowDebuggerSubsystem::ShouldCreateSubsystem(UObject* Outer) const { // Only create an instance if there is no override implementation defined elsewhere @@ -36,23 +53,31 @@ bool UFlowDebuggerSubsystem::ShouldCreateSubsystem(UObject* Outer) const void UFlowDebuggerSubsystem::OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) { + check(IsValid(AssetTemplate)); + AssetTemplate->OnPinTriggered.BindUObject(this, &ThisClass::OnPinTriggered); } -void UFlowDebuggerSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) const +void UFlowDebuggerSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) { + check(IsValid(AssetTemplate)); + AssetTemplate->OnPinTriggered.Unbind(); + + OnDebuggerFlowAssetTemplateRemoved.Broadcast(*AssetTemplate); } -void UFlowDebuggerSubsystem::OnPinTriggered(const FGuid& NodeGuid, const FName& PinName) +void UFlowDebuggerSubsystem::OnPinTriggered(UFlowAsset* FlowAsset, const FGuid& NodeGuid, const FName& PinName) { + check(IsValid(FlowAsset)); + if (FindBreakpoint(NodeGuid, PinName)) { - MarkAsHit(NodeGuid, PinName); + MarkAsHit(*FlowAsset, NodeGuid, PinName); } // Node breakpoints waits on any pin triggered - MarkAsHit(NodeGuid); + MarkAsHit(*FlowAsset, NodeGuid); } void UFlowDebuggerSubsystem::AddBreakpoint(const FGuid& NodeGuid) @@ -140,15 +165,23 @@ void UFlowDebuggerSubsystem::RemoveObsoletePinBreakpoints(const UEdGraphNode* No PinNames.Emplace(Pin->PinName); } - for (TPair& PinBreakpoint : NodeBreakpoint->PinBreakpoints) + TArray PinsToRemove; + PinsToRemove.Reserve(NodeBreakpoint->PinBreakpoints.Num()); + + for (const TPair& PinBreakpoint : NodeBreakpoint->PinBreakpoints) { if (!PinNames.Contains(PinBreakpoint.Key)) { - NodeBreakpoint->PinBreakpoints.Remove(PinBreakpoint.Key); - bAnythingRemoved = true; + PinsToRemove.Add(PinBreakpoint.Key); } } + for (const FName& PinName : PinsToRemove) + { + NodeBreakpoint->PinBreakpoints.Remove(PinName); + bAnythingRemoved = true; + } + if (NodeBreakpoint->IsEmpty()) { Settings->NodeBreakpoints.Remove(Node->NodeGuid); @@ -244,71 +277,169 @@ bool UFlowDebuggerSubsystem::IsBreakpointEnabled(const FGuid& NodeGuid, const FN return false; } -void UFlowDebuggerSubsystem::MarkAsHit(const FGuid& NodeGuid) +void UFlowDebuggerSubsystem::RequestHaltFlowExecution(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid) +{ + bHaltFlowExecution = true; + HaltedOnFlowAssetInstance = &FlowAssetInstance; + HaltedOnNodeGuid = NodeGuid; +} + +void UFlowDebuggerSubsystem::ClearHaltFlowExecution() +{ + bHaltFlowExecution = false; + HaltedOnFlowAssetInstance.Reset(); + HaltedOnNodeGuid.Invalidate(); +} + +void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() +{ + if (!LastHitNodeGuid.IsValid()) + { + return; + } + + // Pin breakpoint "hit" state lives in the PinBreakpoints map, node breakpoint "hit" lives on NodeBreakpoint.Breakpoint. + if (!LastHitPinName.IsNone()) + { + if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(LastHitNodeGuid, LastHitPinName)) + { + PinBreakpoint->MarkAsHit(false); + } + } + else + { + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(LastHitNodeGuid)) + { + NodeBreakpoint->MarkAsHit(false); + } + } + + LastHitNodeGuid.Invalidate(); + LastHitPinName = NAME_None; +} + +void UFlowDebuggerSubsystem::MarkAsHit(const UFlowAsset& FlowAsset, const FGuid& NodeGuid) { if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(NodeGuid)) { if (NodeBreakpoint->IsEnabled()) { + // Ensure only one breakpoint location is "hit" at a time. + ClearLastHitBreakpoint(); + NodeBreakpoint->MarkAsHit(true); - PauseSession(); + + LastHitNodeGuid = NodeGuid; + LastHitPinName = NAME_None; + + RequestHaltFlowExecution(FlowAsset, NodeGuid); + + OnDebuggerBreakpointHit.Broadcast(FlowAsset, NodeGuid); + + PauseSession(FlowAsset); } } } -void UFlowDebuggerSubsystem::MarkAsHit(const FGuid& NodeGuid, const FName& PinName) +void UFlowDebuggerSubsystem::MarkAsHit(const UFlowAsset& FlowAsset, const FGuid& NodeGuid, const FName& PinName) { if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(NodeGuid, PinName)) { if (PinBreakpoint->IsEnabled()) { + // Ensure only one breakpoint location is "hit" at a time. + ClearLastHitBreakpoint(); + PinBreakpoint->MarkAsHit(true); - PauseSession(); + + LastHitNodeGuid = NodeGuid; + LastHitPinName = PinName; + + RequestHaltFlowExecution(FlowAsset, NodeGuid); + + OnDebuggerBreakpointHit.Broadcast(FlowAsset, NodeGuid); + + PauseSession(FlowAsset); } } } -void UFlowDebuggerSubsystem::PauseSession() +void UFlowDebuggerSubsystem::PauseSession(const UFlowAsset& FlowAsset) { - SetPause(true); + SetPause(FlowAsset, true); } -void UFlowDebuggerSubsystem::ResumeSession() +void UFlowDebuggerSubsystem::ResumeSession(const UFlowAsset& FlowAsset) { - SetPause(false); + SetPause(FlowAsset, false); } -void UFlowDebuggerSubsystem::SetPause(const bool bPause) +void UFlowDebuggerSubsystem::SetPause(const UFlowAsset& FlowAsset, const bool bPause) { - // experimental implementation, untested, shows intent for future development - // here be dragons: same as APlayerController::SetPause, but we allow debugger to pause on clients - if (const UWorld* World = GEngine->GetWorldFromContextObject(this, EGetWorldErrorMode::LogAndReturnNull)) + // Default bWasPaused to opposite of bPause + // (which we hope to get a better measure if we can get access to what we need) + bool bWasPaused = !bPause; + + AGameModeBase* GameMode = nullptr; + APlayerController* PlayerController = nullptr; + + const UWorld* World = FlowAsset.GetWorld(); + if (IsValid(World)) { - if (const UGameInstance* GameInstance = World->GetGameInstance()) + GameMode = World->GetAuthGameMode(); + + if (IsValid(GameMode)) { - if (APlayerController* PlayerController = GameInstance->GetFirstLocalPlayerController()) + bWasPaused = GameMode->IsPaused(); + } + + const UGameInstance* GameInstance = World->GetGameInstance(); + if (IsValid(GameInstance)) + { + PlayerController = GameInstance->GetFirstLocalPlayerController(); + } + } + + if (bWasPaused != bPause) + { + if (bPause) + { + // Pausing (from an unpaused state) + + if (IsValid(PlayerController)) { - if (AGameModeBase* const GameMode = GetWorld()->GetAuthGameMode()) + if (IsValid(GameMode)) + { + GameMode->SetPause(PlayerController); + } + + if (AWorldSettings* WorldSettings = PlayerController->GetWorldSettings()) { - const bool bCurrentPauseState = PlayerController->IsPaused(); - if (bPause && !bCurrentPauseState) - { - GameMode->SetPause(PlayerController); - - if (AWorldSettings* WorldSettings = PlayerController->GetWorldSettings()) - { - WorldSettings->ForceNetUpdate(); - } - } - else if (!bPause && bCurrentPauseState) - { - if (GameMode->ClearPause()) - { - ClearHitBreakpoints(); - } - } + WorldSettings->ForceNetUpdate(); } } + + // Broadcast the Pause event + OnDebuggerPaused.Broadcast(FlowAsset); + } + else + { + // Resuming (from a paused state) + + ClearHaltFlowExecution(); + + // Replay any Flow propagation that was deferred while execution was halted. + FFlowExecutionGate::FlushDeferredTriggerInputs(); + + // Intentionally do NOT clear hit flags here. The editor-specific resume path will clear the last-hit + // breakpoint safely (without racing against immediate breakpoint hits during flush). + if (IsValid(GameMode)) + { + (void) GameMode->ClearPause(); + } + + // Broadcast the Resume event + OnDebuggerResumed.Broadcast(FlowAsset); } } } @@ -326,6 +457,9 @@ void UFlowDebuggerSubsystem::ClearHitBreakpoints() PinBreakpoint.Value.MarkAsHit(false); } } + + LastHitNodeGuid.Invalidate(); + LastHitPinName = NAME_None; } bool UFlowDebuggerSubsystem::IsBreakpointHit(const FGuid& NodeGuid) @@ -352,4 +486,4 @@ void UFlowDebuggerSubsystem::SaveSettings() { UFlowDebuggerSettings* Settings = GetMutableDefault(); Settings->SaveConfig(); -} +} \ No newline at end of file diff --git a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h index c83e004cd..2fb20e89e 100644 --- a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h @@ -5,32 +5,44 @@ #include "Subsystems/EngineSubsystem.h" #include "Debugger/FlowDebuggerTypes.h" +#include "Interfaces/FlowExecutionGate.h" + #include "FlowDebuggerSubsystem.generated.h" class UEdGraphNode; class UFlowAsset; +DECLARE_MULTICAST_DELEGATE_OneParam(FFlowAssetDebuggerEvent, const UFlowAsset& /*FlowAsset*/); +DECLARE_MULTICAST_DELEGATE_TwoParams(FFlowAssetDebuggerBreakpointHitEvent, const UFlowAsset& /*FlowAsset*/, const FGuid& /*NodeGuid*/); + /** - * Persistent subsystem supporting Flow Graph debugging. - * It might be utilized to use cook-specific graph debugger. - */ +* Persistent subsystem supporting Flow Graph debugging. +* It might be utilized to use cook-specific graph debugger. +*/ UCLASS() -class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem +class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public IFlowExecutionGate { GENERATED_BODY() public: UFlowDebuggerSubsystem(); + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; protected: virtual void OnInstancedTemplateAdded(UFlowAsset* AssetTemplate); - virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) const; + virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate); - virtual void OnPinTriggered(const FGuid& NodeGuid, const FName& PinName); + virtual void OnPinTriggered(UFlowAsset* FlowAsset, const FGuid& NodeGuid, const FName& PinName); public: + // IFlowExecutionGate + virtual bool IsFlowExecutionHalted() const override { return bHaltFlowExecution; } + // -- + virtual void AddBreakpoint(const FGuid& NodeGuid); virtual void AddBreakpoint(const FGuid& NodeGuid, const FName& PinName); @@ -56,20 +68,45 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem virtual bool IsBreakpointEnabled(const FGuid& NodeGuid, const FName& PinName); protected: - virtual void MarkAsHit(const FGuid& NodeGuid); - virtual void MarkAsHit(const FGuid& NodeGuid, const FName& PinName); - - virtual void PauseSession(); - virtual void ResumeSession(); - void SetPause(const bool bPause); + virtual void MarkAsHit(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid); + virtual void MarkAsHit(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName); + + virtual void PauseSession(const UFlowAsset& FlowAssetInstance); + virtual void ResumeSession(const UFlowAsset& FlowAssetInstance); + void SetPause(const UFlowAsset& FlowAssetInstance, const bool bPause); + + /** + * Clears the "currently hit" breakpoint only (node or pin). + * This avoids races where blanket-clearing all hit flags can erase a newly-hit breakpoint during resume/flush. + */ + void ClearLastHitBreakpoint(); + /** Clears hit state for all breakpoints. Prefer ClearLastHitBreakpoint() for resume/step logic. */ virtual void ClearHitBreakpoints(); +protected: + void RequestHaltFlowExecution(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid); + void ClearHaltFlowExecution(); + public: virtual bool IsBreakpointHit(const FGuid& NodeGuid); virtual bool IsBreakpointHit(const FGuid& NodeGuid, const FName& PinName); + // Delegates for debugger events (broadcast when pausing, resuming, or hitting breakpoints) + FFlowAssetDebuggerEvent OnDebuggerPaused; + FFlowAssetDebuggerEvent OnDebuggerResumed; + FFlowAssetDebuggerBreakpointHitEvent OnDebuggerBreakpointHit; + FFlowAssetDebuggerEvent OnDebuggerFlowAssetTemplateRemoved; + private: + bool bHaltFlowExecution = false; + TWeakObjectPtr HaltedOnFlowAssetInstance; + FGuid HaltedOnNodeGuid; + + // Track the single breakpoint location that is currently "hit" (node or pin). + FGuid LastHitNodeGuid; + FName LastHitPinName; + /** Saves any modifications made to breakpoints */ virtual void SaveSettings(); -}; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp index c794ad0e5..12e54d6a2 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp @@ -67,6 +67,11 @@ SFlowAssetInstanceList::~SFlowAssetInstanceList() void SFlowAssetInstanceList::RefreshInstances() { + if (!TemplateAsset.IsValid()) + { + return; + } + // collect instance names of this Flow Asset InstanceNames = {MakeShareable(new FName(*NoInstanceSelectedText.ToString()))}; TemplateAsset->GetInstanceDisplayNames(InstanceNames); @@ -77,7 +82,7 @@ void SFlowAssetInstanceList::RefreshInstances() const FName& InspectedInstanceName = InspectedInstance->GetDisplayName(); for (const TSharedPtr& Instance : InstanceNames) { - if (*Instance == InspectedInstanceName) + if (Instance.IsValid() && *Instance == InspectedInstanceName) { SelectedInstance = Instance; break; @@ -109,7 +114,11 @@ void SFlowAssetInstanceList::OnSelectionChanged(const TSharedPtr Selected if (TemplateAsset.IsValid()) { - const FName NewSelectedInstanceName = (SelectedInstance.IsValid() && *SelectedInstance != *InstanceNames[0]) ? *SelectedInstance : NAME_None; + const bool bIsNoInstance = + (!SelectedInstance.IsValid()) || + (InstanceNames.Num() > 0 && SelectedInstance.IsValid() && *SelectedInstance == *InstanceNames[0]); + + const FName NewSelectedInstanceName = bIsNoInstance ? NAME_None : *SelectedInstance; TemplateAsset->SetInspectedInstance(NewSelectedInstanceName); } } diff --git a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp index b30394a0e..0f0428457 100644 --- a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp +++ b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp @@ -3,11 +3,18 @@ #include "Asset/FlowDebugEditorSubsystem.h" #include "Asset/FlowAssetEditor.h" #include "Asset/FlowMessageLogListing.h" +#include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditor.h" +#include "Graph/FlowGraphUtils.h" +#include "Graph/Nodes/FlowGraphNode.h" +#include "Interfaces/FlowExecutionGate.h" +#include "FlowAsset.h" #include "Editor/UnrealEdEngine.h" #include "Engine/Engine.h" #include "Engine/World.h" #include "Framework/Notifications/NotificationManager.h" +#include "Subsystems/AssetEditorSubsystem.h" #include "Templates/Function.h" #include "UnrealEdGlobals.h" #include "Widgets/Notifications/SNotificationList.h" @@ -21,6 +28,8 @@ UFlowDebugEditorSubsystem::UFlowDebugEditorSubsystem() FEditorDelegates::BeginPIE.AddUObject(this, &ThisClass::OnBeginPIE); FEditorDelegates::ResumePIE.AddUObject(this, &ThisClass::OnResumePIE); FEditorDelegates::EndPIE.AddUObject(this, &ThisClass::OnEndPIE); + + OnDebuggerBreakpointHit.AddUObject(this, &ThisClass::OnBreakpointHit); } void UFlowDebugEditorSubsystem::OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) @@ -34,7 +43,7 @@ void UFlowDebugEditorSubsystem::OnInstancedTemplateAdded(UFlowAsset* AssetTempla } } -void UFlowDebugEditorSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) const +void UFlowDebugEditorSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) { AssetTemplate->OnRuntimeMessageAdded().RemoveAll(this); @@ -53,19 +62,34 @@ void UFlowDebugEditorSubsystem::OnRuntimeMessageAdded(const UFlowAsset* AssetTem void UFlowDebugEditorSubsystem::OnBeginPIE(const bool bIsSimulating) { - // clear all logs from a previous session + // Clear all logs from a previous session RuntimeLogs.Empty(); + + // Clear any stale "hit" state from previous run + ClearHitBreakpoints(); } void UFlowDebugEditorSubsystem::OnResumePIE(const bool bIsSimulating) { - ClearHitBreakpoints(); + // Editor-level resume event (also used by Advance Single Frame). + // This does not necessarily flow through AGameModeBase::ClearPause(), so we must unhalt Flow here. + // + // Clear only the last-hit breakpoint to return to enabled/disabled visuals without racing against + // a newly hit breakpoint during FlushDeferredTriggerInputs(). + ClearHaltFlowExecution(); + ClearLastHitBreakpoint(); + + FFlowExecutionGate::FlushDeferredTriggerInputs(); } void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) { + // Ensure we don't carry over a halted state between PIE sessions. ClearHitBreakpoints(); + ClearHaltFlowExecution(); + FFlowExecutionGate::FlushDeferredTriggerInputs(); + for (const TPair, TSharedPtr>& Log : RuntimeLogs) { if (Log.Key.IsValid() && Log.Value->NumMessages(EMessageSeverity::Warning) > 0) @@ -92,12 +116,84 @@ void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) } } -void UFlowDebugEditorSubsystem::PauseSession() +void UFlowDebugEditorSubsystem::PauseSession(const UFlowAsset& FlowAssetInstance) { - if (!GUnrealEd->SetPIEWorldsPaused(true)) + Super::PauseSession(FlowAssetInstance); + + constexpr bool bShouldBePaused = true; + const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); + if (!bWasPaused) { GUnrealEd->PlaySessionPaused(); } } -#undef LOCTEXT_NAMESPACE +void UFlowDebugEditorSubsystem::ResumeSession(const UFlowAsset& FlowAssetInstance) +{ + Super::ResumeSession(FlowAssetInstance); + + constexpr bool bShouldBePaused = false; + const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); + if (bWasPaused) + { + GUnrealEd->PlaySessionResumed(); + } +} + +void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid) const +{ + UFlowAsset* TemplateAsset = const_cast(FlowAssetInstance.GetTemplateAsset()); + if (!IsValid(TemplateAsset)) + { + return; + } + + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor ? GEditor->GetEditorSubsystem() : nullptr; + if (!AssetEditorSubsystem) + { + return; + } + + if (!AssetEditorSubsystem->OpenEditorForAsset(TemplateAsset)) + { + return; + } + + TemplateAsset->SetInspectedInstance(FlowAssetInstance.GetDisplayName()); + + UFlowGraph* FlowGraph = Cast(TemplateAsset->GetGraph()); + if (!IsValid(FlowGraph)) + { + return; + } + + // NOTE: This may be redundant call, but it ensures Slate re-queries breakpoint hit state and updates node overlays immediately. + FlowGraph->NotifyGraphChanged(); + + UEdGraphNode* NodeToFocus = nullptr; + for (UEdGraphNode* Node : FlowGraph->Nodes) + { + UFlowGraphNode* FlowGraphNode = Cast(Node); + if (IsValid(FlowGraphNode) && FlowGraphNode->NodeGuid == NodeGuid) + { + NodeToFocus = FlowGraphNode; + break; + } + } + + if (!NodeToFocus) + { + return; + } + + const TSharedPtr GraphEditor = FFlowGraphUtils::GetFlowGraphEditor(FlowGraph); + if (GraphEditor.IsValid()) + { + constexpr bool bRequestRename = false; + constexpr bool bSelectNode = true; + + GraphEditor->JumpToNode(NodeToFocus, bRequestRename, bSelectNode); + } +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp b/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp index ccdd093fb..674e035fb 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp @@ -53,6 +53,69 @@ void SFlowGraphEditor::Construct(const FArguments& InArgs, const TSharedPtrPinType.PinCategory)) + { + return false; + } + + // - If the owning node is not a UFlowGraphNode, allow it. + // - If it is a UFlowGraphNode, require it to allow breakpoints. + const UEdGraphNode* EdNode = Pin->GetOwningNode(); + if (!EdNode) + { + return false; + } + + const UFlowGraphNode* FlowNode = Cast(EdNode); + if (FlowNode && !FlowNode->CanPlaceBreakpoints()) + { + return false; + } + + OutNodeGuid = EdNode->NodeGuid; + OutPinName = Pin->PinName; + return true; +} + +const FFlowBreakpoint* SFlowGraphEditor::FindPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin) +{ + if (!InDebuggerSubsystem) + { + return nullptr; + } + + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) + { + return nullptr; + } + + return InDebuggerSubsystem->FindBreakpoint(NodeGuid, PinName); +} + +bool SFlowGraphEditor::HasPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin) +{ + return FindPinBreakpoint(InDebuggerSubsystem, Pin) != nullptr; +} + +bool SFlowGraphEditor::HasEnabledPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin) +{ + if (const FFlowBreakpoint* BP = FindPinBreakpoint(InDebuggerSubsystem, Pin)) + { + return BP->IsEnabled(); + } + + return false; +} + void SFlowGraphEditor::BindGraphCommands() { FGraphEditorCommands::Register(); @@ -1104,11 +1167,14 @@ void SFlowGraphEditor::OnAddPinBreakpoint() check(DebuggerSubsystem.IsValid()); if (const UEdGraphPin* Pin = GetGraphPinForMenu()) { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) { - DebuggerSubsystem->AddBreakpoint(Pin->GetOwningNode()->NodeGuid, Pin->PinName); + return; } + + DebuggerSubsystem->AddBreakpoint(NodeGuid, PinName); } } @@ -1134,11 +1200,14 @@ bool SFlowGraphEditor::CanAddPinBreakpoint() check(DebuggerSubsystem.IsValid()); if (const UEdGraphPin* Pin = GetGraphPinForMenu()) { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) { - return DebuggerSubsystem->FindBreakpoint(Pin->GetOwningNode()->NodeGuid, Pin->PinName) == nullptr; + return false; } + + return DebuggerSubsystem->FindBreakpoint(NodeGuid, PinName) == nullptr; } return false; @@ -1161,11 +1230,14 @@ void SFlowGraphEditor::OnRemovePinBreakpoint() check(DebuggerSubsystem.IsValid()); if (const UEdGraphPin* Pin = GetGraphPinForMenu()) { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) { - DebuggerSubsystem->RemovePinBreakpoint(Pin->GetOwningNode()->NodeGuid, Pin->PinName); + return; } + + DebuggerSubsystem->RemovePinBreakpoint(NodeGuid, PinName); } } @@ -1189,16 +1261,7 @@ bool SFlowGraphEditor::CanRemoveBreakpoint() const bool SFlowGraphEditor::CanRemovePinBreakpoint() { check(DebuggerSubsystem.IsValid()); - if (const UEdGraphPin* Pin = GetGraphPinForMenu()) - { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) - { - return DebuggerSubsystem->FindBreakpoint(Pin->GetOwningNode()->NodeGuid, Pin->PinName) != nullptr; - } - } - - return false; + return HasPinBreakpoint(DebuggerSubsystem.Get(), GetGraphPinForMenu()); } void SFlowGraphEditor::OnEnableBreakpoint() const @@ -1218,11 +1281,14 @@ void SFlowGraphEditor::OnEnablePinBreakpoint() check(DebuggerSubsystem.IsValid()); if (const UEdGraphPin* Pin = GetGraphPinForMenu()) { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) { - DebuggerSubsystem->SetBreakpointEnabled(Pin->GetOwningNode()->NodeGuid, Pin->PinName, true); + return; } + + DebuggerSubsystem->SetBreakpointEnabled(NodeGuid, PinName, true); } } @@ -1245,17 +1311,8 @@ bool SFlowGraphEditor::CanEnableBreakpoint() const bool SFlowGraphEditor::CanEnablePinBreakpoint() { - if (const UEdGraphPin* Pin = GetGraphPinForMenu()) - { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) - { - const FFlowBreakpoint* Breakpoint = DebuggerSubsystem->FindBreakpoint(Pin->GetOwningNode()->NodeGuid, Pin->PinName); - return Breakpoint && !Breakpoint->IsEnabled(); - } - } - - return false; + return HasPinBreakpoint(DebuggerSubsystem.Get(), GetGraphPinForMenu()) + && !HasEnabledPinBreakpoint(DebuggerSubsystem.Get(), GetGraphPinForMenu()); } void SFlowGraphEditor::OnDisableBreakpoint() const @@ -1275,11 +1332,14 @@ void SFlowGraphEditor::OnDisablePinBreakpoint() check(DebuggerSubsystem.IsValid()); if (const UEdGraphPin* Pin = GetGraphPinForMenu()) { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) { - DebuggerSubsystem->SetBreakpointEnabled(Pin->GetOwningNode()->NodeGuid, Pin->PinName, false); + return; } + + DebuggerSubsystem->SetBreakpointEnabled(NodeGuid, PinName, false); } } @@ -1304,17 +1364,7 @@ bool SFlowGraphEditor::CanDisableBreakpoint() const bool SFlowGraphEditor::CanDisablePinBreakpoint() { check(DebuggerSubsystem.IsValid()); - if (const UEdGraphPin* Pin = GetGraphPinForMenu()) - { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) - { - const FFlowBreakpoint* Breakpoint = DebuggerSubsystem->FindBreakpoint(Pin->GetOwningNode()->NodeGuid, Pin->PinName); - return Breakpoint && Breakpoint->IsEnabled(); - } - } - - return false; + return HasEnabledPinBreakpoint(DebuggerSubsystem.Get(), GetGraphPinForMenu()); } void SFlowGraphEditor::OnToggleBreakpoint() const @@ -1334,11 +1384,14 @@ void SFlowGraphEditor::OnTogglePinBreakpoint() check(DebuggerSubsystem.IsValid()); if (const UEdGraphPin* Pin = GetGraphPinForMenu()) { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) { - DebuggerSubsystem->ToggleBreakpoint(Pin->GetOwningNode()->NodeGuid, Pin->PinName); + return; } + + DebuggerSubsystem->ToggleBreakpoint(NodeGuid, PinName); } } @@ -1359,11 +1412,9 @@ bool SFlowGraphEditor::CanTogglePinBreakpoint() { if (const UEdGraphPin* Pin = GetGraphPinForMenu()) { - const UFlowGraphNode* OwningNode = Cast(Pin->GetOwningNode()); - if (!OwningNode || OwningNode->CanPlaceBreakpoints()) - { - return true; - } + FGuid NodeGuid; + FName PinName; + return GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName); } return false; diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp index 420b5bdc7..211efdd24 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp @@ -15,6 +15,8 @@ #include "Graph/FlowGraphSettings.h" #include "Graph/Widgets/SFlowGraphNode.h" #include "Graph/Widgets/SGraphEditorActionMenuFlow.h" +#include "Interfaces/FlowDataPinValueSupplierInterface.h" +#include "Types/FlowDataPinValue.h" #include "BlueprintNodeHelpers.h" #include "Developer/ToolMenus/Public/ToolMenus.h" @@ -1082,8 +1084,10 @@ void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextO // start with the default hover text (from the pin's tool-tip) Super::GetPinHoverText(Pin, HoverTextOut); + const bool bHasValidPlayWorld = IsValid(GEditor->PlayWorld); + // add information on pin activations - if (GEditor->PlayWorld) + if (bHasValidPlayWorld) { if (const UFlowNode* InspectedNodeInstance = GetInspectedNodeInstance()) { @@ -1093,11 +1097,7 @@ void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextO } const TArray& PinRecords = InspectedNodeInstance->GetPinRecords(Pin.PinName, Pin.Direction); - if (PinRecords.Num() == 0) - { - HoverTextOut.Append(FPinRecord::NoActivations); - } - else + if (PinRecords.Num() > 0) { HoverTextOut.Append(FPinRecord::PinActivations); for (int32 i = 0; i < PinRecords.Num(); i++) @@ -1107,22 +1107,78 @@ void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextO switch (PinRecords[i].ActivationType) { - case EFlowPinActivationType::Default: - break; - case EFlowPinActivationType::Forced: - HoverTextOut.Append(FPinRecord::ForcedActivation); - break; - case EFlowPinActivationType::PassThrough: - HoverTextOut.Append(FPinRecord::PassThroughActivation); - break; - default: ; + case EFlowPinActivationType::Default: + break; + case EFlowPinActivationType::Forced: + HoverTextOut.Append(FPinRecord::ForcedActivation); + break; + case EFlowPinActivationType::PassThrough: + HoverTextOut.Append(FPinRecord::PassThroughActivation); + break; + default:; } } } } } -} + // add information on data pin values (only for data pins) + const bool bIsDataPinCategory = !FFlowPin::IsExecPinCategory(Pin.PinType.PinCategory); + if (bIsDataPinCategory) + { + const UEdGraphPin* GraphPinObj = &Pin; + + // Prefer showing runtime values when PIE (consistent with activation history) + const UFlowNodeBase* FlowNodeBase = GetFlowNodeBase(); + + if (bHasValidPlayWorld) + { + FlowNodeBase = GetInspectedNodeInstance(); + } + + FFlowDataPinResult DataResult(EFlowDataPinResolveResult::FailedNullFlowNodeBase); + + if (IsValid(FlowNodeBase)) + { + if (GraphPinObj->Direction == EGPD_Input) + { + // Input pins: do a pin resolve to source the value + DataResult = FlowNodeBase->TryResolveDataPin(GraphPinObj->PinName); + } + else + { + // Output pins: ask this node what it supplies for that output data pin + const UFlowNode* FlowNode = Cast(FlowNodeBase); + if (FlowNode) + { + DataResult = IFlowDataPinValueSupplierInterface::Execute_TrySupplyDataPin(FlowNode, GraphPinObj->PinName); + } + } + } + + FString ValueString; + + if (FlowPinType::IsSuccess(DataResult.Result) && DataResult.ResultValue.IsValid()) + { + const FFlowDataPinValue& Value = DataResult.ResultValue.Get(); + if (!Value.TryConvertValuesToString(ValueString)) + { + ValueString = TEXT(""); + } + } + else + { + ValueString = TEXT(""); + } + + if (!HoverTextOut.IsEmpty()) + { + HoverTextOut.Append(LINE_TERMINATOR).Append(LINE_TERMINATOR); + } + + HoverTextOut.Appendf(TEXT("Value: %s"), *ValueString); + } +} void UFlowGraphNode::ForcePinActivation(const FEdGraphPinReference PinReference) const { @@ -1990,4 +2046,4 @@ bool UFlowGraphNode::CanAcceptSubNodeAsChild(const UFlowGraphNode& OtherSubNode, return false; } -#undef LOCTEXT_NAMESPACE +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Public/Asset/FlowAssetEditor.h b/Source/FlowEditor/Public/Asset/FlowAssetEditor.h index 8c479b8e0..8c10beb1d 100644 --- a/Source/FlowEditor/Public/Asset/FlowAssetEditor.h +++ b/Source/FlowEditor/Public/Asset/FlowAssetEditor.h @@ -121,7 +121,7 @@ class FLOWEDITOR_API FFlowAssetEditor : public FAssetEditorToolkit, public FEdit public: /** Edits the specified FlowAsset object */ - void InitFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UObject* ObjectToEdit); + virtual void InitFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UObject* ObjectToEdit); protected: virtual void CreateToolbar(); diff --git a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h index 3cbe13fa8..41e9230ec 100644 --- a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h +++ b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h @@ -25,7 +25,7 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem TMap, TSharedPtr> RuntimeLogs; virtual void OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) override; - virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) const override; + virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) override; void OnRuntimeMessageAdded(const UFlowAsset* AssetTemplate, const TSharedRef& Message) const; @@ -33,5 +33,8 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem virtual void OnResumePIE(const bool bIsSimulating); virtual void OnEndPIE(const bool bIsSimulating); - virtual void PauseSession() override; -}; + virtual void PauseSession(const UFlowAsset& FlowAssetInstance) override; + virtual void ResumeSession(const UFlowAsset& FlowAssetInstance) override; + + void OnBreakpointHit(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid) const; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Graph/FlowGraphEditor.h b/Source/FlowEditor/Public/Graph/FlowGraphEditor.h index 331ebb1e1..739ecb8a2 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphEditor.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphEditor.h @@ -10,7 +10,9 @@ class FFlowAssetEditor; class IDetailsView; +class UEdGraphPin; class UFlowDebuggerSubsystem; +struct FFlowBreakpoint; /** * @@ -107,6 +109,12 @@ class FLOWEDITOR_API SFlowGraphEditor : public SGraphEditor virtual void ReconstructNode() const; virtual bool CanReconstructNode() const; + // ---- Pin breakpoint helpers ---- + static bool GetValidExecBreakpointPinContext(const UEdGraphPin* Pin, FGuid& OutNodeGuid, FName& OutPinName); + static const FFlowBreakpoint* FindPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin); + static bool HasPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin); + static bool HasEnabledPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin); + private: void AddInput() const; bool CanAddInput() const; From 8e4fcf64dc5f877b5e8a15d63448ec50608778fe Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:11:34 -0800 Subject: [PATCH 03/10] Reverted this fix for PR --- Source/Flow/Public/Types/FlowNamedDataPinProperty.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Flow/Public/Types/FlowNamedDataPinProperty.h b/Source/Flow/Public/Types/FlowNamedDataPinProperty.h index 0b6031186..939b67fa1 100644 --- a/Source/Flow/Public/Types/FlowNamedDataPinProperty.h +++ b/Source/Flow/Public/Types/FlowNamedDataPinProperty.h @@ -27,7 +27,7 @@ struct FFlowNamedDataPinProperty private: // DataPinProperty payload - UPROPERTY(meta = (DeprecatedProperty)) + UPROPERTY(VisibleAnywhere, Category = DataPins, meta = (DeprecatedProperty)) TInstancedStruct DataPinProperty; public: From 15fb2809b37aa6b9041e89f542f9713970935514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Justy=C5=84ski?= Date: Tue, 27 Jan 2026 22:08:21 +0100 Subject: [PATCH 04/10] restore changes from other PRs --- .../Private/Asset/FlowAssetToolbar.cpp | 38 +------------------ Source/FlowEditor/Public/FlowEditorModule.h | 5 --- 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp index 4cb239390..4ef7e0220 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp @@ -92,37 +92,6 @@ SFlowAssetInstanceList::~SFlowAssetInstanceList() } } -void SFlowAssetInstanceList::RefreshInstances() -{ - if (!TemplateAsset.IsValid()) - { - return; - } - - // collect instance names of this Flow Asset - InstanceNames = {MakeShareable(new FName(*NoInstanceSelectedText.ToString()))}; - TemplateAsset->GetInstanceDisplayNames(InstanceNames); - - // select instance - if (const UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance()) - { - const FName& InspectedInstanceName = InspectedInstance->GetDisplayName(); - for (const TSharedPtr& Instance : InstanceNames) - { - if (Instance.IsValid() && *Instance == InspectedInstanceName) - { - SelectedInstance = Instance; - break; - } - } - } - else - { - // default object is always available - SelectedInstance = InstanceNames[0]; - } -} - EVisibility SFlowAssetInstanceList::GetDebuggerVisibility() { return GEditor->PlayWorld ? EVisibility::Visible : EVisibility::Collapsed; @@ -258,12 +227,7 @@ void SFlowAssetInstanceList::OnInstanceSelectionChanged(const TSharedPtr(SelectedInstance->ResolveObjectPtr()); if (TemplateAsset.IsValid()) { - const bool bIsNoInstance = - (!SelectedInstance.IsValid()) || - (InstanceNames.Num() > 0 && SelectedInstance.IsValid() && *SelectedInstance == *InstanceNames[0]); - - const FName NewSelectedInstanceName = bIsNoInstance ? NAME_None : *SelectedInstance; - TemplateAsset->SetInspectedInstance(NewSelectedInstanceName); + TemplateAsset->SetInspectedInstance(Instance); } } } diff --git a/Source/FlowEditor/Public/FlowEditorModule.h b/Source/FlowEditor/Public/FlowEditorModule.h index 16370e74c..9e32e6605 100644 --- a/Source/FlowEditor/Public/FlowEditorModule.h +++ b/Source/FlowEditor/Public/FlowEditorModule.h @@ -32,11 +32,6 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface, public IHasMen TSet CustomStructLayouts; bool bIsRegisteredForAssetChanges = false; - TSharedPtr MenuExtensibilityManager; - TSharedPtr ToolBarExtensibilityManager; - - bool bIsRegisteredForAssetChanges = false; - public: virtual void StartupModule() override; virtual void ShutdownModule() override; From ee0bcffc4029d925c0ad973b6d37f8e3c2c2e642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Justy=C5=84ski?= Date: Tue, 27 Jan 2026 22:30:12 +0100 Subject: [PATCH 05/10] integrated usage of Flow Node as main parameter passed with breakpoint hit --- Source/Flow/Private/Nodes/FlowNode.cpp | 10 +- Source/Flow/Public/FlowAsset.h | 2 +- .../Debugger/FlowDebuggerSubsystem.cpp | 136 +++++++++--------- .../Public/Debugger/FlowDebuggerSubsystem.h | 16 +-- .../Asset/FlowDebugEditorSubsystem.cpp | 20 +-- .../Public/Asset/FlowDebugEditorSubsystem.h | 6 +- 6 files changed, 91 insertions(+), 99 deletions(-) diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index 8685f8345..753f36e93 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -894,10 +894,9 @@ void UFlowNode::TriggerInput(const FName& PinName, const EFlowPinActivationType TArray& Records = InputRecords.FindOrAdd(PinName); Records.Add(FPinRecord(FApp::GetCurrentTime(), ActivationType)); - UFlowAsset* FlowAssetInstance = GetFlowAsset(); - if (const UFlowAsset* FlowAssetTemplate = FlowAssetInstance->GetTemplateAsset()) + if (const UFlowAsset* FlowAssetTemplate = GetFlowAsset()->GetTemplateAsset()) { - (void) FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(FlowAssetInstance, NodeGuid, PinName); + (void)FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(this, PinName); } #endif } @@ -961,10 +960,9 @@ void UFlowNode::TriggerOutput(const FName PinName, const bool bFinish /*= false* TArray& Records = OutputRecords.FindOrAdd(PinName); Records.Add(FPinRecord(FApp::GetCurrentTime(), ActivationType)); - UFlowAsset* FlowAssetInstance = GetFlowAsset(); - if (const UFlowAsset* FlowAssetTemplate = FlowAssetInstance->GetTemplateAsset()) + if (const UFlowAsset* FlowAssetTemplate = GetFlowAsset()->GetTemplateAsset()) { - FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(FlowAssetInstance, NodeGuid, PinName); + FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(this, PinName); } } else diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index 1414afd49..bd16210e7 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -26,7 +26,7 @@ class UFlowAssetParams; #if !UE_BUILD_SHIPPING DECLARE_DELEGATE(FFlowGraphEvent); -DECLARE_DELEGATE_ThreeParams(FFlowSignalEvent, UFlowAsset* /*FlowAsset*/, const FGuid& /*NodeGuid*/, const FName& /*PinName*/); +DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, UFlowNode* /*FlowNode*/, const FName& /*PinName*/); #endif /** diff --git a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp index 0578516a7..086b40b5f 100644 --- a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp @@ -68,17 +68,15 @@ void UFlowDebuggerSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplat OnDebuggerFlowAssetTemplateRemoved.Broadcast(*AssetTemplate); } -void UFlowDebuggerSubsystem::OnPinTriggered(UFlowAsset* FlowAsset, const FGuid& NodeGuid, const FName& PinName) +void UFlowDebuggerSubsystem::OnPinTriggered(UFlowNode* FlowNode, const FName& PinName) { - check(IsValid(FlowAsset)); - - if (FindBreakpoint(NodeGuid, PinName)) + if (FindBreakpoint(FlowNode->NodeGuid, PinName)) { - MarkAsHit(*FlowAsset, NodeGuid, PinName); + MarkAsHit(FlowNode, PinName); } // Node breakpoints waits on any pin triggered - MarkAsHit(*FlowAsset, NodeGuid); + MarkAsHit(FlowNode); } void UFlowDebuggerSubsystem::AddBreakpoint(const FGuid& NodeGuid) @@ -328,48 +326,7 @@ bool UFlowDebuggerSubsystem::IsBreakpointEnabled(const FGuid& NodeGuid, const FN return false; } -void UFlowDebuggerSubsystem::RequestHaltFlowExecution(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid) -{ - bHaltFlowExecution = true; - HaltedOnFlowAssetInstance = &FlowAssetInstance; - HaltedOnNodeGuid = NodeGuid; -} - -void UFlowDebuggerSubsystem::ClearHaltFlowExecution() -{ - bHaltFlowExecution = false; - HaltedOnFlowAssetInstance.Reset(); - HaltedOnNodeGuid.Invalidate(); -} - -void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() -{ - if (!LastHitNodeGuid.IsValid()) - { - return; - } - - // Pin breakpoint "hit" state lives in the PinBreakpoints map, node breakpoint "hit" lives on NodeBreakpoint.Breakpoint. - if (!LastHitPinName.IsNone()) - { - if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(LastHitNodeGuid, LastHitPinName)) - { - PinBreakpoint->MarkAsHit(false); - } - } - else - { - if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(LastHitNodeGuid)) - { - NodeBreakpoint->MarkAsHit(false); - } - } - - LastHitNodeGuid.Invalidate(); - LastHitPinName = NAME_None; -} - -void UFlowDebuggerSubsystem::MarkAsHit(const UFlowAsset& FlowAsset, const FGuid& NodeGuid) +bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr FlowAsset) { UFlowDebuggerSettings* Settings = GetMutableDefault(); for (const TPair& Node : FlowAsset->GetNodes()) @@ -419,9 +376,9 @@ bool UFlowDebuggerSubsystem::HasAnyBreakpointsDisabled(const TWeakObjectPtrNodeGuid)) + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(FlowNode->NodeGuid)) { if (NodeBreakpoint->IsEnabled()) { @@ -430,23 +387,21 @@ bool UFlowDebuggerSubsystem::TryMarkAsHit(const UFlowNode* Node) NodeBreakpoint->MarkAsHit(true); - LastHitNodeGuid = NodeGuid; + LastHitNodeGuid = FlowNode->NodeGuid; LastHitPinName = NAME_None; - RequestHaltFlowExecution(FlowAsset, NodeGuid); + RequestHaltFlowExecution(FlowNode); - OnDebuggerBreakpointHit.Broadcast(FlowAsset, NodeGuid); + OnDebuggerBreakpointHit.Broadcast(FlowNode); - PauseSession(FlowAsset); + PauseSession(*FlowNode); } } - - return false; } -void UFlowDebuggerSubsystem::MarkAsHit(const UFlowAsset& FlowAsset, const FGuid& NodeGuid, const FName& PinName) +void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode, const FName& PinName) { - if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(Node->NodeGuid, PinName)) + if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(FlowNode->NodeGuid, PinName)) { if (PinBreakpoint->IsEnabled()) { @@ -455,31 +410,69 @@ void UFlowDebuggerSubsystem::MarkAsHit(const UFlowAsset& FlowAsset, const FGuid& PinBreakpoint->MarkAsHit(true); - LastHitNodeGuid = NodeGuid; + LastHitNodeGuid = FlowNode->NodeGuid; LastHitPinName = PinName; - RequestHaltFlowExecution(FlowAsset, NodeGuid); + RequestHaltFlowExecution(FlowNode); + OnDebuggerBreakpointHit.Broadcast(FlowNode); + + PauseSession(*FlowNode); + } + } +} + +void UFlowDebuggerSubsystem::RequestHaltFlowExecution(const UFlowNode* Node) +{ + bHaltFlowExecution = true; + HaltedOnFlowAssetInstance = Node->GetFlowAsset(); + HaltedOnNodeGuid = Node->NodeGuid; +} - OnDebuggerBreakpointHit.Broadcast(FlowAsset, NodeGuid); +void UFlowDebuggerSubsystem::ClearHaltFlowExecution() +{ + bHaltFlowExecution = false; + HaltedOnFlowAssetInstance.Reset(); + HaltedOnNodeGuid.Invalidate(); +} - PauseSession(FlowAsset); +void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() +{ + if (!LastHitNodeGuid.IsValid()) + { + return; + } + + // Pin breakpoint "hit" state lives in the PinBreakpoints map, node breakpoint "hit" lives on NodeBreakpoint.Breakpoint. + if (!LastHitPinName.IsNone()) + { + if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(LastHitNodeGuid, LastHitPinName)) + { + PinBreakpoint->MarkAsHit(false); + } + } + else + { + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(LastHitNodeGuid)) + { + NodeBreakpoint->MarkAsHit(false); } } - return false; + LastHitNodeGuid.Invalidate(); + LastHitPinName = NAME_None; } -void UFlowDebuggerSubsystem::PauseSession(const UFlowAsset& FlowAsset) +void UFlowDebuggerSubsystem::PauseSession(const UFlowNode& FlowNode) { - SetPause(FlowAsset, true); + SetPause(FlowNode, true); } -void UFlowDebuggerSubsystem::ResumeSession(const UFlowAsset& FlowAsset) +void UFlowDebuggerSubsystem::ResumeSession(const UFlowNode& FlowNode) { - SetPause(FlowAsset, false); + SetPause(FlowNode, false); } -void UFlowDebuggerSubsystem::SetPause(const UFlowAsset& FlowAsset, const bool bPause) +void UFlowDebuggerSubsystem::SetPause(const UFlowNode& FlowNode, const bool bPause) { // Default bWasPaused to opposite of bPause // (which we hope to get a better measure if we can get access to what we need) @@ -488,7 +481,8 @@ void UFlowDebuggerSubsystem::SetPause(const UFlowAsset& FlowAsset, const bool bP AGameModeBase* GameMode = nullptr; APlayerController* PlayerController = nullptr; - const UWorld* World = FlowAsset.GetWorld(); + const UFlowAsset* FlowAssetInstance = FlowNode.GetFlowAsset(); + const UWorld* World = FlowAssetInstance->GetWorld(); if (IsValid(World)) { GameMode = World->GetAuthGameMode(); @@ -525,7 +519,7 @@ void UFlowDebuggerSubsystem::SetPause(const UFlowAsset& FlowAsset, const bool bP } // Broadcast the Pause event - OnDebuggerPaused.Broadcast(FlowAsset); + OnDebuggerPaused.Broadcast(*FlowAssetInstance); } else { @@ -544,7 +538,7 @@ void UFlowDebuggerSubsystem::SetPause(const UFlowAsset& FlowAsset, const bool bP } // Broadcast the Resume event - OnDebuggerResumed.Broadcast(FlowAsset); + OnDebuggerResumed.Broadcast(*FlowAssetInstance); } } } diff --git a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h index 3b92114ba..a5f0027c1 100644 --- a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h @@ -15,7 +15,7 @@ class UFlowAsset; class UFlowNode; DECLARE_MULTICAST_DELEGATE_OneParam(FFlowAssetDebuggerEvent, const UFlowAsset& /*FlowAsset*/); -DECLARE_MULTICAST_DELEGATE_TwoParams(FFlowAssetDebuggerBreakpointHitEvent, const UFlowAsset& /*FlowAsset*/, const FGuid& /*NodeGuid*/); +DECLARE_MULTICAST_DELEGATE_OneParam(FFlowAssetDebuggerBreakpointHitEvent, const UFlowNode* /*FlowNode*/); /** * Persistent subsystem supporting Flow Graph debugging. @@ -41,7 +41,7 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public virtual void OnInstancedTemplateAdded(UFlowAsset* AssetTemplate); virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate); - virtual void OnPinTriggered(UFlowAsset* FlowAsset, const FGuid& NodeGuid, const FName& PinName); + virtual void OnPinTriggered(UFlowNode* FlowNode, const FName& PinName); public: // IFlowExecutionGate @@ -78,12 +78,12 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public static bool HasAnyBreakpointsDisabled(const TWeakObjectPtr FlowAsset); protected: - virtual void MarkAsHit(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid); - virtual void MarkAsHit(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName); + virtual void MarkAsHit(const UFlowNode* FlowNode); + virtual void MarkAsHit(const UFlowNode* FlowNode, const FName& PinName); - virtual void PauseSession(const UFlowAsset& FlowAssetInstance); - virtual void ResumeSession(const UFlowAsset& FlowAssetInstance); - void SetPause(const UFlowAsset& FlowAssetInstance, const bool bPause); + virtual void PauseSession(const UFlowNode& FlowNode); + virtual void ResumeSession(const UFlowNode& FlowNode); + void SetPause(const UFlowNode& FlowNode, const bool bPause); /** * Clears the "currently hit" breakpoint only (node or pin). @@ -95,7 +95,7 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public virtual void ClearHitBreakpoints(); protected: - void RequestHaltFlowExecution(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid); + void RequestHaltFlowExecution(const UFlowNode* Node); void ClearHaltFlowExecution(); public: diff --git a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp index 80f1d7e15..09cee9ff7 100644 --- a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp +++ b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp @@ -116,9 +116,9 @@ void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) } } -void UFlowDebugEditorSubsystem::PauseSession(const UFlowAsset& FlowAssetInstance) +void UFlowDebugEditorSubsystem::PauseSession(const UFlowNode& FlowNode) { - Super::PauseSession(FlowAssetInstance); + Super::PauseSession(FlowNode); constexpr bool bShouldBePaused = true; const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); @@ -131,7 +131,7 @@ void UFlowDebugEditorSubsystem::PauseSession(const UFlowAsset& FlowAssetInstance { bPausedAtFlowBreakpoint = true; - const UFlowAsset* HitInstance = Node->GetFlowAsset(); + const UFlowAsset* HitInstance = FlowNode.GetFlowAsset(); if (ensure(HitInstance)) { UFlowAsset* AssetTemplate = HitInstance->GetTemplateAsset(); @@ -142,7 +142,7 @@ void UFlowDebugEditorSubsystem::PauseSession(const UFlowAsset& FlowAssetInstance { if (const TSharedPtr FlowAssetEditor = FFlowGraphUtils::GetFlowAssetEditor(AssetTemplate)) { - FlowAssetEditor->JumpToNode(Node->GetGraphNode()); + FlowAssetEditor->JumpToNode(FlowNode.GetGraphNode()); } } } @@ -151,9 +151,9 @@ void UFlowDebugEditorSubsystem::PauseSession(const UFlowAsset& FlowAssetInstance } } -void UFlowDebugEditorSubsystem::ResumeSession(const UFlowAsset& FlowAssetInstance) +void UFlowDebugEditorSubsystem::ResumeSession(const UFlowNode& FlowNode) { - Super::ResumeSession(FlowAssetInstance); + Super::ResumeSession(FlowNode); constexpr bool bShouldBePaused = false; const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); @@ -163,9 +163,9 @@ void UFlowDebugEditorSubsystem::ResumeSession(const UFlowAsset& FlowAssetInstanc } } -void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid) const +void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowNode* FlowNode) const { - UFlowAsset* TemplateAsset = const_cast(FlowAssetInstance.GetTemplateAsset()); + UFlowAsset* TemplateAsset = const_cast(FlowNode->GetFlowAsset()->GetTemplateAsset()); if (!IsValid(TemplateAsset)) { return; @@ -182,7 +182,7 @@ void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowAsset& FlowAssetInsta return; } - TemplateAsset->SetInspectedInstance(FlowAssetInstance.GetDisplayName()); + TemplateAsset->SetInspectedInstance(FlowNode->GetFlowAsset()); UFlowGraph* FlowGraph = Cast(TemplateAsset->GetGraph()); if (!IsValid(FlowGraph)) @@ -197,7 +197,7 @@ void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowAsset& FlowAssetInsta for (UEdGraphNode* Node : FlowGraph->Nodes) { UFlowGraphNode* FlowGraphNode = Cast(Node); - if (IsValid(FlowGraphNode) && FlowGraphNode->NodeGuid == NodeGuid) + if (IsValid(FlowGraphNode) && FlowGraphNode->NodeGuid == FlowNode->NodeGuid) { NodeToFocus = FlowGraphNode; break; diff --git a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h index a1788fa13..4b2f839be 100644 --- a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h +++ b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h @@ -33,8 +33,8 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem virtual void OnResumePIE(const bool bIsSimulating); virtual void OnEndPIE(const bool bIsSimulating); - virtual void PauseSession(const UFlowAsset& FlowAssetInstance) override; - virtual void ResumeSession(const UFlowAsset& FlowAssetInstance) override; + virtual void PauseSession(const UFlowNode& FlowNode) override; + virtual void ResumeSession(const UFlowNode& FlowNode) override; - void OnBreakpointHit(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid) const; + void OnBreakpointHit(const UFlowNode* FlowNode) const; }; From a5df9946444a5d52cbe9cbb27de92717aa86a6af Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:40:32 -0800 Subject: [PATCH 06/10] Integrations with our version --- Source/Flow/Public/AddOns/FlowNodeAddOn.h | 2 +- .../Debugger/FlowDebuggerSubsystem.cpp | 108 ++++++----- .../Public/Debugger/FlowDebuggerSubsystem.h | 8 +- .../Private/Asset/FlowAssetToolbar.cpp | 167 +++++++----------- .../Asset/FlowDebugEditorSubsystem.cpp | 25 +-- .../Private/Graph/FlowGraphEditor.cpp | 3 + Source/FlowEditor/Public/FlowEditorModule.h | 1 - 7 files changed, 124 insertions(+), 190 deletions(-) diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn.h b/Source/Flow/Public/AddOns/FlowNodeAddOn.h index aabea5c4d..e2d2931be 100644 --- a/Source/Flow/Public/AddOns/FlowNodeAddOn.h +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn.h @@ -41,7 +41,7 @@ class UFlowNodeAddOn : public UFlowNodeBase // UFlowNodeBase #if WITH_EDITOR - virtual UEdGraphNode* GetGraphNode() const override; + FLOW_API virtual UEdGraphNode* GetGraphNode() const override; #endif // AddOns may opt in to be eligible for a given parent diff --git a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp index 0578516a7..38a30bb96 100644 --- a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp @@ -16,7 +16,6 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDebuggerSubsystem) UFlowDebuggerSubsystem::UFlowDebuggerSubsystem() - : bPausedAtFlowBreakpoint(false) { UFlowSubsystem::OnInstancedTemplateAdded.BindUObject(this, &ThisClass::OnInstancedTemplateAdded); UFlowSubsystem::OnInstancedTemplateRemoved.BindUObject(this, &ThisClass::OnInstancedTemplateRemoved); @@ -371,57 +370,7 @@ void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() void UFlowDebuggerSubsystem::MarkAsHit(const UFlowAsset& FlowAsset, const FGuid& NodeGuid) { - UFlowDebuggerSettings* Settings = GetMutableDefault(); - for (const TPair& Node : FlowAsset->GetNodes()) - { - if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(Node.Key)) - { - if (NodeBreakpoint->Breakpoint.IsActive() && NodeBreakpoint->Breakpoint.IsEnabled()) - { - return true; - } - - for (auto& [Name, PinBreakpoint] : NodeBreakpoint->PinBreakpoints) - { - if (PinBreakpoint.IsEnabled()) - { - return true; - } - } - } - } - - return false; -} - -bool UFlowDebuggerSubsystem::HasAnyBreakpointsDisabled(const TWeakObjectPtr FlowAsset) -{ - UFlowDebuggerSettings* Settings = GetMutableDefault(); - for (const TPair& Node : FlowAsset->GetNodes()) - { - if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(Node.Key)) - { - if (NodeBreakpoint->Breakpoint.IsActive() && !NodeBreakpoint->Breakpoint.IsEnabled()) - { - return true; - } - - for (auto& [Name, PinBreakpoint] : NodeBreakpoint->PinBreakpoints) - { - if (!PinBreakpoint.IsEnabled()) - { - return true; - } - } - } - } - - return false; -} - -bool UFlowDebuggerSubsystem::TryMarkAsHit(const UFlowNode* Node) -{ - if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(Node->NodeGuid)) + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(NodeGuid)) { if (NodeBreakpoint->IsEnabled()) { @@ -440,13 +389,11 @@ bool UFlowDebuggerSubsystem::TryMarkAsHit(const UFlowNode* Node) PauseSession(FlowAsset); } } - - return false; } void UFlowDebuggerSubsystem::MarkAsHit(const UFlowAsset& FlowAsset, const FGuid& NodeGuid, const FName& PinName) { - if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(Node->NodeGuid, PinName)) + if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(NodeGuid, PinName)) { if (PinBreakpoint->IsEnabled()) { @@ -465,8 +412,54 @@ void UFlowDebuggerSubsystem::MarkAsHit(const UFlowAsset& FlowAsset, const FGuid& PauseSession(FlowAsset); } } +} - return false; +bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr& FlowAsset) +{ + return HasAnyBreakpointsMatching(FlowAsset, true); +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpointsDisabled(const TWeakObjectPtr& FlowAsset) +{ + return HasAnyBreakpointsMatching(FlowAsset, false); +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpointsMatching(const TWeakObjectPtr& FlowAsset, bool bDesiresEnabled) +{ + if (!FlowAsset.IsValid()) + { + return false; + } + + const UFlowDebuggerSettings* Settings = GetDefault(); + if (!Settings) + { + return false; + } + + for (const TPair& NodePair : FlowAsset->GetNodes()) + { + if (const FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(NodePair.Key)) + { + // Node-level breakpoint must be active to count (matches original behavior) + if (NodeBreakpoint->Breakpoint.IsActive() && + (NodeBreakpoint->Breakpoint.IsEnabled() == bDesiresEnabled)) + { + return true; + } + + // Pin-level breakpoints + for (const auto& PinPair : NodeBreakpoint->PinBreakpoints) + { + if (PinPair.Value.IsEnabled() == bDesiresEnabled) + { + return true; + } + } + } + } + + return false; } void UFlowDebuggerSubsystem::PauseSession(const UFlowAsset& FlowAsset) @@ -551,9 +544,8 @@ void UFlowDebuggerSubsystem::SetPause(const UFlowAsset& FlowAsset, const bool bP void UFlowDebuggerSubsystem::ClearHitBreakpoints() { - bPausedAtFlowBreakpoint = false; - UFlowDebuggerSettings* Settings = GetMutableDefault(); + for (TPair& NodeBreakpoint : Settings->NodeBreakpoints) { NodeBreakpoint.Value.Breakpoint.MarkAsHit(false); diff --git a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h index 3b92114ba..1b1878d20 100644 --- a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h @@ -34,9 +34,6 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public virtual bool ShouldCreateSubsystem(UObject* Outer) const override; -protected: - bool bPausedAtFlowBreakpoint; - protected: virtual void OnInstancedTemplateAdded(UFlowAsset* AssetTemplate); virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate); @@ -74,8 +71,9 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public virtual bool IsBreakpointEnabled(const FGuid& NodeGuid); virtual bool IsBreakpointEnabled(const FGuid& NodeGuid, const FName& PinName); - static bool HasAnyBreakpointsEnabled(const TWeakObjectPtr FlowAsset); - static bool HasAnyBreakpointsDisabled(const TWeakObjectPtr FlowAsset); + static bool HasAnyBreakpointsEnabled(const TWeakObjectPtr& FlowAsset); + static bool HasAnyBreakpointsDisabled(const TWeakObjectPtr& FlowAsset); + static bool HasAnyBreakpointsMatching(const TWeakObjectPtr& FlowAsset, bool bDesiresEnabled); protected: virtual void MarkAsHit(const UFlowAsset& FlowAssetInstance, const FGuid& NodeGuid); diff --git a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp index 4cb239390..0fd6ce003 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp @@ -11,6 +11,7 @@ #include "FlowAsset.h" #include "Nodes/Graph/FlowNode_SubGraph.h" +#include "Brushes/SlateRoundedBoxBrush.h" #include "Kismet2/DebuggerCommands.h" #include "Misc/Attribute.h" #include "Misc/MessageDialog.h" @@ -52,7 +53,7 @@ void SFlowAssetInstanceList::Construct(const FArguments& InArgs, const TWeakObje .ContentPadding(FMargin(0.f, 2.f)) [ SNew(STextBlock) - .Text(this, &SFlowAssetInstanceList::GetSelectedContextName) + .Text(this, &SFlowAssetInstanceList::GetSelectedContextName) ]; InstanceComboBox = SNew(SComboBox>) @@ -62,26 +63,26 @@ void SFlowAssetInstanceList::Construct(const FArguments& InArgs, const TWeakObje .ContentPadding(FMargin(0.f, 2.f)) [ SNew(STextBlock) - .Text(this, &SFlowAssetInstanceList::GetSelectedInstanceName) + .Text(this, &SFlowAssetInstanceList::GetSelectedInstanceName) ]; ChildSlot - [ - SNew(SHorizontalBox) - .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) - + SHorizontalBox::Slot() - .AutoWidth() - .Padding(0.0f, 0.0f, 8.0f, 0.0f) - [ - ContextComboBox.ToSharedRef() - ] - + SHorizontalBox::Slot() - .AutoWidth() - .Padding(0.0f, 0.0f, 4.0f, 0.0f) [ - InstanceComboBox.ToSharedRef() - ] - ]; + SNew(SHorizontalBox) + .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + ContextComboBox.ToSharedRef() + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + InstanceComboBox.ToSharedRef() + ] + ]; } SFlowAssetInstanceList::~SFlowAssetInstanceList() @@ -92,37 +93,6 @@ SFlowAssetInstanceList::~SFlowAssetInstanceList() } } -void SFlowAssetInstanceList::RefreshInstances() -{ - if (!TemplateAsset.IsValid()) - { - return; - } - - // collect instance names of this Flow Asset - InstanceNames = {MakeShareable(new FName(*NoInstanceSelectedText.ToString()))}; - TemplateAsset->GetInstanceDisplayNames(InstanceNames); - - // select instance - if (const UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance()) - { - const FName& InspectedInstanceName = InspectedInstance->GetDisplayName(); - for (const TSharedPtr& Instance : InstanceNames) - { - if (Instance.IsValid() && *Instance == InspectedInstanceName) - { - SelectedInstance = Instance; - break; - } - } - } - else - { - // default object is always available - SelectedInstance = InstanceNames[0]; - } -} - EVisibility SFlowAssetInstanceList::GetDebuggerVisibility() { return GEditor->PlayWorld ? EVisibility::Visible : EVisibility::Collapsed; @@ -258,12 +228,7 @@ void SFlowAssetInstanceList::OnInstanceSelectionChanged(const TSharedPtr(SelectedInstance->ResolveObjectPtr()); if (TemplateAsset.IsValid()) { - const bool bIsNoInstance = - (!SelectedInstance.IsValid()) || - (InstanceNames.Num() > 0 && SelectedInstance.IsValid() && *SelectedInstance == *InstanceNames[0]); - - const FName NewSelectedInstanceName = bIsNoInstance ? NAME_None : *SelectedInstance; - TemplateAsset->SetInspectedInstance(NewSelectedInstanceName); + TemplateAsset->SetInspectedInstance(Instance); } } } @@ -305,28 +270,28 @@ void SFlowAssetBreadcrumb::Construct(const FArguments& InArgs, const TWeakObject // create breadcrumb SAssignNew(BreadcrumbTrail, SBreadcrumbTrail) - .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) - .OnCrumbClicked(this, &SFlowAssetBreadcrumb::OnCrumbClicked) - .ButtonStyle(FAppStyle::Get(), "SimpleButton") - .TextStyle(FAppStyle::Get(), "NormalText") - .ButtonContentPadding(FMargin(2.0f, 4.0f)) - .DelimiterImage(FAppStyle::GetBrush("Icons.ChevronRight")) - .ShowLeadingDelimiter(true) - .PersistentBreadcrumbs(true); + .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) + .OnCrumbClicked(this, &SFlowAssetBreadcrumb::OnCrumbClicked) + .ButtonStyle(FAppStyle::Get(), "SimpleButton") + .TextStyle(FAppStyle::Get(), "NormalText") + .ButtonContentPadding(FMargin(2.0f, 4.0f)) + .DelimiterImage(FAppStyle::GetBrush("Icons.ChevronRight")) + .ShowLeadingDelimiter(true) + .PersistentBreadcrumbs(true); ChildSlot - [ - SNew(SBorder) - .Visibility(this, &SFlowAssetBreadcrumb::GetBreadcrumbVisibility) - .BorderImage(new FSlateRoundedBoxBrush(FStyleColors::Transparent, 4, FStyleColors::InputOutline, 1)) [ - SNew(SBox) - .MaxDesiredWidth(500.f) - [ - BreadcrumbTrail.ToSharedRef() - ] - ] - ]; + SNew(SBorder) + .Visibility(this, &SFlowAssetBreadcrumb::GetBreadcrumbVisibility) + .BorderImage(new FSlateRoundedBoxBrush(FStyleColors::Transparent, 4, FStyleColors::InputOutline, 1)) + [ + SNew(SBox) + .MaxDesiredWidth(500.f) + [ + BreadcrumbTrail.ToSharedRef() + ] + ] + ]; TemplateAsset->OnDebuggerRefresh().AddSP(this, &SFlowAssetBreadcrumb::FillBreadcrumb); FillBreadcrumb(); @@ -342,7 +307,7 @@ void SFlowAssetBreadcrumb::FillBreadcrumb() const BreadcrumbTrail->ClearCrumbs(); if (const UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance()) { - TArray> InstancesFromRoot = {InspectedInstance}; + TArray> InstancesFromRoot = { InspectedInstance }; const UFlowAsset* CheckedInstance = InspectedInstance; while (UFlowAsset* ParentInstance = CheckedInstance->GetParentInstance()) @@ -411,23 +376,23 @@ void FFlowAssetToolbar::BuildAssetToolbar(UToolMenu* ToolbarMenu) const // Visual Diff: menu to choose asset revision compared with the current one Section.AddDynamicEntry("SourceControlCommands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) - { - const UFlowAssetEditorContext* Context = InSection.FindContext(); - if (Context && Context->FlowAssetEditor.IsValid()) { - InSection.InsertPosition = FToolMenuInsert(); - FToolMenuEntry DiffEntry = FToolMenuEntry::InitComboButton( - "Diff", - FUIAction(), - FOnGetContent::CreateStatic(&FFlowAssetToolbar::MakeDiffMenu, Context), - LOCTEXT("Diff", "Diff"), - LOCTEXT("FlowAssetEditorDiffToolTip", "Diff against previous revisions"), - FSlateIcon(FAppStyle::Get().GetStyleSetName(), "BlueprintDiff.ToolbarIcon") - ); - DiffEntry.StyleNameOverride = "CalloutToolbar"; - InSection.AddEntry(DiffEntry); - } - })); + const UFlowAssetEditorContext* Context = InSection.FindContext(); + if (Context && Context->FlowAssetEditor.IsValid()) + { + InSection.InsertPosition = FToolMenuInsert(); + FToolMenuEntry DiffEntry = FToolMenuEntry::InitComboButton( + "Diff", + FUIAction(), + FOnGetContent::CreateStatic(&FFlowAssetToolbar::MakeDiffMenu, Context), + LOCTEXT("Diff", "Diff"), + LOCTEXT("FlowAssetEditorDiffToolTip", "Diff against previous revisions"), + FSlateIcon(FAppStyle::Get().GetStyleSetName(), "BlueprintDiff.ToolbarIcon") + ); + DiffEntry.StyleNameOverride = "CalloutToolbar"; + InSection.AddEntry(DiffEntry); + } + })); Section.AddEntry(FToolMenuEntry::InitToolBarButton( FFlowToolbarCommands::Get().SearchInAsset, @@ -467,8 +432,8 @@ static void OnDiffRevisionPicked(FRevisionInfo const& RevisionInfo, const FStrin if (PreviousAsset) { const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); - const FRevisionInfo OldRevision = {Revision->GetRevision(), Revision->GetCheckInIdentifier(), Revision->GetDate()}; - const FRevisionInfo CurrentRevision = {TEXT(""), Revision->GetCheckInIdentifier(), Revision->GetDate()}; + const FRevisionInfo OldRevision = { Revision->GetRevision(), Revision->GetCheckInIdentifier(), Revision->GetDate() }; + const FRevisionInfo CurrentRevision = { TEXT(""), Revision->GetCheckInIdentifier(), Revision->GetDate() }; AssetToolsModule.Get().DiffAssets(PreviousAsset, CurrentAsset.Get(), OldRevision, CurrentRevision); } } @@ -518,16 +483,16 @@ void FFlowAssetToolbar::BuildDebuggerToolbar(UToolMenu* ToolbarMenu) const Section.InsertPosition = FToolMenuInsert("View", EToolMenuInsertType::After); Section.AddDynamicEntry("DebuggingCommands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) - { - const UFlowAssetEditorContext* Context = InSection.FindContext(); - if (Context && Context->GetFlowAsset()) { - FPlayWorldCommands::BuildToolbar(InSection); + const UFlowAssetEditorContext* Context = InSection.FindContext(); + if (Context && Context->GetFlowAsset()) + { + FPlayWorldCommands::BuildToolbar(InSection); - InSection.AddEntry(FToolMenuEntry::InitWidget("AssetInstances", SNew(SFlowAssetInstanceList, Context->GetFlowAsset()), FText(), true)); - InSection.AddEntry(FToolMenuEntry::InitWidget("AssetBreadcrumb", SNew(SFlowAssetBreadcrumb, Context->GetFlowAsset()), FText(), true)); - } - })); + InSection.AddEntry(FToolMenuEntry::InitWidget("AssetInstances", SNew(SFlowAssetInstanceList, Context->GetFlowAsset()), FText(), true)); + InSection.AddEntry(FToolMenuEntry::InitWidget("AssetBreadcrumb", SNew(SFlowAssetBreadcrumb, Context->GetFlowAsset()), FText(), true)); + } + })); } -#undef LOCTEXT_NAMESPACE +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp index 80f1d7e15..dc7d9858e 100644 --- a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp +++ b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp @@ -124,29 +124,6 @@ void UFlowDebugEditorSubsystem::PauseSession(const UFlowAsset& FlowAssetInstance const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); if (!bWasPaused) { - return; - } - - if (GUnrealEd->SetPIEWorldsPaused(true)) - { - bPausedAtFlowBreakpoint = true; - - const UFlowAsset* HitInstance = Node->GetFlowAsset(); - if (ensure(HitInstance)) - { - UFlowAsset* AssetTemplate = HitInstance->GetTemplateAsset(); - AssetTemplate->SetInspectedInstance(HitInstance); - - UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); - if (AssetEditorSubsystem->OpenEditorForAsset(AssetTemplate)) - { - if (const TSharedPtr FlowAssetEditor = FFlowGraphUtils::GetFlowAssetEditor(AssetTemplate)) - { - FlowAssetEditor->JumpToNode(Node->GetGraphNode()); - } - } - } - GUnrealEd->PlaySessionPaused(); } } @@ -182,7 +159,7 @@ void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowAsset& FlowAssetInsta return; } - TemplateAsset->SetInspectedInstance(FlowAssetInstance.GetDisplayName()); + TemplateAsset->SetInspectedInstance(&FlowAssetInstance); UFlowGraph* FlowGraph = Cast(TemplateAsset->GetGraph()); if (!IsValid(FlowGraph)) diff --git a/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp b/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp index 5217b4990..0e9a97c82 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp @@ -19,6 +19,9 @@ #include "LevelEditor.h" #include "Modules/ModuleManager.h" #include "ScopedTransaction.h" +#include "ToolMenu.h" +#include "ToolMenuDelegates.h" +#include "ToolMenus.h" #include "UnrealEdGlobals.h" #include "Widgets/Docking/SDockTab.h" #include "Algo/AnyOf.h" diff --git a/Source/FlowEditor/Public/FlowEditorModule.h b/Source/FlowEditor/Public/FlowEditorModule.h index 16370e74c..c25a3e190 100644 --- a/Source/FlowEditor/Public/FlowEditorModule.h +++ b/Source/FlowEditor/Public/FlowEditorModule.h @@ -30,7 +30,6 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface, public IHasMen TArray> RegisteredAssetActions; TSet CustomClassLayouts; TSet CustomStructLayouts; - bool bIsRegisteredForAssetChanges = false; TSharedPtr MenuExtensibilityManager; TSharedPtr ToolBarExtensibilityManager; From ad4063a6eaf246282e3e2a9a84bdf26cd1a67f56 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:32:03 -0800 Subject: [PATCH 07/10] compile fixes --- Source/Flow/Private/Nodes/FlowNode.cpp | 154 +++++++-------- Source/Flow/Private/Nodes/FlowNodeBase.cpp | 2 + .../Debugger/FlowDebuggerSubsystem.cpp | 177 +++++++----------- .../Asset/FlowDebugEditorSubsystem.cpp | 1 - Source/FlowEditor/Public/FlowEditorModule.h | 2 + 5 files changed, 145 insertions(+), 191 deletions(-) diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index 753f36e93..e9dbc076c 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -415,36 +415,54 @@ void UFlowNode::SetAutoOutputDataPins(const TArray& AutoOutputPins) AutoOutputDataPins = AutoOutputPins; } +void UFlowNode::SetConnections(const TMap& InConnections) +{ + const TMap OldConnections = Connections; + Connections = InConnections; + OnConnectionsChanged(OldConnections); +} + #endif // WITH_EDITOR -FFlowDataPinResult UFlowNode::TrySupplyDataPin_Implementation(FName PinName) const +// #FlowDataPinLegacy +void UFlowNode::FixupDataPinTypes() { - const FFlowPin* FlowPin = FindOutputPinByName(PinName); - if (!FlowPin) + FixupDataPinTypesForArray(InputPins); + FixupDataPinTypesForArray(OutputPins); +#if WITH_EDITOR + FixupDataPinTypesForArray(AutoInputDataPins); + FixupDataPinTypesForArray(AutoOutputDataPins); +#endif +} + +void UFlowNode::FixupDataPinTypesForArray(TArray& MutableDataPinArray) +{ + for (FFlowPin& MutableFlowPin : MutableDataPinArray) { - // Also look in the Input Pins (for supplying default values for unconnected pins) - FlowPin = FindInputPinByName(PinName); - if (!FlowPin) - { - return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); - } + FixupDataPinTypesForPin(MutableFlowPin); } +} - const FFlowPinType* DataPinType = FlowPin->ResolveFlowPinType(); - if (!DataPinType) +void UFlowNode::FixupDataPinTypesForPin(FFlowPin& MutableDataPin) +{ + const FFlowPinTypeName NewPinTypeName = FFlowPin::GetPinTypeNameForLegacyPinType(MutableDataPin.PinType); + + if (!NewPinTypeName.IsNone()) { - return FFlowDataPinResult(EFlowDataPinResolveResult::FailedMismatchedType); + MutableDataPin.SetPinTypeName(NewPinTypeName); } - FFlowDataPinResult SuppliedResult; - if (TryGatherPropertyOwnersAndPopulateResult(PinName, *DataPinType, *FlowPin, SuppliedResult)) + if (MutableDataPin.GetPinTypeName().IsNone()) { - return SuppliedResult; + // Ensure we have a pin type even if the enum was invalid before + MutableDataPin.SetPinTypeName(FFlowPinType_Exec::GetPinTypeNameStatic()); } - return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); + MutableDataPin.PinType = EFlowPinType::Invalid; } +// -- + bool UFlowNode::TryFindPropertyByPinName( const UObject& PropertyOwnerObject, const FName& PinName, @@ -488,6 +506,34 @@ void UFlowNode::GatherPotentialPropertyOwnersForDataPins(TArray& InOutOwners.AddUnique(this); } +FFlowDataPinResult UFlowNode::TrySupplyDataPin_Implementation(FName PinName) const +{ + const FFlowPin* FlowPin = FindOutputPinByName(PinName); + if (!FlowPin) + { + // Also look in the Input Pins (for supplying default values for unconnected pins) + FlowPin = FindInputPinByName(PinName); + if (!FlowPin) + { + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); + } + } + + const FFlowPinType* DataPinType = FlowPin->ResolveFlowPinType(); + if (!DataPinType) + { + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedMismatchedType); + } + + FFlowDataPinResult SuppliedResult; + if (TryGatherPropertyOwnersAndPopulateResult(PinName, *DataPinType, *FlowPin, SuppliedResult)) + { + return SuppliedResult; + } + + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); +} + bool UFlowNode::TryGatherPropertyOwnersAndPopulateResult( const FName& PinName, const FFlowPinType& DataPinType, @@ -513,7 +559,6 @@ bool UFlowNode::TryGatherPropertyOwnersAndPopulateResult( return false; } -// -- bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, TFlowPinValueSupplierDataArray& InOutPinValueSupplierDatas) const { @@ -574,66 +619,6 @@ bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, T return !InOutPinValueSupplierDatas.IsEmpty(); } -void UFlowNode::AutoGenerateDataPins(FFlowAutoDataPinsWorkingData& InOutWorkingData) const -{ - // Gather all of the potential providers for this DataPin - TArray PropertyOwnerObjects; - GatherPotentialPropertyOwnersForDataPins(PropertyOwnerObjects); - - // GenerateDataPins for all of the potential providers - for (const UObject* PropertyOwnerObject : PropertyOwnerObjects) - { - checkf(IsValid(PropertyOwnerObject), TEXT("Every UObject provided by GatherPotentialPropertyOwnersForDataPins must be valid")); - - InOutWorkingData.AddFlowDataPinsForClassProperties(*PropertyOwnerObject); - } -} - -// #FlowDataPinLegacy -void UFlowNode::FixupDataPinTypes() -{ - FixupDataPinTypesForArray(InputPins); - FixupDataPinTypesForArray(OutputPins); -#if WITH_EDITOR - FixupDataPinTypesForArray(AutoInputDataPins); - FixupDataPinTypesForArray(AutoOutputDataPins); -#endif -} - -void UFlowNode::FixupDataPinTypesForArray(TArray& MutableDataPinArray) -{ - for (FFlowPin& MutableFlowPin : MutableDataPinArray) - { - FixupDataPinTypesForPin(MutableFlowPin); - } -} - -void UFlowNode::FixupDataPinTypesForPin(FFlowPin& MutableDataPin) -{ - const FFlowPinTypeName NewPinTypeName = FFlowPin::GetPinTypeNameForLegacyPinType(MutableDataPin.PinType); - - if (!NewPinTypeName.IsNone()) - { - MutableDataPin.SetPinTypeName(NewPinTypeName); - } - - if (MutableDataPin.GetPinTypeName().IsNone()) - { - // Ensure we have a pin type even if the enum was invalid before - MutableDataPin.SetPinTypeName(FFlowPinType_Exec::GetPinTypeNameStatic()); - } - - MutableDataPin.PinType = EFlowPinType::Invalid; -} -// -- - -void UFlowNode::SetConnections(const TMap& InConnections) -{ - const TMap OldConnections = Connections; - Connections = InConnections; - OnConnectionsChanged(OldConnections); -} - TSet UFlowNode::GatherConnectedNodes() const { TSet Result; @@ -1102,6 +1087,21 @@ TArray UFlowNode::GetPinRecords(const FName& PinName, const EEdGraph } } +void UFlowNode::AutoGenerateDataPins(FFlowAutoDataPinsWorkingData& InOutWorkingData) const +{ + // Gather all of the potential providers for this DataPin + TArray PropertyOwnerObjects; + GatherPotentialPropertyOwnersForDataPins(PropertyOwnerObjects); + + // GenerateDataPins for all of the potential providers + for (const UObject* PropertyOwnerObject : PropertyOwnerObjects) + { + checkf(IsValid(PropertyOwnerObject), TEXT("Every UObject provided by GatherPotentialPropertyOwnersForDataPins must be valid")); + + InOutWorkingData.AddFlowDataPinsForClassProperties(*PropertyOwnerObject); + } +} + #endif FString UFlowNode::GetIdentityTagDescription(const FGameplayTag& Tag) diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp index ec0b0e2eb..35e406f8a 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBase.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -958,6 +958,7 @@ bool UFlowNodeBase::BuildMessage(FString& Message) const } #endif +#if WITH_EDITOR EDataValidationResult UFlowNodeBase::ValidateNode() { EDataValidationResult ValidationResult = EDataValidationResult::NotValidated; @@ -969,6 +970,7 @@ EDataValidationResult UFlowNodeBase::ValidateNode() return ValidationResult; } +#endif bool UFlowNodeBase::TryAddValueToFormatNamedArguments(const FFlowNamedDataPinProperty& NamedDataPinProperty, FFormatNamedArguments& InOutArguments) const { diff --git a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp index b18ceeae3..e014fe8e1 100644 --- a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp @@ -325,21 +325,44 @@ bool UFlowDebuggerSubsystem::IsBreakpointEnabled(const FGuid& NodeGuid, const FN return false; } -bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr FlowAsset) +bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr& FlowAsset) { - UFlowDebuggerSettings* Settings = GetMutableDefault(); - for (const TPair& Node : FlowAsset->GetNodes()) + return HasAnyBreakpointsMatching(FlowAsset, true); +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpointsDisabled(const TWeakObjectPtr& FlowAsset) +{ + return HasAnyBreakpointsMatching(FlowAsset, false); +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpointsMatching(const TWeakObjectPtr& FlowAsset, bool bDesiresEnabled) +{ + if (!FlowAsset.IsValid()) { - if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(Node.Key)) + return false; + } + + const UFlowDebuggerSettings* Settings = GetDefault(); + if (!Settings) + { + return false; + } + + for (const TPair& NodePair : FlowAsset->GetNodes()) + { + if (const FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(NodePair.Key)) { - if (NodeBreakpoint->Breakpoint.IsActive() && NodeBreakpoint->Breakpoint.IsEnabled()) + // Node-level breakpoint must be active to count (matches original behavior) + if (NodeBreakpoint->Breakpoint.IsActive() && + (NodeBreakpoint->Breakpoint.IsEnabled() == bDesiresEnabled)) { return true; } - for (auto& [Name, PinBreakpoint] : NodeBreakpoint->PinBreakpoints) + // Pin-level breakpoints + for (const auto& PinPair : NodeBreakpoint->PinBreakpoints) { - if (PinBreakpoint.IsEnabled()) + if (PinPair.Value.IsEnabled() == bDesiresEnabled) { return true; } @@ -350,29 +373,45 @@ bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr FlowAsset) +void UFlowDebuggerSubsystem::RequestHaltFlowExecution(const UFlowNode* Node) { - UFlowDebuggerSettings* Settings = GetMutableDefault(); - for (const TPair& Node : FlowAsset->GetNodes()) + bHaltFlowExecution = true; + HaltedOnFlowAssetInstance = Node->GetFlowAsset(); + HaltedOnNodeGuid = Node->NodeGuid; +} + +void UFlowDebuggerSubsystem::ClearHaltFlowExecution() +{ + bHaltFlowExecution = false; + HaltedOnFlowAssetInstance.Reset(); + HaltedOnNodeGuid.Invalidate(); +} + +void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() +{ + if (!LastHitNodeGuid.IsValid()) { - if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(Node.Key)) - { - if (NodeBreakpoint->Breakpoint.IsActive() && !NodeBreakpoint->Breakpoint.IsEnabled()) - { - return true; - } + return; + } - for (auto& [Name, PinBreakpoint] : NodeBreakpoint->PinBreakpoints) - { - if (!PinBreakpoint.IsEnabled()) - { - return true; - } - } + // Pin breakpoint "hit" state lives in the PinBreakpoints map, node breakpoint "hit" lives on NodeBreakpoint.Breakpoint. + if (!LastHitPinName.IsNone()) + { + if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(LastHitNodeGuid, LastHitPinName)) + { + PinBreakpoint->MarkAsHit(false); + } + } + else + { + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(LastHitNodeGuid)) + { + NodeBreakpoint->MarkAsHit(false); } } - return false; + LastHitNodeGuid.Invalidate(); + LastHitPinName = NAME_None; } void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode) @@ -413,6 +452,7 @@ void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode, const FName& P LastHitPinName = PinName; RequestHaltFlowExecution(FlowNode); + OnDebuggerBreakpointHit.Broadcast(FlowNode); PauseSession(*FlowNode); @@ -420,95 +460,6 @@ void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode, const FName& P } } -void UFlowDebuggerSubsystem::RequestHaltFlowExecution(const UFlowNode* Node) -{ - bHaltFlowExecution = true; - HaltedOnFlowAssetInstance = Node->GetFlowAsset(); - HaltedOnNodeGuid = Node->NodeGuid; -} - -void UFlowDebuggerSubsystem::ClearHaltFlowExecution() -{ - bHaltFlowExecution = false; - HaltedOnFlowAssetInstance.Reset(); - HaltedOnNodeGuid.Invalidate(); -} - -void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() -{ - if (!LastHitNodeGuid.IsValid()) - { - return; - } - - // Pin breakpoint "hit" state lives in the PinBreakpoints map, node breakpoint "hit" lives on NodeBreakpoint.Breakpoint. - if (!LastHitPinName.IsNone()) - { - if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(LastHitNodeGuid, LastHitPinName)) - { - PinBreakpoint->MarkAsHit(false); - } - } - else - { - if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(LastHitNodeGuid)) - { - NodeBreakpoint->MarkAsHit(false); - } - } -} - -bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr& FlowAsset) -{ - return HasAnyBreakpointsMatching(FlowAsset, true); -} - -bool UFlowDebuggerSubsystem::HasAnyBreakpointsDisabled(const TWeakObjectPtr& FlowAsset) -{ - return HasAnyBreakpointsMatching(FlowAsset, false); -} - -bool UFlowDebuggerSubsystem::HasAnyBreakpointsMatching(const TWeakObjectPtr& FlowAsset, bool bDesiresEnabled) -{ - if (!FlowAsset.IsValid()) - { - return false; - } - - const UFlowDebuggerSettings* Settings = GetDefault(); - if (!Settings) - { - return false; - } - - for (const TPair& NodePair : FlowAsset->GetNodes()) - { - if (const FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(NodePair.Key)) - { - // Node-level breakpoint must be active to count (matches original behavior) - if (NodeBreakpoint->Breakpoint.IsActive() && - (NodeBreakpoint->Breakpoint.IsEnabled() == bDesiresEnabled)) - { - return true; - } - - // Pin-level breakpoints - for (const auto& PinPair : NodeBreakpoint->PinBreakpoints) - { - if (PinPair.Value.IsEnabled() == bDesiresEnabled) - { - return true; - } - } - } - } - - return false; - - LastHitNodeGuid.Invalidate(); - LastHitPinName = NAME_None; -} - void UFlowDebuggerSubsystem::PauseSession(const UFlowNode& FlowNode) { SetPause(FlowNode, true); diff --git a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp index 675f86805..90cac2020 100644 --- a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp +++ b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp @@ -159,7 +159,6 @@ void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowNode* FlowNode) const return; } - TemplateAsset->SetInspectedInstance(&FlowAssetInstance); TemplateAsset->SetInspectedInstance(FlowNode->GetFlowAsset()); UFlowGraph* FlowGraph = Cast(TemplateAsset->GetGraph()); diff --git a/Source/FlowEditor/Public/FlowEditorModule.h b/Source/FlowEditor/Public/FlowEditorModule.h index 325b8493d..b75187d91 100644 --- a/Source/FlowEditor/Public/FlowEditorModule.h +++ b/Source/FlowEditor/Public/FlowEditorModule.h @@ -31,6 +31,8 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface, public IHasMen TSet CustomClassLayouts; TSet CustomStructLayouts; + bool bIsRegisteredForAssetChanges = false; + public: virtual void StartupModule() override; virtual void ShutdownModule() override; From 7d4b31f3363003effc6708fc8a4c8124d7d9bfd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Justy=C5=84ski?= Date: Wed, 28 Jan 2026 17:20:44 +0100 Subject: [PATCH 08/10] reverted formatting changes --- .../Private/Asset/FlowAssetToolbar.cpp | 128 +++++++++--------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp index 0fd6ce003..633a52816 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp @@ -53,7 +53,7 @@ void SFlowAssetInstanceList::Construct(const FArguments& InArgs, const TWeakObje .ContentPadding(FMargin(0.f, 2.f)) [ SNew(STextBlock) - .Text(this, &SFlowAssetInstanceList::GetSelectedContextName) + .Text(this, &SFlowAssetInstanceList::GetSelectedContextName) ]; InstanceComboBox = SNew(SComboBox>) @@ -63,26 +63,26 @@ void SFlowAssetInstanceList::Construct(const FArguments& InArgs, const TWeakObje .ContentPadding(FMargin(0.f, 2.f)) [ SNew(STextBlock) - .Text(this, &SFlowAssetInstanceList::GetSelectedInstanceName) + .Text(this, &SFlowAssetInstanceList::GetSelectedInstanceName) ]; ChildSlot + [ + SNew(SHorizontalBox) + .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.0f, 0.0f, 8.0f, 0.0f) [ - SNew(SHorizontalBox) - .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) - + SHorizontalBox::Slot() - .AutoWidth() - .Padding(0.0f, 0.0f, 8.0f, 0.0f) - [ - ContextComboBox.ToSharedRef() - ] - + SHorizontalBox::Slot() - .AutoWidth() - .Padding(0.0f, 0.0f, 4.0f, 0.0f) - [ - InstanceComboBox.ToSharedRef() - ] - ]; + ContextComboBox.ToSharedRef() + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + InstanceComboBox.ToSharedRef() + ] + ]; } SFlowAssetInstanceList::~SFlowAssetInstanceList() @@ -270,28 +270,28 @@ void SFlowAssetBreadcrumb::Construct(const FArguments& InArgs, const TWeakObject // create breadcrumb SAssignNew(BreadcrumbTrail, SBreadcrumbTrail) - .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) - .OnCrumbClicked(this, &SFlowAssetBreadcrumb::OnCrumbClicked) - .ButtonStyle(FAppStyle::Get(), "SimpleButton") - .TextStyle(FAppStyle::Get(), "NormalText") - .ButtonContentPadding(FMargin(2.0f, 4.0f)) - .DelimiterImage(FAppStyle::GetBrush("Icons.ChevronRight")) - .ShowLeadingDelimiter(true) - .PersistentBreadcrumbs(true); + .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) + .OnCrumbClicked(this, &SFlowAssetBreadcrumb::OnCrumbClicked) + .ButtonStyle(FAppStyle::Get(), "SimpleButton") + .TextStyle(FAppStyle::Get(), "NormalText") + .ButtonContentPadding(FMargin(2.0f, 4.0f)) + .DelimiterImage(FAppStyle::GetBrush("Icons.ChevronRight")) + .ShowLeadingDelimiter(true) + .PersistentBreadcrumbs(true); ChildSlot + [ + SNew(SBorder) + .Visibility(this, &SFlowAssetBreadcrumb::GetBreadcrumbVisibility) + .BorderImage(new FSlateRoundedBoxBrush(FStyleColors::Transparent, 4, FStyleColors::InputOutline, 1)) [ - SNew(SBorder) - .Visibility(this, &SFlowAssetBreadcrumb::GetBreadcrumbVisibility) - .BorderImage(new FSlateRoundedBoxBrush(FStyleColors::Transparent, 4, FStyleColors::InputOutline, 1)) - [ - SNew(SBox) - .MaxDesiredWidth(500.f) - [ - BreadcrumbTrail.ToSharedRef() - ] - ] - ]; + SNew(SBox) + .MaxDesiredWidth(500.f) + [ + BreadcrumbTrail.ToSharedRef() + ] + ] + ]; TemplateAsset->OnDebuggerRefresh().AddSP(this, &SFlowAssetBreadcrumb::FillBreadcrumb); FillBreadcrumb(); @@ -307,7 +307,7 @@ void SFlowAssetBreadcrumb::FillBreadcrumb() const BreadcrumbTrail->ClearCrumbs(); if (const UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance()) { - TArray> InstancesFromRoot = { InspectedInstance }; + TArray> InstancesFromRoot = {InspectedInstance}; const UFlowAsset* CheckedInstance = InspectedInstance; while (UFlowAsset* ParentInstance = CheckedInstance->GetParentInstance()) @@ -376,23 +376,23 @@ void FFlowAssetToolbar::BuildAssetToolbar(UToolMenu* ToolbarMenu) const // Visual Diff: menu to choose asset revision compared with the current one Section.AddDynamicEntry("SourceControlCommands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) + { + const UFlowAssetEditorContext* Context = InSection.FindContext(); + if (Context && Context->FlowAssetEditor.IsValid()) { - const UFlowAssetEditorContext* Context = InSection.FindContext(); - if (Context && Context->FlowAssetEditor.IsValid()) - { - InSection.InsertPosition = FToolMenuInsert(); - FToolMenuEntry DiffEntry = FToolMenuEntry::InitComboButton( - "Diff", - FUIAction(), - FOnGetContent::CreateStatic(&FFlowAssetToolbar::MakeDiffMenu, Context), - LOCTEXT("Diff", "Diff"), - LOCTEXT("FlowAssetEditorDiffToolTip", "Diff against previous revisions"), - FSlateIcon(FAppStyle::Get().GetStyleSetName(), "BlueprintDiff.ToolbarIcon") - ); - DiffEntry.StyleNameOverride = "CalloutToolbar"; - InSection.AddEntry(DiffEntry); - } - })); + InSection.InsertPosition = FToolMenuInsert(); + FToolMenuEntry DiffEntry = FToolMenuEntry::InitComboButton( + "Diff", + FUIAction(), + FOnGetContent::CreateStatic(&FFlowAssetToolbar::MakeDiffMenu, Context), + LOCTEXT("Diff", "Diff"), + LOCTEXT("FlowAssetEditorDiffToolTip", "Diff against previous revisions"), + FSlateIcon(FAppStyle::Get().GetStyleSetName(), "BlueprintDiff.ToolbarIcon") + ); + DiffEntry.StyleNameOverride = "CalloutToolbar"; + InSection.AddEntry(DiffEntry); + } + })); Section.AddEntry(FToolMenuEntry::InitToolBarButton( FFlowToolbarCommands::Get().SearchInAsset, @@ -432,8 +432,8 @@ static void OnDiffRevisionPicked(FRevisionInfo const& RevisionInfo, const FStrin if (PreviousAsset) { const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); - const FRevisionInfo OldRevision = { Revision->GetRevision(), Revision->GetCheckInIdentifier(), Revision->GetDate() }; - const FRevisionInfo CurrentRevision = { TEXT(""), Revision->GetCheckInIdentifier(), Revision->GetDate() }; + const FRevisionInfo OldRevision = {Revision->GetRevision(), Revision->GetCheckInIdentifier(), Revision->GetDate()}; + const FRevisionInfo CurrentRevision = {TEXT(""), Revision->GetCheckInIdentifier(), Revision->GetDate()}; AssetToolsModule.Get().DiffAssets(PreviousAsset, CurrentAsset.Get(), OldRevision, CurrentRevision); } } @@ -483,16 +483,16 @@ void FFlowAssetToolbar::BuildDebuggerToolbar(UToolMenu* ToolbarMenu) const Section.InsertPosition = FToolMenuInsert("View", EToolMenuInsertType::After); Section.AddDynamicEntry("DebuggingCommands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) + { + const UFlowAssetEditorContext* Context = InSection.FindContext(); + if (Context && Context->GetFlowAsset()) { - const UFlowAssetEditorContext* Context = InSection.FindContext(); - if (Context && Context->GetFlowAsset()) - { - FPlayWorldCommands::BuildToolbar(InSection); + FPlayWorldCommands::BuildToolbar(InSection); - InSection.AddEntry(FToolMenuEntry::InitWidget("AssetInstances", SNew(SFlowAssetInstanceList, Context->GetFlowAsset()), FText(), true)); - InSection.AddEntry(FToolMenuEntry::InitWidget("AssetBreadcrumb", SNew(SFlowAssetBreadcrumb, Context->GetFlowAsset()), FText(), true)); - } - })); + InSection.AddEntry(FToolMenuEntry::InitWidget("AssetInstances", SNew(SFlowAssetInstanceList, Context->GetFlowAsset()), FText(), true)); + InSection.AddEntry(FToolMenuEntry::InitWidget("AssetBreadcrumb", SNew(SFlowAssetBreadcrumb, Context->GetFlowAsset()), FText(), true)); + } + })); } -#undef LOCTEXT_NAMESPACE \ No newline at end of file +#undef LOCTEXT_NAMESPACE From 1c00344c9ac551da5f90dfed68f1ab34f2802f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Justy=C5=84ski?= Date: Wed, 28 Jan 2026 18:06:48 +0100 Subject: [PATCH 09/10] restored previous class layout --- Source/Flow/Private/Nodes/FlowNode.cpp | 165 +++++++++++++------------ 1 file changed, 87 insertions(+), 78 deletions(-) diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index e9dbc076c..aa319b35a 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -415,54 +415,36 @@ void UFlowNode::SetAutoOutputDataPins(const TArray& AutoOutputPins) AutoOutputDataPins = AutoOutputPins; } -void UFlowNode::SetConnections(const TMap& InConnections) -{ - const TMap OldConnections = Connections; - Connections = InConnections; - OnConnectionsChanged(OldConnections); -} - #endif // WITH_EDITOR -// #FlowDataPinLegacy -void UFlowNode::FixupDataPinTypes() -{ - FixupDataPinTypesForArray(InputPins); - FixupDataPinTypesForArray(OutputPins); -#if WITH_EDITOR - FixupDataPinTypesForArray(AutoInputDataPins); - FixupDataPinTypesForArray(AutoOutputDataPins); -#endif -} - -void UFlowNode::FixupDataPinTypesForArray(TArray& MutableDataPinArray) +FFlowDataPinResult UFlowNode::TrySupplyDataPin_Implementation(FName PinName) const { - for (FFlowPin& MutableFlowPin : MutableDataPinArray) + const FFlowPin* FlowPin = FindOutputPinByName(PinName); + if (!FlowPin) { - FixupDataPinTypesForPin(MutableFlowPin); + // Also look in the Input Pins (for supplying default values for unconnected pins) + FlowPin = FindInputPinByName(PinName); + if (!FlowPin) + { + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); + } } -} -void UFlowNode::FixupDataPinTypesForPin(FFlowPin& MutableDataPin) -{ - const FFlowPinTypeName NewPinTypeName = FFlowPin::GetPinTypeNameForLegacyPinType(MutableDataPin.PinType); - - if (!NewPinTypeName.IsNone()) + const FFlowPinType* DataPinType = FlowPin->ResolveFlowPinType(); + if (!DataPinType) { - MutableDataPin.SetPinTypeName(NewPinTypeName); + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedMismatchedType); } - if (MutableDataPin.GetPinTypeName().IsNone()) + FFlowDataPinResult SuppliedResult; + if (TryGatherPropertyOwnersAndPopulateResult(PinName, *DataPinType, *FlowPin, SuppliedResult)) { - // Ensure we have a pin type even if the enum was invalid before - MutableDataPin.SetPinTypeName(FFlowPinType_Exec::GetPinTypeNameStatic()); + return SuppliedResult; } - MutableDataPin.PinType = EFlowPinType::Invalid; + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); } -// -- - bool UFlowNode::TryFindPropertyByPinName( const UObject& PropertyOwnerObject, const FName& PinName, @@ -506,34 +488,6 @@ void UFlowNode::GatherPotentialPropertyOwnersForDataPins(TArray& InOutOwners.AddUnique(this); } -FFlowDataPinResult UFlowNode::TrySupplyDataPin_Implementation(FName PinName) const -{ - const FFlowPin* FlowPin = FindOutputPinByName(PinName); - if (!FlowPin) - { - // Also look in the Input Pins (for supplying default values for unconnected pins) - FlowPin = FindInputPinByName(PinName); - if (!FlowPin) - { - return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); - } - } - - const FFlowPinType* DataPinType = FlowPin->ResolveFlowPinType(); - if (!DataPinType) - { - return FFlowDataPinResult(EFlowDataPinResolveResult::FailedMismatchedType); - } - - FFlowDataPinResult SuppliedResult; - if (TryGatherPropertyOwnersAndPopulateResult(PinName, *DataPinType, *FlowPin, SuppliedResult)) - { - return SuppliedResult; - } - - return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); -} - bool UFlowNode::TryGatherPropertyOwnersAndPopulateResult( const FName& PinName, const FFlowPinType& DataPinType, @@ -559,6 +513,7 @@ bool UFlowNode::TryGatherPropertyOwnersAndPopulateResult( return false; } +// -- bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, TFlowPinValueSupplierDataArray& InOutPinValueSupplierDatas) const { @@ -619,6 +574,70 @@ bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, T return !InOutPinValueSupplierDatas.IsEmpty(); } +#if WITH_EDITOR +void UFlowNode::AutoGenerateDataPins(FFlowAutoDataPinsWorkingData& InOutWorkingData) const +{ + // Gather all of the potential providers for this DataPin + TArray PropertyOwnerObjects; + GatherPotentialPropertyOwnersForDataPins(PropertyOwnerObjects); + + // GenerateDataPins for all of the potential providers + for (const UObject* PropertyOwnerObject : PropertyOwnerObjects) + { + checkf(IsValid(PropertyOwnerObject), TEXT("Every UObject provided by GatherPotentialPropertyOwnersForDataPins must be valid")); + + InOutWorkingData.AddFlowDataPinsForClassProperties(*PropertyOwnerObject); + } +} +#endif + +// #FlowDataPinLegacy +void UFlowNode::FixupDataPinTypes() +{ + FixupDataPinTypesForArray(InputPins); + FixupDataPinTypesForArray(OutputPins); +#if WITH_EDITOR + FixupDataPinTypesForArray(AutoInputDataPins); + FixupDataPinTypesForArray(AutoOutputDataPins); +#endif +} + +void UFlowNode::FixupDataPinTypesForArray(TArray& MutableDataPinArray) +{ + for (FFlowPin& MutableFlowPin : MutableDataPinArray) + { + FixupDataPinTypesForPin(MutableFlowPin); + } +} + +void UFlowNode::FixupDataPinTypesForPin(FFlowPin& MutableDataPin) +{ + const FFlowPinTypeName NewPinTypeName = FFlowPin::GetPinTypeNameForLegacyPinType(MutableDataPin.PinType); + + if (!NewPinTypeName.IsNone()) + { + MutableDataPin.SetPinTypeName(NewPinTypeName); + } + + if (MutableDataPin.GetPinTypeName().IsNone()) + { + // Ensure we have a pin type even if the enum was invalid before + MutableDataPin.SetPinTypeName(FFlowPinType_Exec::GetPinTypeNameStatic()); + } + + MutableDataPin.PinType = EFlowPinType::Invalid; +} +// -- + +#if WITH_EDITOR +void UFlowNode::SetConnections(const TMap& InConnections) +{ + const TMap OldConnections = Connections; + Connections = InConnections; + OnConnectionsChanged(OldConnections); +} +#endif + TSet UFlowNode::GatherConnectedNodes() const { TSet Result; @@ -928,7 +947,7 @@ void UFlowNode::TriggerOutput(const FName PinName, const bool bFinish /*= false* if (HasFinished()) { // do not trigger output if node is already finished or aborted - LogError(TEXT("Trying to TriggerOutput after finished or aborted")); + LogError(TEXT("Trying to TriggerOutput after finished or aborted"), EFlowOnScreenMessageType::Disabled); return; } @@ -1063,6 +1082,11 @@ void UFlowNode::OnPassThrough_Implementation() Finish(); } +bool UFlowNode::ShouldSave_Implementation() +{ + return GetActivationState() == EFlowNodeState::Active; +} + #if WITH_EDITOR TMap UFlowNode::GetWireRecords() const { @@ -1087,21 +1111,6 @@ TArray UFlowNode::GetPinRecords(const FName& PinName, const EEdGraph } } -void UFlowNode::AutoGenerateDataPins(FFlowAutoDataPinsWorkingData& InOutWorkingData) const -{ - // Gather all of the potential providers for this DataPin - TArray PropertyOwnerObjects; - GatherPotentialPropertyOwnersForDataPins(PropertyOwnerObjects); - - // GenerateDataPins for all of the potential providers - for (const UObject* PropertyOwnerObject : PropertyOwnerObjects) - { - checkf(IsValid(PropertyOwnerObject), TEXT("Every UObject provided by GatherPotentialPropertyOwnersForDataPins must be valid")); - - InOutWorkingData.AddFlowDataPinsForClassProperties(*PropertyOwnerObject); - } -} - #endif FString UFlowNode::GetIdentityTagDescription(const FGameplayTag& Tag) From 7fe61982be9f161c6fda83189d0c2c13a124955d Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:59:11 -0800 Subject: [PATCH 10/10] [Flow] Trigger Outputs deferred while processing an Input Trigger This is a change to the core flow triggering logic to fix a category of sequencing bugs from the previous behavior. It would immediately fully process a triggered input and so on down the chain of flow nodes, without allowing the current flow node to finish executing, this caused a whole category of problems where the node wasn't able to finish its execution before being interrupted by a retirgger (from downstream) and AddOns wouldn't execute at the same time as their owning flow node reliably Now FlowAsset will queue any triggers generated while processing a trigger, and flush them when ending the processing of that trigger. We also integrated the debugger queued trigger caching mechanism to use the same syste. Subclasses of UFlowAsset that do their own deferred asset triggering can disable this feature, except for the debugger portion, which is still processed using the UFlowAsset queue. CR - JDurica, BJarvinen --- Source/Flow/Private/AddOns/FlowNodeAddOn.cpp | 15 -- .../Asset/FlowDeferredTransitionScope.cpp | 32 +++ Source/Flow/Private/FlowAsset.cpp | 178 +++++++++++++++-- Source/Flow/Private/FlowSettings.cpp | 1 + Source/Flow/Private/FlowSubsystem.cpp | 58 ++++++ .../Private/Interfaces/FlowExecutionGate.cpp | 111 ----------- Source/Flow/Public/AddOns/FlowNodeAddOn.h | 4 - .../Asset/FlowDeferredTransitionScope.h | 36 ++++ Source/Flow/Public/FlowAsset.h | 43 +++- Source/Flow/Public/FlowSettings.h | 6 + Source/Flow/Public/FlowSubsystem.h | 9 + .../Public/Interfaces/FlowExecutionGate.h | 9 - .../Public/Nodes/Developer/FlowNode_Log.h | 3 + .../Debugger/FlowDebuggerSubsystem.cpp | 183 +++++++++--------- .../Public/Debugger/FlowDebuggerSubsystem.h | 53 ++++- .../Asset/FlowDebugEditorSubsystem.cpp | 96 +++++++-- .../Public/Asset/FlowDebugEditorSubsystem.h | 8 +- 17 files changed, 567 insertions(+), 278 deletions(-) create mode 100644 Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp create mode 100644 Source/Flow/Public/Asset/FlowDeferredTransitionScope.h diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp index 87537f30b..3a809ddff 100644 --- a/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp @@ -17,21 +17,6 @@ UFlowNodeAddOn::UFlowNodeAddOn() #endif } -#if WITH_EDITOR -UEdGraphNode* UFlowNodeAddOn::GetGraphNode() const -{ - // todo: we might want to cache editor-time pointer to owning Flow Node - // it may require more work than just caching it, as pointer needs - // to be updated during editor operations - if (const UFlowNode* OwningFlowNode = FindOwningFlowNode()) - { - return OwningFlowNode->GetGraphNode(); - } - - return nullptr; -} -#endif - void UFlowNodeAddOn::InitializeInstance() { CacheFlowNode(); diff --git a/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp b/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp new file mode 100644 index 000000000..c5ac2b4db --- /dev/null +++ b/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp @@ -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(); +} \ No newline at end of file diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index 63014aea2..f0899a725 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -971,6 +971,9 @@ void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlow void UFlowAsset::DeinitializeInstance() { + // These should have been flushed in FinishFlow() + check(DeferredTransitionScopes.IsEmpty()); + if (IsInstanceInitialized()) { for (const TPair& Node : ObjectPtrDecay(Nodes)) @@ -1037,6 +1040,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) { @@ -1058,6 +1063,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& ScopePtr : DeferredTransitionScopes) + { + if (!ScopePtr.IsValid()) + { + continue; + } + + const TArray& 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; @@ -1081,11 +1142,6 @@ TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGr void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const { - if (FFlowExecutionGate::IsHalted()) - { - return; - } - // NOTE (gtaylor) Custom Input nodes cannot currently add data pins (like Start or DefineProperties nodes can) // but we may want to allow them to source parameters, so I am providing the subgraph node as the // IFlowDataPinValueSupplierInterface when triggering the node (even though it's not used at this time). @@ -1099,11 +1155,6 @@ void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNod void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier) { - if (FFlowExecutionGate::IsHalted()) - { - return; - } - for (UFlowNode_CustomInput* CustomInputNode : CustomInputNodes) { if (CustomInputNode->EventName == EventName) @@ -1143,11 +1194,34 @@ void UFlowAsset::TriggerCustomOutput(const FName& EventName) void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) { - if (FFlowExecutionGate::EnqueueDeferredTriggerInput(this, NodeGuid, PinName, FromPin)) + if (ShouldDeferTriggersForDebugger()) { - return; + 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 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)) { if (!ActiveNodes.Contains(Node)) @@ -1160,6 +1234,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 UFlowAsset::PushDeferredTransitionScope() +{ + // Close the former top scope (if any) + if (!DeferredTransitionScopes.IsEmpty()) + { + const TSharedPtr& FormerTop = DeferredTransitionScopes.Top(); + FormerTop->CloseScope(); + } + + // Push a fresh open scope + return DeferredTransitionScopes.Add_GetRef(MakeShared()); +} + +bool UFlowAsset::TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& 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 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 UFlowAsset::GetTopDeferredTransitionScope() const +{ + return !DeferredTransitionScopes.IsEmpty() ? DeferredTransitionScopes.Top() : nullptr; +} + void UFlowAsset::FinishNode(UFlowNode* Node) { if (ActiveNodes.Contains(Node)) diff --git a/Source/Flow/Private/FlowSettings.cpp b/Source/Flow/Private/FlowSettings.cpp index e098071fd..e6f835f4e 100644 --- a/Source/Flow/Private/FlowSettings.cpp +++ b/Source/Flow/Private/FlowSettings.cpp @@ -11,6 +11,7 @@ UFlowSettings::UFlowSettings(const FObjectInitializer& ObjectInitializer) , bWarnAboutMissingIdentityTags(true) , bLogOnSignalDisabled(true) , bLogOnSignalPassthrough(true) + , bDeferTriggeredOutputsWhileTriggering(true) , bUseAdaptiveNodeTitles(false) , DefaultExpectedOwnerClass(UFlowComponent::StaticClass()) { diff --git a/Source/Flow/Private/FlowSubsystem.cpp b/Source/Flow/Private/FlowSubsystem.cpp index 70a88eafe..9a3cf1ec7 100644 --- a/Source/Flow/Private/FlowSubsystem.cpp +++ b/Source/Flow/Private/FlowSubsystem.cpp @@ -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" @@ -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 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; diff --git a/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp index 44fd864f2..e09b7d02a 100644 --- a/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp +++ b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp @@ -5,20 +5,6 @@ #include "FlowAsset.h" #include "Nodes/FlowPin.h" -namespace FlowExecutionGate_Private -{ - struct FDeferredTriggerInput - { - TWeakObjectPtr FlowAssetInstance; - FGuid NodeGuid; - FName PinName; - FConnectedPin FromPin; - }; - - static TArray DeferredTriggerInputs; - static bool bIsFlushing = false; -} - IFlowExecutionGate* FFlowExecutionGate::Gate = nullptr; void FFlowExecutionGate::SetGate(IFlowExecutionGate* InGate) @@ -34,101 +20,4 @@ IFlowExecutionGate* FFlowExecutionGate::GetGate() bool FFlowExecutionGate::IsHalted() { return (Gate != nullptr) && Gate->IsFlowExecutionHalted(); -} - -bool FFlowExecutionGate::EnqueueDeferredTriggerInput(UFlowAsset* FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) -{ - using namespace FlowExecutionGate_Private; - - // If we're halted, always enqueue (even during flushing). The whole point is to stop propagation. - if (IsHalted()) - { - if (!IsValid(FlowAssetInstance)) - { - return true; // treat as handled while halted - } - - FDeferredTriggerInput& Entry = DeferredTriggerInputs.AddDefaulted_GetRef(); - Entry.FlowAssetInstance = FlowAssetInstance; - Entry.NodeGuid = NodeGuid; - Entry.PinName = PinName; - Entry.FromPin = FromPin; - - return true; - } - - // Not halted: - // During flush we must not enqueue "normal" triggers (we want them to execute now), - // otherwise we can get infinite deferral. - if (bIsFlushing) - { - return false; - } - - return false; -} - -void FFlowExecutionGate::FlushDeferredTriggerInputs() -{ - using namespace FlowExecutionGate_Private; - - if (bIsFlushing) - { - return; - } - - // Do not flush while halted; callers should clear the halt first. - if (IsHalted()) - { - return; - } - - if (DeferredTriggerInputs.IsEmpty()) - { - return; - } - - bIsFlushing = true; - - // Move into a local array so new deferred triggers can be added while we flush. - TArray Local = MoveTemp(DeferredTriggerInputs); - DeferredTriggerInputs.Reset(); - - for (int32 Index = 0; Index < Local.Num(); ++Index) - { - // If a breakpoint was hit during this flush, stop immediately and re-queue remaining work. - if (IsHalted()) - { - const int32 Remaining = Local.Num() - Index; - if (Remaining > 0) - { - TArray RemainingItems; - RemainingItems.Reserve(Remaining); - - for (int32 j = Index; j < Local.Num(); ++j) - { - RemainingItems.Add(Local[j]); - } - - // RemainingItems should run before any items that may already be queued. - if (!DeferredTriggerInputs.IsEmpty()) - { - RemainingItems.Append(MoveTemp(DeferredTriggerInputs)); - } - - DeferredTriggerInputs = MoveTemp(RemainingItems); - } - - bIsFlushing = false; - return; - } - - const FDeferredTriggerInput& Entry = Local[Index]; - if (UFlowAsset* Asset = Entry.FlowAssetInstance.Get()) - { - Asset->TriggerDeferredInputFromDebugger(Entry.NodeGuid, Entry.PinName, Entry.FromPin); - } - } - - bIsFlushing = false; } \ No newline at end of file diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn.h b/Source/Flow/Public/AddOns/FlowNodeAddOn.h index e2d2931be..abe446cf9 100644 --- a/Source/Flow/Public/AddOns/FlowNodeAddOn.h +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn.h @@ -40,10 +40,6 @@ class UFlowNodeAddOn : public UFlowNodeBase public: // UFlowNodeBase -#if WITH_EDITOR - FLOW_API virtual UEdGraphNode* GetGraphNode() const override; -#endif - // AddOns may opt in to be eligible for a given parent // - ParentTemplate - the template of the FlowNode or FlowNodeAddOn that is being considered as a potential parent // - AdditionalAddOnsToAssumeAreChildren - other AddOns to assume that are already child AddOns for the purposes of this test. diff --git a/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h b/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h new file mode 100644 index 000000000..66a3783be --- /dev/null +++ b/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h @@ -0,0 +1,36 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Misc/Guid.h" + +#include "Nodes/FlowPin.h" + +class UFlowAsset; + +struct FFlowDeferredTriggerInput +{ + FGuid NodeGuid; + FName PinName; + FConnectedPin FromPin; +}; + +struct FLOW_API FFlowDeferredTransitionScope +{ +public: + void EnqueueDeferredTrigger(const FFlowDeferredTriggerInput& Entry); + bool TryFlushDeferredTriggers(UFlowAsset& OwningFlowAsset); + + void CloseScope() { bIsOpen = false; } + bool IsOpen() const { return bIsOpen; } + + const TArray& GetDeferredTriggers() const { return DeferredTriggers; } + +protected: + + // Deferred triggers for this scope + TArray DeferredTriggers; + + // Is currently accepting new deferred triggers + bool bIsOpen = true; +}; diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index bd16210e7..ec5d20981 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -5,13 +5,15 @@ #include "FlowSave.h" #include "FlowTypes.h" #include "Asset/FlowAssetParamsTypes.h" +#include "Asset/FlowDeferredTransitionScope.h" #include "Nodes/FlowNode.h" #if WITH_EDITOR #include "FlowMessageLog.h" #endif - +#include "Templates/SharedPointer.h" #include "UObject/ObjectKey.h" + #include "FlowAsset.generated.h" class UFlowNode_CustomOutput; @@ -46,6 +48,7 @@ class FLOW_API UFlowAsset : public UObject friend class FFlowAssetDetails; friend class FFlowNode_SubGraphDetails; friend class UFlowGraphSchema; + friend struct FFlowDeferredTransitionScope; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow Asset") FGuid AssetGuid; @@ -396,6 +399,44 @@ class FLOW_API UFlowAsset : public UObject UFUNCTION(BlueprintPure, Category = "Flow") const TArray& GetRecordedNodes() const { return RecordedNodes; } +////////////////////////////////////////////////////////////////////////// +// Deferred trigger support + +public: + // Try to flush (and clear) all Deferred Trigger scopes + // (can fail to flush all if a FFlowExecutionGate causes a new halt) + bool TryFlushAllDeferredTriggerScopes(); + + // Clear (do not trigger) any remaining deferred transitions + // (for shutdown cases) + void ClearAllDeferredTriggerScopes(); + +protected: + /** Stack of active deferred transition scopes (innermost = top). + * Stored as TSharedPtr so callers can safely cache a reference to a specific scope + * without it being invalidated by array reallocations/resizes during nested triggers. */ + TArray> DeferredTransitionScopes; + + bool ShouldDeferTriggersForDebugger() const; + + // Allow subclasses to disable the standard defer trigger mechanism + virtual bool ShouldUseStandardDeferTriggers() const; + + void EnqueueDeferredTrigger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); + bool TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& Scope); + + TSharedPtr PushDeferredTransitionScope(); + void PopDeferredTransitionScope(const TSharedPtr& Scope) { TryFlushAndRemoveDeferredTransitionScope(Scope); } + + void CancelAndWarnForUnflushedDeferredTriggers(); + + /** Returns a shared pointer to the current top (innermost) deferred transition scope, + * or nullptr if there is no active scope. Safe to cache and use later. */ + TSharedPtr GetTopDeferredTransitionScope() const; + + // Trigger the node directly (no deferral, no new scope) + void TriggerInputDirect(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); + ////////////////////////////////////////////////////////////////////////// // Expected Owner Class support (for use with CallOwnerFunction nodes) diff --git a/Source/Flow/Public/FlowSettings.h b/Source/Flow/Public/FlowSettings.h index 1a8468bbc..fc9cd9b05 100644 --- a/Source/Flow/Public/FlowSettings.h +++ b/Source/Flow/Public/FlowSettings.h @@ -39,6 +39,12 @@ class FLOW_API UFlowSettings : public UDeveloperSettings UPROPERTY(Config, EditAnywhere, Category = "Flow") bool bLogOnSignalPassthrough; + // If enabled, defer the Triggered Outputs for a FlowAsset while it is currently processing a TriggeredInput. + // This is the new default in Flow, but the old behavior is provided (via false, here) + // for backward compatability during the transition. + UPROPERTY(Config, EditAnywhere, Category = "Flow") + bool bDeferTriggeredOutputsWhileTriggering; + // Adjust the Titles for FlowNodes to be more expressive than default // by incorporating data that would otherwise go in the Description UPROPERTY(EditAnywhere, config, Category = "Nodes") diff --git a/Source/Flow/Public/FlowSubsystem.h b/Source/Flow/Public/FlowSubsystem.h index 492742214..201e4e852 100644 --- a/Source/Flow/Public/FlowSubsystem.h +++ b/Source/Flow/Public/FlowSubsystem.h @@ -59,6 +59,15 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem static FNativeFlowAssetEvent OnInstancedTemplateRemoved; #endif +public: + // Try to flush (and clear) all Deferred Trigger scopes + // (can fail to flush all if a FFlowExecutionGate causes a new halt) + bool TryFlushAllDeferredTriggerScopes(); + + // Clear (do not trigger) any remaining deferred transitions + // (for shutdown cases) + void ClearAllDeferredTriggerScopes(); + protected: UPROPERTY() TObjectPtr LoadedSaveGame; diff --git a/Source/Flow/Public/Interfaces/FlowExecutionGate.h b/Source/Flow/Public/Interfaces/FlowExecutionGate.h index 8e1abdf66..8859dc266 100644 --- a/Source/Flow/Public/Interfaces/FlowExecutionGate.h +++ b/Source/Flow/Public/Interfaces/FlowExecutionGate.h @@ -31,15 +31,6 @@ class FLOW_API FFlowExecutionGate /** True if a gate exists and it currently wants Flow execution halted. */ static bool IsHalted(); - /** If halted, queues the trigger for later. Returns true if queued (caller should early-out). */ - static bool EnqueueDeferredTriggerInput(UFlowAsset* FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName, const struct FConnectedPin& FromPin); - - /** - * Flushes queued trigger inputs (FIFO). - * Safe to call even if nothing is queued. - */ - static void FlushDeferredTriggerInputs(); - private: static IFlowExecutionGate* Gate; }; \ No newline at end of file diff --git a/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h index 0fdf3fa77..3c7825607 100644 --- a/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h +++ b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h @@ -57,4 +57,7 @@ class FLOW_API UFlowNode_Log : public UFlowNode_DefineProperties virtual void UpdateNodeConfigText_Implementation() override; #endif + +public: + EFlowLogVerbosity GetVerbosity() const { return Verbosity; } }; diff --git a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp index e014fe8e1..4a731fe1d 100644 --- a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp @@ -26,6 +26,8 @@ void UFlowDebuggerSubsystem::Initialize(FSubsystemCollectionBase& Collection) Super::Initialize(Collection); FFlowExecutionGate::SetGate(this); + + SetFlowDebuggerState(EFlowDebuggerState::InitialRunning, nullptr); } void UFlowDebuggerSubsystem::Deinitialize() @@ -35,6 +37,8 @@ void UFlowDebuggerSubsystem::Deinitialize() FFlowExecutionGate::SetGate(nullptr); } + SetFlowDebuggerState(EFlowDebuggerState::Invalid, nullptr); + Super::Deinitialize(); } @@ -373,20 +377,6 @@ bool UFlowDebuggerSubsystem::HasAnyBreakpointsMatching(const TWeakObjectPtrGetFlowAsset(); - HaltedOnNodeGuid = Node->NodeGuid; -} - -void UFlowDebuggerSubsystem::ClearHaltFlowExecution() -{ - bHaltFlowExecution = false; - HaltedOnFlowAssetInstance.Reset(); - HaltedOnNodeGuid.Invalidate(); -} - void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() { if (!LastHitNodeGuid.IsValid()) @@ -428,11 +418,9 @@ void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode) LastHitNodeGuid = FlowNode->NodeGuid; LastHitPinName = NAME_None; - RequestHaltFlowExecution(FlowNode); - OnDebuggerBreakpointHit.Broadcast(FlowNode); - PauseSession(*FlowNode); + PauseSession(*FlowNode->GetFlowAsset()); } } } @@ -451,94 +439,26 @@ void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode, const FName& P LastHitNodeGuid = FlowNode->NodeGuid; LastHitPinName = PinName; - RequestHaltFlowExecution(FlowNode); - OnDebuggerBreakpointHit.Broadcast(FlowNode); - PauseSession(*FlowNode); + PauseSession(*FlowNode->GetFlowAsset()); } } } -void UFlowDebuggerSubsystem::PauseSession(const UFlowNode& FlowNode) +void UFlowDebuggerSubsystem::PauseSession(UFlowAsset& FlowAssetInstance) { - SetPause(FlowNode, true); + SetFlowDebuggerState(EFlowDebuggerState::Paused, &FlowAssetInstance); } -void UFlowDebuggerSubsystem::ResumeSession(const UFlowNode& FlowNode) +void UFlowDebuggerSubsystem::ResumeSession(UFlowAsset& FlowAssetInstance) { - SetPause(FlowNode, false); + SetFlowDebuggerState(EFlowDebuggerState::Resumed, &FlowAssetInstance); } -void UFlowDebuggerSubsystem::SetPause(const UFlowNode& FlowNode, const bool bPause) +void UFlowDebuggerSubsystem::StopSession() { - // Default bWasPaused to opposite of bPause - // (which we hope to get a better measure if we can get access to what we need) - bool bWasPaused = !bPause; - - AGameModeBase* GameMode = nullptr; - APlayerController* PlayerController = nullptr; - - const UFlowAsset* FlowAssetInstance = FlowNode.GetFlowAsset(); - const UWorld* World = FlowAssetInstance->GetWorld(); - if (IsValid(World)) - { - GameMode = World->GetAuthGameMode(); - - if (IsValid(GameMode)) - { - bWasPaused = GameMode->IsPaused(); - } - - const UGameInstance* GameInstance = World->GetGameInstance(); - if (IsValid(GameInstance)) - { - PlayerController = GameInstance->GetFirstLocalPlayerController(); - } - } - - if (bWasPaused != bPause) - { - if (bPause) - { - // Pausing (from an unpaused state) - - if (IsValid(PlayerController)) - { - if (IsValid(GameMode)) - { - GameMode->SetPause(PlayerController); - } - - if (AWorldSettings* WorldSettings = PlayerController->GetWorldSettings()) - { - WorldSettings->ForceNetUpdate(); - } - } - - // Broadcast the Pause event - OnDebuggerPaused.Broadcast(*FlowAssetInstance); - } - else - { - // Resuming (from a paused state) - - ClearHaltFlowExecution(); - - // Replay any Flow propagation that was deferred while execution was halted. - FFlowExecutionGate::FlushDeferredTriggerInputs(); - - // Intentionally do NOT clear hit flags here. The editor-specific resume path will clear the last-hit - // breakpoint safely (without racing against immediate breakpoint hits during flush). - if (IsValid(GameMode)) - { - (void) GameMode->ClearPause(); - } - - // Broadcast the Resume event - OnDebuggerResumed.Broadcast(*FlowAssetInstance); - } - } + SetFlowDebuggerState(EFlowDebuggerState::Invalid, nullptr); } void UFlowDebuggerSubsystem::ClearHitBreakpoints() @@ -555,8 +475,7 @@ void UFlowDebuggerSubsystem::ClearHitBreakpoints() } } - LastHitNodeGuid.Invalidate(); - LastHitPinName = NAME_None; + ClearLastHitBreakpoint(); } bool UFlowDebuggerSubsystem::IsBreakpointHit(const FGuid& NodeGuid) @@ -583,4 +502,78 @@ void UFlowDebuggerSubsystem::SaveSettings() { UFlowDebuggerSettings* Settings = GetMutableDefault(); Settings->SaveConfig(); -} \ No newline at end of file +} + +void UFlowDebuggerSubsystem::SetFlowDebuggerState(EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) +{ + if (FlowDebuggerState == NextState) + { + return; + } + + const EFlowDebuggerState PrevState = FlowDebuggerState; + FlowDebuggerState = NextState; + + ManageGameModePaused(PrevState, NextState, FlowAssetInstance); + + // OnFlowDebuggerStateChanged MUST be the final operation in SetFlowDebuggerState + // as it could potentially cause a new FlowDebuggerState entered + { + OnFlowDebuggerStateChanged(PrevState, NextState, FlowAssetInstance); + return; + } +} + +void UFlowDebuggerSubsystem::ManageGameModePaused(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) +{ + if (!IsValid(FlowAssetInstance)) + { + return; + } + + const UWorld* World = FlowAssetInstance->GetWorld(); + AGameModeBase* GameMode = World->GetAuthGameMode(); + if (!IsValid(GameMode)) + { + // No game mode on non-server instances + return; + } + + using namespace EFlowDebuggerState_Classifiers; + + const bool bIsPauseGameModeStatePrev = IsPausedGameState(PrevState); + const bool bIsPauseGameModeStateNext = IsPausedGameState(NextState); + + if (bIsPauseGameModeStatePrev == bIsPauseGameModeStateNext) + { + return; + } + + // Gather some pointers + const UGameInstance* GameInstance = World->GetGameInstance(); + APlayerController* FirstLocalPlayerController = nullptr; + if (IsValid(GameInstance)) + { + FirstLocalPlayerController = GameInstance->GetFirstLocalPlayerController(); + } + + // Change the GameMode pause state + if (bIsPauseGameModeStateNext) + { + if (FirstLocalPlayerController) + { + GameMode->SetPause(FirstLocalPlayerController); + + if (AWorldSettings* WorldSettings = World->GetWorldSettings()) + { + WorldSettings->ForceNetUpdate(); + } + } + } + else + { + // Intentionally do NOT clear hit flags here. The editor-specific resume path will clear the last-hit + // breakpoint safely (without racing against immediate breakpoint hits during flush). + (void)GameMode->ClearPause(); + } +} diff --git a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h index bc4f42219..ef454af6d 100644 --- a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h @@ -6,6 +6,7 @@ #include "Debugger/FlowDebuggerTypes.h" #include "Interfaces/FlowExecutionGate.h" +#include "Types/FlowEnumUtils.h" #include "FlowDebuggerSubsystem.generated.h" @@ -14,6 +15,37 @@ class UEdGraphNode; class UFlowAsset; class UFlowNode; +UENUM() +enum class EFlowDebuggerState +{ + // Initialized, running, but never halted + InitialRunning, + + // Running after being pausing + Resumed, + + // Currently paused at a breakpoint + Paused, + + Max UMETA(Hidden), + Invalid = -1 UMETA(Hidden), + Min = 0 UMETA(Hidden), + + // Subranges for classifier checks + PausedGameFirst = Paused UMETA(Hidden), + PausedGameLast = Paused UMETA(Hidden), + + FlushDeferredTriggersFirst = Resumed UMETA(Hidden), + FlushDeferredTriggersLast = Resumed UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowDebuggerState); + +namespace EFlowDebuggerState_Classifiers +{ + FORCEINLINE bool IsPausedGameState(EFlowDebuggerState State) { return FLOW_IS_ENUM_IN_SUBRANGE(State, EFlowDebuggerState::PausedGame); } + FORCEINLINE bool IsFlushDeferredTriggersState(EFlowDebuggerState State) { return FLOW_IS_ENUM_IN_SUBRANGE(State, EFlowDebuggerState::FlushDeferredTriggers); } +} + DECLARE_MULTICAST_DELEGATE_OneParam(FFlowAssetDebuggerEvent, const UFlowAsset& /*FlowAsset*/); DECLARE_MULTICAST_DELEGATE_OneParam(FFlowAssetDebuggerBreakpointHitEvent, const UFlowNode* /*FlowNode*/); @@ -42,7 +74,7 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public public: // IFlowExecutionGate - virtual bool IsFlowExecutionHalted() const override { return bHaltFlowExecution; } + virtual bool IsFlowExecutionHalted() const override { return EFlowDebuggerState_Classifiers::IsPausedGameState(FlowDebuggerState); } // -- virtual void AddBreakpoint(const FGuid& NodeGuid); @@ -79,9 +111,9 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public virtual void MarkAsHit(const UFlowNode* FlowNode); virtual void MarkAsHit(const UFlowNode* FlowNode, const FName& PinName); - virtual void PauseSession(const UFlowNode& FlowNode); - virtual void ResumeSession(const UFlowNode& FlowNode); - void SetPause(const UFlowNode& FlowNode, const bool bPause); + virtual void PauseSession(UFlowAsset& FlowAssetInstance); + virtual void ResumeSession(UFlowAsset& FlowAssetInstance); + virtual void StopSession(); /** * Clears the "currently hit" breakpoint only (node or pin). @@ -92,9 +124,12 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public /** Clears hit state for all breakpoints. Prefer ClearLastHitBreakpoint() for resume/step logic. */ virtual void ClearHitBreakpoints(); +private: + void SetFlowDebuggerState(EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); + void ManageGameModePaused(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); + protected: - void RequestHaltFlowExecution(const UFlowNode* Node); - void ClearHaltFlowExecution(); + virtual void OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) {} public: virtual bool IsBreakpointHit(const FGuid& NodeGuid); @@ -106,10 +141,8 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public FFlowAssetDebuggerBreakpointHitEvent OnDebuggerBreakpointHit; FFlowAssetDebuggerEvent OnDebuggerFlowAssetTemplateRemoved; -private: - bool bHaltFlowExecution = false; - TWeakObjectPtr HaltedOnFlowAssetInstance; - FGuid HaltedOnNodeGuid; +protected: + EFlowDebuggerState FlowDebuggerState = EFlowDebuggerState::Invalid; // Track the single breakpoint location that is currently "hit" (node or pin). FGuid LastHitNodeGuid; diff --git a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp index 90cac2020..eed0440a5 100644 --- a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp +++ b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp @@ -9,6 +9,7 @@ #include "Graph/Nodes/FlowGraphNode.h" #include "Interfaces/FlowExecutionGate.h" #include "FlowAsset.h" +#include "FlowSubsystem.h" #include "Editor/UnrealEdEngine.h" #include "Engine/Engine.h" @@ -73,13 +74,12 @@ void UFlowDebugEditorSubsystem::OnResumePIE(const bool bIsSimulating) { // Editor-level resume event (also used by Advance Single Frame). // This does not necessarily flow through AGameModeBase::ClearPause(), so we must unhalt Flow here. - // - // Clear only the last-hit breakpoint to return to enabled/disabled visuals without racing against - // a newly hit breakpoint during FlushDeferredTriggerInputs(). - ClearHaltFlowExecution(); ClearLastHitBreakpoint(); - FFlowExecutionGate::FlushDeferredTriggerInputs(); + if (HaltedOnFlowAssetInstance.IsValid()) + { + ResumeSession(*HaltedOnFlowAssetInstance.Get()); + } } void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) @@ -87,8 +87,7 @@ void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) // Ensure we don't carry over a halted state between PIE sessions. ClearHitBreakpoints(); - ClearHaltFlowExecution(); - FFlowExecutionGate::FlushDeferredTriggerInputs(); + StopSession(); for (const TPair, TSharedPtr>& Log : RuntimeLogs) { @@ -116,27 +115,86 @@ void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) } } -void UFlowDebugEditorSubsystem::PauseSession(const UFlowNode& FlowNode) +void UFlowDebugEditorSubsystem::PauseSession(UFlowAsset& FlowAssetInstance) +{ + HaltedOnFlowAssetInstance = &FlowAssetInstance; + + Super::PauseSession(FlowAssetInstance); +} + +void UFlowDebugEditorSubsystem::ResumeSession(UFlowAsset& FlowAssetInstance) { - Super::PauseSession(FlowNode); + HaltedOnFlowAssetInstance = &FlowAssetInstance; + + Super::ResumeSession(FlowAssetInstance); +} - constexpr bool bShouldBePaused = true; - const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); - if (!bWasPaused) +void UFlowDebugEditorSubsystem::StopSession() +{ + // Drop any pending deferred triggers — we are stopping the session entirely + if (HaltedOnFlowAssetInstance.IsValid()) { - GUnrealEd->PlaySessionPaused(); + UFlowSubsystem* FlowSubsystem = HaltedOnFlowAssetInstance->GetFlowSubsystem(); + + if (IsValid(FlowSubsystem)) + { + FlowSubsystem->ClearAllDeferredTriggerScopes(); + } } + + HaltedOnFlowAssetInstance.Reset(); + + Super::StopSession(); } -void UFlowDebugEditorSubsystem::ResumeSession(const UFlowNode& FlowNode) +void UFlowDebugEditorSubsystem::OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) { - Super::ResumeSession(FlowNode); + check(PrevState != NextState); + + using namespace EFlowDebuggerState_Classifiers; + + const bool bIsPausedGameStatePrev = IsPausedGameState(PrevState); + const bool bIsPausedGameStateNext = IsPausedGameState(NextState); - constexpr bool bShouldBePaused = false; - const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); - if (bWasPaused) + // Handle Pause/Unpause of the game & pie systems + if (bIsPausedGameStatePrev != bIsPausedGameStateNext) { - GUnrealEd->PlaySessionResumed(); + const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bIsPausedGameStateNext); + + if (bIsPausedGameStateNext && !bWasPaused) + { + GUnrealEd->PlaySessionPaused(); + } + else if (!bIsPausedGameStateNext && bWasPaused) + { + GUnrealEd->PlaySessionResumed(); + } + } + + // Issue the broadcasts for specific state entry + FLOW_ASSERT_ENUM_MAX(EFlowDebuggerState, 3); + if (NextState == EFlowDebuggerState::Paused) + { + OnDebuggerPaused.Broadcast(*FlowAssetInstance); + } + else if (NextState == EFlowDebuggerState::Resumed) + { + OnDebuggerResumed.Broadcast(*FlowAssetInstance); + } + + UFlowSubsystem* FlowSubsystem = + IsValid(FlowAssetInstance) ? + FlowAssetInstance->GetFlowSubsystem() : + nullptr; + + if (FlowSubsystem && IsFlushDeferredTriggersState(NextState)) + { + // Flush any deferred triggers now that halt is cleared. + FlowSubsystem->TryFlushAllDeferredTriggerScopes(); + + // NOTE (gtaylor) this flush needs to be the last thing we do in this function + // (thus the explicit return to emphasize it), as this flush can be interrupted by another breakpoint + return; } } diff --git a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h index 4b2f839be..6833c0570 100644 --- a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h +++ b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h @@ -24,6 +24,8 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem protected: TMap, TSharedPtr> RuntimeLogs; + TWeakObjectPtr HaltedOnFlowAssetInstance; + virtual void OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) override; virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) override; @@ -33,8 +35,10 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem virtual void OnResumePIE(const bool bIsSimulating); virtual void OnEndPIE(const bool bIsSimulating); - virtual void PauseSession(const UFlowNode& FlowNode) override; - virtual void ResumeSession(const UFlowNode& FlowNode) override; + virtual void PauseSession(UFlowAsset& FlowAssetInstance) override; + virtual void ResumeSession(UFlowAsset& FlowAssetInstance) override; + virtual void StopSession() override; + virtual void OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); void OnBreakpointHit(const UFlowNode* FlowNode) const; };