diff --git a/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp b/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp new file mode 100644 index 00000000..c5ac2b4d --- /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 a6d32fb4..88effd0a 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" @@ -645,30 +646,34 @@ bool UFlowAsset::TryUpdateManagedFlowPinsForNode(UFlowNode& FlowNode) FlowNode.GetAutoInputDataPins(), FlowNode.GetAutoOutputDataPins()); - // Allow the node to auto-generate data pins - FlowNode.AutoGenerateDataPins(WorkingData); + bool bAutoInputDataPinsChanged = false; + bool bAutoOutputDataPinsChanged = false; - // If the auto-generated data pins array changed, it counts as dirty as well - const bool bAutoInputDataPinsChanged = WorkingData.DidAutoInputDataPinsChange(); - const bool bAutoOutputDataPinsChanged = WorkingData.DidAutoOutputDataPinsChange(); - - if (bAutoInputDataPinsChanged || bAutoOutputDataPinsChanged) + if (WorkingData.AutoGenerateDataPinsForFlowNode(FlowNode, bAutoInputDataPinsChanged, bAutoOutputDataPinsChanged)) { FlowNode.SetFlags(RF_Transactional); FlowNode.Modify(); // Lock-in the data that changed. - if (bAutoInputDataPinsChanged || bAutoOutputDataPinsChanged) + TArray FlowPinsNext; + const int32 LargestPinNum = FMath::Max(WorkingData.AutoInputDataPinsNext.Num(), WorkingData.AutoOutputDataPinsNext.Num()); + + if (bAutoInputDataPinsChanged) { - if (bAutoInputDataPinsChanged) - { - FlowNode.SetAutoInputDataPins(WorkingData.AutoInputDataPinsNext); - } + FlowPinsNext.Reserve(LargestPinNum); - if (bAutoOutputDataPinsChanged) - { - FlowNode.SetAutoOutputDataPins(WorkingData.AutoOutputDataPinsNext); - } + WorkingData.BuildNextFlowPinArray(WorkingData.AutoInputDataPinsNext, FlowPinsNext); + + FlowNode.SetAutoInputDataPins(FlowPinsNext); + } + + if (bAutoOutputDataPinsChanged) + { + FlowPinsNext.Reserve(LargestPinNum); + + WorkingData.BuildNextFlowPinArray(WorkingData.AutoOutputDataPinsNext, FlowPinsNext); + + FlowNode.SetAutoOutputDataPins(FlowPinsNext); } FlowNode.PostEditChange(); @@ -970,6 +975,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)) @@ -1012,6 +1020,11 @@ void UFlowAsset::PreStartFlow() void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier) { + if (FFlowExecutionGate::IsHalted()) + { + return; + } + PreStartFlow(); if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode()) @@ -1031,6 +1044,8 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool b { FinishPolicy = InFinishPolicy; + CancelAndWarnForUnflushedDeferredTriggers(); + // end execution of this asset and all of its nodes for (UFlowNode* Node : ActiveNodes) { @@ -1052,6 +1067,62 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool b } } +void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers() +{ + // Aggressively drop any pending deferred triggers — graph is done + // In normal execution these should have been flushed via PopDeferredTransitionScope() in TriggerInputDirect + // In the debugger they should have been flushed by ResumePIE + // Remaining scopes here usually mean: + // - early/abnormal termination (e.g. FinishFlow called from unexpected place) + // - exception/early return before Pop + // - forced deinitialization during active execution (e.g. PIE stop, subsystem cleanup) + if (!DeferredTransitionScopes.IsEmpty()) + { + int32 TotalDroppedTriggers = 0; + + for (const TSharedPtr& 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; @@ -1126,6 +1197,34 @@ void UFlowAsset::TriggerCustomOutput(const FName& EventName) } void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) +{ + if (ShouldDeferTriggersForDebugger()) + { + EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); + } + else if (ShouldUseStandardDeferTriggers()) + { + // Defer only if we have an open top scope + if (!DeferredTransitionScopes.IsEmpty() && DeferredTransitionScopes.Top()->IsOpen()) + { + EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); + } + else + { + const TSharedPtr 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)) { @@ -1139,6 +1238,86 @@ void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const } } +bool UFlowAsset::ShouldDeferTriggersForDebugger() const +{ + // Halt always takes precedence for debugger correctness + return FFlowExecutionGate::IsHalted(); +} + +bool UFlowAsset::ShouldUseStandardDeferTriggers() const +{ + return UFlowSettings::Get()->bDeferTriggeredOutputsWhileTriggering; +} + +TSharedPtr 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 e098071f..e6f835f4 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 70a88eaf..9a3cf1ec 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 new file mode 100644 index 00000000..e09b7d02 --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp @@ -0,0 +1,23 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowExecutionGate.h" + +#include "FlowAsset.h" +#include "Nodes/FlowPin.h" + +IFlowExecutionGate* FFlowExecutionGate::Gate = nullptr; + +void FFlowExecutionGate::SetGate(IFlowExecutionGate* InGate) +{ + Gate = InGate; +} + +IFlowExecutionGate* FFlowExecutionGate::GetGate() +{ + return Gate; +} + +bool FFlowExecutionGate::IsHalted() +{ + return (Gate != nullptr) && Gate->IsFlowExecutionHalted(); +} \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index aa319b35..a4e8377f 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -483,9 +483,20 @@ bool UFlowNode::TryFindPropertyByPinName_Static( void UFlowNode::GatherPotentialPropertyOwnersForDataPins(TArray& InOutOwners) const { - // TODO (gtaylor) Also add any AddOns that can supply data pins, when/if we want to add AddOn data pin supply support + check(!InOutOwners.Contains(this)); - InOutOwners.AddUnique(this); + InOutOwners.Add(this); + + // Give all of the AddOns a chance to supply data pins as well + (void) ForEachAddOnConst( + [&](const UFlowNodeAddOn& AddOn) + { + check(!InOutOwners.Contains(&AddOn)); + + InOutOwners.Add(&AddOn); + + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); } bool UFlowNode::TryGatherPropertyOwnersAndPopulateResult( @@ -494,21 +505,65 @@ bool UFlowNode::TryGatherPropertyOwnersAndPopulateResult( const FFlowPin& FlowPin, FFlowDataPinResult& OutSuppliedResult) const { - // Gather all of the potential providers for this DataPin + // Gather all potential UObject instances that might own properties + // mapped to data pins on this node (usually the node itself + any referenced objects) TArray PropertyOwnerObjects; GatherPotentialPropertyOwnersForDataPins(PropertyOwnerObjects); - // Look through all of the potential providers - for (const UObject* PropertyOwnerObject : PropertyOwnerObjects) + // Early out if we have no possible owners at all + if (PropertyOwnerObjects.IsEmpty()) { - const UFlowNode& FlowNodeThis = *this; + LogError(FString::Printf(TEXT("No property owners available for data pin '%s' on node %s"), + *PinName.ToString(), *GetName()), EFlowOnScreenMessageType::Temporary); - checkf(IsValid(PropertyOwnerObject), TEXT("Every UObject provided by GatherPotentialPropertyOwnersForDataPins must be valid")); + return false; + } + + const UObject* PropertyOwnerObject = nullptr; + FName PropertyNameToLookup; + + // Look up explicit mapping (used for non-default owners or disambiguated pins) + if (const FFlowPinPropertySource* FlowPropertySource = MapDataPinNameToPropertySource.Find(PinName)) + { + const int32 OwnerIndex = FlowPropertySource->PropertyOwnerIndex; - if (DataPinType.PopulateResult(*PropertyOwnerObject, FlowNodeThis, FlowPin, OutSuppliedResult)) + if (PropertyOwnerObjects.IsValidIndex(OwnerIndex)) { - return true; + PropertyOwnerObject = PropertyOwnerObjects[OwnerIndex]; + PropertyNameToLookup = FlowPropertySource->PropertyName; } + else + { + // Critical: mapped index is out of bounds → configuration or generation bug + LogError(FString::Printf(TEXT("Invalid property owner index %d for pin '%s' on node %s (max %d owners)"), + OwnerIndex, *PinName.ToString(), *GetName(), PropertyOwnerObjects.Num() - 1), + EFlowOnScreenMessageType::Temporary); + + return false; + } + } + else + { + check(!PropertyOwnerObjects.IsEmpty()); + + // Fallback for unmapped pins → assume default owner (index 0) + pin name == property name + PropertyOwnerObject = PropertyOwnerObjects[0]; + PropertyNameToLookup = PinName; + } + + if (!PropertyOwnerObject) + { + LogError(FString::Printf(TEXT("Failed to resolve property owner for data pin '%s' on node %s"), + *PinName.ToString(), *GetName()), EFlowOnScreenMessageType::Temporary); + + return false; + } + + // Populate the value for the pin on the its owner object + const UFlowNode& FlowNodeThis = *this; + if (DataPinType.PopulateResult(*PropertyOwnerObject, FlowNodeThis, PropertyNameToLookup, OutSuppliedResult)) + { + return true; } return false; @@ -582,11 +637,13 @@ void UFlowNode::AutoGenerateDataPins(FFlowAutoDataPinsWorkingData& InOutWorkingD GatherPotentialPropertyOwnersForDataPins(PropertyOwnerObjects); // GenerateDataPins for all of the potential providers - for (const UObject* PropertyOwnerObject : PropertyOwnerObjects) + for (int32 PropertyOwnerIndex = 0; PropertyOwnerIndex < PropertyOwnerObjects.Num(); ++PropertyOwnerIndex) { + const UObject* PropertyOwnerObject = PropertyOwnerObjects[PropertyOwnerIndex]; + checkf(IsValid(PropertyOwnerObject), TEXT("Every UObject provided by GatherPotentialPropertyOwnersForDataPins must be valid")); - InOutWorkingData.AddFlowDataPinsForClassProperties(*PropertyOwnerObject); + InOutWorkingData.AddFlowDataPinsForClassProperties(*PropertyOwnerObject, PropertyOwnerIndex); } } #endif diff --git a/Source/Flow/Private/Nodes/FlowPin.cpp b/Source/Flow/Private/Nodes/FlowPin.cpp index 84a3351a..8c2a34cd 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/Nodes/Graph/FlowNode_DefineProperties.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_DefineProperties.cpp index 6d453eaf..10335e5f 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_DefineProperties.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_DefineProperties.cpp @@ -67,14 +67,16 @@ void UFlowNode_DefineProperties::AutoGenerateDataPins(FFlowAutoDataPinsWorkingDa if (DataPinProperty.IsValid()) { const FFlowDataPinValue& DataPinValue = DataPinProperty.DataPinValue.Get(); + const FName PropertyOwnerObjectName = GetFName(); + constexpr int32 PropertyOwnerIndex = 0; if (DataPinValue.IsInputPin()) { - InOutWorkingData.AutoInputDataPinsNext.AddUnique(DataPinProperty.CreateFlowPin()); + InOutWorkingData.AutoInputDataPinsNext.Add(FFlowPinSourceData(DataPinProperty.CreateFlowPin(), PropertyOwnerObjectName, PropertyOwnerIndex, &DataPinValue)); } else { - InOutWorkingData.AutoOutputDataPinsNext.AddUnique(DataPinProperty.CreateFlowPin()); + InOutWorkingData.AutoOutputDataPinsNext.Add(FFlowPinSourceData(DataPinProperty.CreateFlowPin(), PropertyOwnerObjectName, PropertyOwnerIndex, &DataPinValue)); } } } diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp index 9e74acc0..86916eef 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp @@ -212,7 +212,15 @@ void UFlowNode_SubGraph::AutoGenerateDataPins(FFlowAutoDataPinsWorkingData& InOu TArray ExternalInputPins; if (ExternalPinSuppliedNode->TryAppendExternalInputPins(ExternalInputPins)) { - InOutWorkingData.AutoInputDataPinsNext.Append(ExternalInputPins); + const int32 NewNum = InOutWorkingData.AutoInputDataPinsNext.Num() + ExternalInputPins.Num(); + InOutWorkingData.AutoInputDataPinsNext.Reserve(NewNum); + + const FName PropertyOwnerObjectName = GetFName(); + + for (const FFlowPin& FlowPin : ExternalInputPins) + { + InOutWorkingData.AutoInputDataPinsNext.Add(FFlowPinSourceData(FlowPin, PropertyOwnerObjectName)); + } } } } diff --git a/Source/Flow/Private/Types/FlowAutoDataPinsWorkingData.cpp b/Source/Flow/Private/Types/FlowAutoDataPinsWorkingData.cpp index 78107aa1..fd3fba57 100644 --- a/Source/Flow/Private/Types/FlowAutoDataPinsWorkingData.cpp +++ b/Source/Flow/Private/Types/FlowAutoDataPinsWorkingData.cpp @@ -2,31 +2,75 @@ #include "Types/FlowAutoDataPinsWorkingData.h" #include "FlowLogChannels.h" +#include "Nodes/FlowNode.h" #include "Types/FlowDataPinValue.h" #include "Types/FlowStructUtils.h" #if WITH_EDITOR + +bool FFlowAutoDataPinsWorkingData::AutoGenerateDataPinsForFlowNode(UFlowNode& FlowNode, bool& bAutoInputDataPinsChanged, bool& bAutoOutputDataPinsChanged) +{ + // Allow the node to auto-generate data pins + FlowNode.AutoGenerateDataPins(*this); + + DisambiguateAndRebuildDataPinPropertySourceMap(FlowNode); + + bAutoInputDataPinsChanged = DidAutoInputDataPinsChange(); + bAutoOutputDataPinsChanged = DidAutoOutputDataPinsChange(); + + // Return true if any of the data pins changed + return bAutoInputDataPinsChanged || bAutoOutputDataPinsChanged; +} + bool FFlowAutoDataPinsWorkingData::DidAutoInputDataPinsChange() const { - return !FFlowPin::DeepArePinArraysMatching(AutoInputDataPinsPrev, AutoInputDataPinsNext); + return !CheckIfProposedPinsMatchPreviousPins(AutoInputDataPinsPrev, AutoInputDataPinsNext); } bool FFlowAutoDataPinsWorkingData::DidAutoOutputDataPinsChange() const { - return !FFlowPin::DeepArePinArraysMatching(AutoOutputDataPinsPrev, AutoOutputDataPinsNext); + return !CheckIfProposedPinsMatchPreviousPins(AutoOutputDataPinsPrev, AutoOutputDataPinsNext); +} + +bool FFlowAutoDataPinsWorkingData::CheckIfProposedPinsMatchPreviousPins(const TArray& PrevPins, const TArray& ProposedPins) +{ + if (PrevPins.Num() != ProposedPins.Num()) + { + return false; + } + + for (int32 Index = 0; Index < PrevPins.Num(); ++Index) + { + if (!PrevPins[Index].DeepIsEqual(ProposedPins[Index].FlowPin)) + { + return false; + } + } + + return true; +} + +void FFlowAutoDataPinsWorkingData::BuildNextFlowPinArray(const TArray& PinSourceDatas, TArray& OutFlowPins) +{ + OutFlowPins.Reset(); + + for (const FFlowPinSourceData& PinSourceData : PinSourceDatas) + { + OutFlowPins.Add(PinSourceData.FlowPin); + } } -void FFlowAutoDataPinsWorkingData::AddFlowDataPinsForClassProperties(const UObject& ObjectContainer) +void FFlowAutoDataPinsWorkingData::AddFlowDataPinsForClassProperties(const UObject& ObjectContainer, int32 PropertyOwnerIndex) { // Try to harvest pins to auto-generate and/or bind to for each property in the flow node UClass* Class = ObjectContainer.GetClass(); for (TFieldIterator PropertyIt(Class); PropertyIt; ++PropertyIt) { - AddFlowDataPinForProperty(*PropertyIt, ObjectContainer); + AddFlowDataPinForProperty(*PropertyIt, ObjectContainer, PropertyOwnerIndex); } } -void FFlowAutoDataPinsWorkingData::AddFlowDataPinForProperty(const FProperty* Property, const UObject& ObjectContainer) +void FFlowAutoDataPinsWorkingData::AddFlowDataPinForProperty(const FProperty* Property, const UObject& ObjectContainer, int32 PropertyOwnerIndex) { bool bIsInputPin = false; @@ -94,7 +138,7 @@ void FFlowAutoDataPinsWorkingData::AddFlowDataPinForProperty(const FProperty* Pr bIsInputPin = bIsInputPin || DefaultForInputFlowPinName != nullptr; // Default assumption is the pin will be an output pin, unless metadata specifies otherwise - TArray* FlowPinArray = bIsInputPin ? &AutoInputDataPinsNext : &AutoOutputDataPinsNext; + TArray* FlowPinArray = bIsInputPin ? &AutoInputDataPinsNext : &AutoOutputDataPinsNext; // Create the new FlowPin FFlowPin NewFlowPin = FlowPinType->CreateFlowPinFromProperty(*Property, Container); @@ -117,7 +161,8 @@ void FFlowAutoDataPinsWorkingData::AddFlowDataPinForProperty(const FProperty* Pr } } - FlowPinArray->Add(NewFlowPin); + const FName PropertyOwnerObjectName = ObjectContainer.GetFName(); + FlowPinArray->Add(FFlowPinSourceData(NewFlowPin, PropertyOwnerObjectName, PropertyOwnerIndex, DataPinValue)); if (DataPinValue) { @@ -126,4 +171,183 @@ void FFlowAutoDataPinsWorkingData::AddFlowDataPinForProperty(const FProperty* Pr } } +void FFlowAutoDataPinsWorkingData::DisambiguateAndRebuildDataPinPropertySourceMap(UFlowNode& FlowNode) +{ + FlowNode.MapDataPinNameToPropertySource.Reset(); + + AddInputDataPinsToMap(FlowNode); + AddOutputDataPinsToMap(FlowNode); +} + +void FFlowAutoDataPinsWorkingData::AddInputDataPinsToMap(UFlowNode& FlowNode) +{ + for (const FFlowPinSourceData& PinSource : AutoInputDataPinsNext) + { + if (PinSource.FlowPin.IsExecPin()) + { + continue; + } + + if (PinSource.PropertyOwnerIndex == 0) + { + // default owner is inferred at runtime + continue; + } + + AddPinMappingToNode( + FlowNode, + PinSource.FlowPin.PinName, + PinSource.FlowPin.PinName, + PinSource.PropertyOwnerIndex); + } +} + +void FFlowAutoDataPinsWorkingData::AddOutputDataPinsToMap(UFlowNode& FlowNode) +{ + if (AutoOutputDataPinsNext.IsEmpty()) + { + return; + } + + // Group output pins by their original/base name to detect duplicates + TMap> MapPinNameToNextPinIndices; + for (int32 Idx = 0; Idx < AutoOutputDataPinsNext.Num(); ++Idx) + { + const FFlowPinSourceData& Pin = AutoOutputDataPinsNext[Idx]; + + const bool bIsDataPin = !Pin.FlowPin.IsExecPin(); + if (bIsDataPin) + { + TArray& UseCountArray = MapPinNameToNextPinIndices.FindOrAdd(Pin.FlowPin.PinName); + UseCountArray.Add(Idx); + } + } + + // Collect names that will remain unchanged (for collision avoidance during renaming) + TSet ReservedFinalNames; + for (const auto& KV : MapPinNameToNextPinIndices) + { + const TArray& Indices = KV.Value; + if (Indices.Num() == 1) + { + const int32 Idx = Indices[0]; + ReservedFinalNames.Add(AutoOutputDataPinsNext[Idx].FlowPin.PinName); + } + } + + // Process each group: map unique pins, rename and map duplicates + for (const auto& KV : MapPinNameToNextPinIndices) + { + const TArray& Indices = KV.Value; + if (Indices.Num() == 1) + { + const int32 Idx = Indices[0]; + const FFlowPinSourceData& Pin = AutoOutputDataPinsNext[Idx]; + + const bool bCanOmitPropertyFromMap = Pin.PropertyOwnerIndex == 0; + if (!bCanOmitPropertyFromMap) + { + AddPinMappingToNode( + FlowNode, + Pin.FlowPin.PinName, + Pin.FlowPin.PinName, + Pin.PropertyOwnerIndex); + } + + continue; + } + + // Handle duplicate names — assign unique suffixes + TSet UsedNames = ReservedFinalNames; + + // 1-based for user-facing display + uint32 LogicalDuplicateIndex = 1; + + for (int32 Idx : Indices) + { + FFlowPinSourceData& Pin = AutoOutputDataPinsNext[Idx]; + const FName OriginalName = Pin.FlowPin.PinName; + + DisambiguateDuplicateOutputDataPin(Pin, UsedNames, LogicalDuplicateIndex); + + const FName& DisambiguatedName = Pin.FlowPin.PinName; + AddPinMappingToNode( + FlowNode, + DisambiguatedName, + OriginalName, + Pin.PropertyOwnerIndex); + + ++LogicalDuplicateIndex; + } + } +} + +void FFlowAutoDataPinsWorkingData::AddPinMappingToNode( + UFlowNode& FlowNode, + const FName& FinalPinName, + const FName& OriginalPinName, + int32 PropertyOwnerIndex) +{ + // Omit trivial cases that runtime lookup can infer + if (PropertyOwnerIndex == 0 && FinalPinName == OriginalPinName) + { + return; + } + + FlowNode.MapDataPinNameToPropertySource.Add( + FinalPinName, + FFlowPinPropertySource(OriginalPinName, PropertyOwnerIndex)); +} + +void FFlowAutoDataPinsWorkingData::DisambiguateDuplicateOutputDataPin( + FFlowPinSourceData& PinSourceData, + TSet& InOutUsedNames, + uint32 LogicalDuplicateIndex) +{ + const FName BaseName = PinSourceData.FlowPin.PinName; + + uint32 DisambiguationSuffix = LogicalDuplicateIndex; + while (true) + { + const FName Candidate = FName(FString::Printf(TEXT("%s_%u"), *BaseName.ToString(), DisambiguationSuffix)); + if (!InOutUsedNames.Contains(Candidate)) + { + PinSourceData.FlowPin.PinName = Candidate; + InOutUsedNames.Add(Candidate); + + break; + } + + ++DisambiguationSuffix; + + if (!ensure(DisambiguationSuffix < 1000000u)) + { + UE_LOG(LogFlow, Error, TEXT("Pin name disambiguation failed for %s"), *BaseName.ToString()); + break; + } + } + + // Apply friendly name with logical (1-based) duplicate index + const FString FriendlyStr = FString::Printf(TEXT("%s (%u)"), *PinSourceData.FlowPin.PinFriendlyName.ToString(), LogicalDuplicateIndex); + PinSourceData.FlowPin.PinFriendlyName = FText::FromString(FriendlyStr); + + // Enhance tooltip with source information + FString& Tooltip = PinSourceData.FlowPin.PinToolTip; + if (!PinSourceData.PropertyOwnerObjectName.IsNone()) + { + if (!Tooltip.IsEmpty()) + { + Tooltip += TEXT("\n"); + } + + Tooltip += FString::Printf(TEXT("Output of %s"), *PinSourceData.PropertyOwnerObjectName.ToString()); + } + + // Update blueprint-facing property name + if (PinSourceData.DataPinValue) + { + PinSourceData.DataPinValue->PropertyPinName = PinSourceData.FlowPin.PinName; + } +} + #endif diff --git a/Source/Flow/Private/Types/FlowDataPinValue.cpp b/Source/Flow/Private/Types/FlowDataPinValue.cpp index b10c3e20..34de3e9f 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 5e1e7627..a31bd141 100644 --- a/Source/Flow/Private/Types/FlowDataPinValuesStandard.cpp +++ b/Source/Flow/Private/Types/FlowDataPinValuesStandard.cpp @@ -495,6 +495,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/Private/Types/FlowPinType.cpp b/Source/Flow/Private/Types/FlowPinType.cpp index c73ffd18..39bc0325 100644 --- a/Source/Flow/Private/Types/FlowPinType.cpp +++ b/Source/Flow/Private/Types/FlowPinType.cpp @@ -33,7 +33,7 @@ bool FFlowPinType::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FNa return false; } -bool FFlowPinType::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { OutResult.Result = EFlowDataPinResolveResult::FailedMismatchedType; return false; diff --git a/Source/Flow/Private/Types/FlowPinTypesStandard.cpp b/Source/Flow/Private/Types/FlowPinTypesStandard.cpp index b959fe6d..6dd857b0 100644 --- a/Source/Flow/Private/Types/FlowPinTypesStandard.cpp +++ b/Source/Flow/Private/Types/FlowPinTypesStandard.cpp @@ -36,32 +36,32 @@ const FFlowPinTypeName FFlowPinType_InstancedStruct::PinTypeNameInstancedStruct const FFlowPinTypeName FFlowPinType_Object::PinTypeNameObject = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameObject); const FFlowPinTypeName FFlowPinType_Class::PinTypeNameClass = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameClass); -bool FFlowPinType_Bool::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Bool::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Int::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Int::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Int64::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Int64::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Float::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Float::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Double::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Double::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Enum::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Enum::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { using TFlowPinType = FFlowPinType_Enum; using TValue = TFlowPinType::ValueType; @@ -71,7 +71,7 @@ bool FFlowPinType_Enum::PopulateResult(const UObject& PropertyOwnerObject, const TInstancedStruct ValueStruct; const FProperty* FoundProperty = nullptr; - if (!Node.TryFindPropertyByPinName(PropertyOwnerObject, Pin.PinName, FoundProperty, ValueStruct)) + if (!Node.TryFindPropertyByPinName(PropertyOwnerObject, PropertyName, FoundProperty, ValueStruct)) { OutResult.Result = EFlowDataPinResolveResult::FailedUnknownPin; return false; @@ -97,59 +97,59 @@ bool FFlowPinType_Enum::PopulateResult(const UObject& PropertyOwnerObject, const return false; } -bool FFlowPinType_Name::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Name::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_String::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_String::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Text::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Text::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Vector::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Vector::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Rotator::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Rotator::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Transform::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Transform::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_GameplayTag::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_GameplayTag::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_GameplayTagContainer::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_GameplayTagContainer::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_InstancedStruct::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_InstancedStruct::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Object::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Object::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } -bool FFlowPinType_Class::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const +bool FFlowPinType_Class::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const { - return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, Pin, OutResult); + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); } #if WITH_EDITOR diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn.h b/Source/Flow/Public/AddOns/FlowNodeAddOn.h index 1fe43bd6..abe446cf 100644 --- a/Source/Flow/Public/AddOns/FlowNodeAddOn.h +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn.h @@ -38,7 +38,9 @@ class UFlowNodeAddOn : public UFlowNodeBase #endif public: - // AddOns may opt in to be eligible for a given parent + // UFlowNodeBase + + // 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. // This list will be populated with the 'other' AddOns in a multi-paste operation in the editor, diff --git a/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h b/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h new file mode 100644 index 00000000..66a3783b --- /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 ee470558..ec5d2098 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; @@ -26,7 +28,7 @@ class UFlowAssetParams; #if !UE_BUILD_SHIPPING DECLARE_DELEGATE(FFlowGraphEvent); -DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, const UFlowNode* /*Node*/, const FName& /*PinName*/); +DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, UFlowNode* /*FlowNode*/, const FName& /*PinName*/); #endif /** @@ -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; @@ -358,6 +361,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); @@ -392,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 1a8468bb..fc9cd9b0 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 49274221..201e4e85 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 new file mode 100644 index 00000000..8859dc26 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowExecutionGate.h @@ -0,0 +1,36 @@ +// 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(); + +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 0fdf3fa7..3c782560 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/Flow/Public/Nodes/FlowNode.h b/Source/Flow/Public/Nodes/FlowNode.h index 6e2d6fbb..5248a34c 100644 --- a/Source/Flow/Public/Nodes/FlowNode.h +++ b/Source/Flow/Public/Nodes/FlowNode.h @@ -16,6 +16,22 @@ #include "FlowNode.generated.h" +// Entry in MapDataPinNameToPropertySource for how to source a non-trivial pin mapping in TryGatherPropertyOwnersAndPopulateResult +USTRUCT() +struct FFlowPinPropertySource +{ + GENERATED_BODY() + + FFlowPinPropertySource() = default; + FFlowPinPropertySource(const FName& InPropertyName, int32 InPropertyOwnerIndex) + : PropertyName(InPropertyName) + , PropertyOwnerIndex(InPropertyOwnerIndex) + { } + + FName PropertyName; + int32 PropertyOwnerIndex = INDEX_NONE; +}; + /** * A Flow Node is UObject-based node designed to handle entire gameplay feature within single node. */ @@ -26,6 +42,7 @@ class FLOW_API UFlowNode : public UFlowNodeBase , public IVisualLoggerDebugSnapshotInterface { GENERATED_UCLASS_BODY() + friend class SFlowGraphNode; friend class UFlowAsset; friend class UFlowGraphNode; @@ -224,6 +241,12 @@ class FLOW_API UFlowNode : public UFlowNodeBase TArray AutoOutputDataPins; #endif // WITH_EDITORONLY_DATA + // Map for PinName to Property supplier for non-trivial data pin property lookups + // (non-trivial means a different pin name from its property source, or a non-zero property owner object index) + // see TryGatherPropertyOwnersAndPopulateResult() + UPROPERTY() + TMap MapDataPinNameToPropertySource; + #if WITH_EDITOR void SetAutoInputDataPins(const TArray& AutoInputPins); void SetAutoOutputDataPins(const TArray& AutoOutputPins); diff --git a/Source/Flow/Public/Nodes/FlowPin.h b/Source/Flow/Public/Nodes/FlowPin.h index 465a2cf1..bf7b34c1 100644 --- a/Source/Flow/Public/Nodes/FlowPin.h +++ b/Source/Flow/Public/Nodes/FlowPin.h @@ -83,6 +83,11 @@ struct FLOW_API FFlowPin { } + FFlowPin(const FFlowPin& InFlowPin) = default; + FFlowPin(FFlowPin&& InFlowPin) = default; + FFlowPin& operator =(FFlowPin&& InFlowPin) = default; + FFlowPin& operator =(const FFlowPin& InFlowPin) = default; + explicit FFlowPin(const FName& InPinName) : PinName(InPinName) { @@ -225,8 +230,6 @@ struct FLOW_API FFlowPin const TWeakObjectPtr& GetPinSubCategoryObject() const { return PinSubCategoryObject; } - FORCEINLINE_DEBUGGABLE static bool DeepArePinArraysMatching(const TArray& Left, const TArray& Right); - // FFlowPin instance signatures for "trait" functions bool IsExecPin() const; static bool IsExecPinCategory(const FName& PC); @@ -278,25 +281,6 @@ struct FLOW_API FFlowPin void TrySetStructSubCategoryObjectFromPinType(); }; -// Inline implementations -bool FFlowPin::DeepArePinArraysMatching(const TArray& Left, const TArray& Right) -{ - if (Left.Num() != Right.Num()) - { - return false; - } - - for (int32 Index = 0; Index < Left.Num(); ++Index) - { - if (!Left[Index].DeepIsEqual(Right[Index])) - { - return false; - } - } - - return true; -} - USTRUCT() struct FLOW_API FFlowPinHandle { @@ -388,7 +372,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/FlowAutoDataPinsWorkingData.h b/Source/Flow/Public/Types/FlowAutoDataPinsWorkingData.h index 1a5f41bd..8b99a93a 100644 --- a/Source/Flow/Public/Types/FlowAutoDataPinsWorkingData.h +++ b/Source/Flow/Public/Types/FlowAutoDataPinsWorkingData.h @@ -4,25 +4,73 @@ #include "Nodes/FlowPin.h" -// Working Data struct for the UFlowDataPinGeneratorNodeInterface::AutoGenerateDataPins function +#if WITH_EDITOR + +class UFlowNode; +class UObject; +struct FFlowDataPinValue; + +// Container for pin data collected during automatic pin generation +struct FFlowPinSourceData +{ + FFlowPinSourceData(const FFlowPin& InFlowPin, const FName& InPropertyOwnerObjectName, int32 InPropertyOwnerIndex = 0, const FFlowDataPinValue* InDataPinValue = nullptr) + : FlowPin(InFlowPin) + , PropertyOwnerObjectName(InPropertyOwnerObjectName) + , PropertyOwnerIndex(InPropertyOwnerIndex) + , DataPinValue(InDataPinValue) + { + } + + FFlowPin FlowPin; + FName PropertyOwnerObjectName; + int32 PropertyOwnerIndex = INDEX_NONE; + const FFlowDataPinValue* DataPinValue = nullptr; +}; + +// Transient working data used during auto-generation of data pins struct FFlowAutoDataPinsWorkingData { +public: FFlowAutoDataPinsWorkingData(const TArray& InputPinsPrev, const TArray& OutputPinsPrev) : AutoInputDataPinsPrev(InputPinsPrev) , AutoOutputDataPinsPrev(OutputPinsPrev) - { } + { + } -#if WITH_EDITOR - FLOW_API void AddFlowDataPinsForClassProperties(const UObject& ObjectContainer); - FLOW_API void AddFlowDataPinForProperty(const FProperty* Property, const UObject& ObjectContainer); + FLOW_API bool AutoGenerateDataPinsForFlowNode(UFlowNode& FlowNode, bool& bAutoInputDataPinsChanged, bool& bAutoOutputDataPinsChanged); - FLOW_API bool DidAutoInputDataPinsChange() const; - FLOW_API bool DidAutoOutputDataPinsChange() const; -#endif + FLOW_API void AddFlowDataPinsForClassProperties(const UObject& ObjectContainer, int32 PropertyOwnerIndex); + FLOW_API static void BuildNextFlowPinArray(const TArray& PinSourceDatas, TArray& OutFlowPins); + +protected: + void DisambiguateAndRebuildDataPinPropertySourceMap(UFlowNode& FlowNode); + void AddInputDataPinsToMap(UFlowNode& FlowNode); + void AddOutputDataPinsToMap(UFlowNode& FlowNode); + void AddFlowDataPinForProperty(const FProperty* Property, const UObject& ObjectContainer, int32 PropertyOwnerIndex); + + void AddPinMappingToNode( + UFlowNode& FlowNode, + const FName& FinalPinName, + const FName& OriginalPinName, + int32 PropertyOwnerIndex); + + bool DidAutoInputDataPinsChange() const; + bool DidAutoOutputDataPinsChange() const; + + static void DisambiguateDuplicateOutputDataPin( + FFlowPinSourceData& PinSourceData, + TSet& InOutUsedNames, + uint32 LogicalDuplicateIndex); + + static bool CheckIfProposedPinsMatchPreviousPins(const TArray& PrevPins, const TArray& ProposedPins); + +public: const TArray& AutoInputDataPinsPrev; const TArray& AutoOutputDataPinsPrev; - - TArray AutoInputDataPinsNext; - TArray AutoOutputDataPinsNext; + + TArray AutoInputDataPinsNext; + TArray AutoOutputDataPinsNext; }; + +#endif \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowDataPinValuesStandard.h b/Source/Flow/Public/Types/FlowDataPinValuesStandard.h index 245921e5..ff731e65 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; }; //====================================================================== @@ -235,9 +235,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 @@ -317,8 +317,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; }; //====================================================================== @@ -340,8 +340,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; }; //====================================================================== @@ -363,8 +363,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; }; //====================================================================== @@ -386,8 +386,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; }; //====================================================================== @@ -411,8 +411,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; }; //====================================================================== @@ -434,7 +434,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; }; //====================================================================== @@ -464,8 +465,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; }; //====================================================================== @@ -494,6 +495,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/FlowPinType.h b/Source/Flow/Public/Types/FlowPinType.h index a9201d41..03f21770 100644 --- a/Source/Flow/Public/Types/FlowPinType.h +++ b/Source/Flow/Public/Types/FlowPinType.h @@ -39,7 +39,7 @@ struct FFlowPinType // Value resolution FLOW_API virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const; - FLOW_API virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const; + FLOW_API virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const; #if WITH_EDITOR // Editor visualization diff --git a/Source/Flow/Public/Types/FlowPinTypeNodeTemplates.h b/Source/Flow/Public/Types/FlowPinTypeNodeTemplates.h index a59cc3c2..0c32158a 100644 --- a/Source/Flow/Public/Types/FlowPinTypeNodeTemplates.h +++ b/Source/Flow/Public/Types/FlowPinTypeNodeTemplates.h @@ -12,7 +12,7 @@ namespace FlowPinType { template - static bool PopulateResultTemplate(const UObject& PropertyOwnerObject, const UFlowNode& FlowNode, const FFlowPin& Pin, FFlowDataPinResult& OutResult) + static bool PopulateResultTemplate(const UObject& PropertyOwnerObject, const UFlowNode& FlowNode, const FName& PropertyName, FFlowDataPinResult& OutResult) { using TValue = typename TPinType::ValueType; using TWrapper = typename TPinType::WrapperType; @@ -21,7 +21,7 @@ namespace FlowPinType TInstancedStruct ValueStruct; const FProperty* FoundProperty = nullptr; - if (!FlowNode.TryFindPropertyByPinName(PropertyOwnerObject, Pin.PinName, FoundProperty, ValueStruct)) + if (!FlowNode.TryFindPropertyByPinName(PropertyOwnerObject, PropertyName, FoundProperty, ValueStruct)) { OutResult.Result = EFlowDataPinResolveResult::FailedUnknownPin; return false; diff --git a/Source/Flow/Public/Types/FlowPinTypesStandard.h b/Source/Flow/Public/Types/FlowPinTypesStandard.h index e8bca110..077371f4 100644 --- a/Source/Flow/Public/Types/FlowPinTypesStandard.h +++ b/Source/Flow/Public/Types/FlowPinTypesStandard.h @@ -101,7 +101,7 @@ struct FLOW_API FFlowPinType_Bool : public FFlowPinType virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Int @@ -126,7 +126,7 @@ struct FLOW_API FFlowPinType_Int : public FFlowPinType virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Int64 @@ -151,7 +151,7 @@ struct FLOW_API FFlowPinType_Int64 : public FFlowPinType virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Float @@ -176,7 +176,7 @@ struct FLOW_API FFlowPinType_Float : public FFlowPinType virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Double @@ -201,7 +201,7 @@ struct FLOW_API FFlowPinType_Double : public FFlowPinType virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Name @@ -226,7 +226,7 @@ struct FLOW_API FFlowPinType_Name : public FFlowPinType virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // String @@ -251,7 +251,7 @@ struct FLOW_API FFlowPinType_String : public FFlowPinType virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Text @@ -276,7 +276,7 @@ struct FLOW_API FFlowPinType_Text : public FFlowPinType virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Enum @@ -303,7 +303,7 @@ struct FLOW_API FFlowPinType_Enum : public FFlowPinType virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Vector @@ -329,7 +329,7 @@ struct FLOW_API FFlowPinType_Vector : public FFlowPinType virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Rotator @@ -355,7 +355,7 @@ struct FLOW_API FFlowPinType_Rotator : public FFlowPinType virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Transform @@ -381,7 +381,7 @@ struct FLOW_API FFlowPinType_Transform : public FFlowPinType virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // GameplayTag @@ -407,7 +407,7 @@ struct FLOW_API FFlowPinType_GameplayTag : public FFlowPinType virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // GameplayTagContainer @@ -434,7 +434,7 @@ struct FLOW_API FFlowPinType_GameplayTagContainer : public FFlowPinType virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // InstancedStruct @@ -460,7 +460,7 @@ struct FLOW_API FFlowPinType_InstancedStruct : public FFlowPinType virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Object @@ -489,7 +489,7 @@ struct FLOW_API FFlowPinType_Object : public FFlowPinType static UClass* TryGetMetaClassFromProperty(const FProperty& MetaDataProperty); #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; // Class @@ -515,5 +515,5 @@ struct FLOW_API FFlowPinType_Class : public FFlowPinType virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; #endif - virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FFlowPin& Pin, FFlowDataPinResult& OutResult) const override; + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; }; \ No newline at end of file diff --git a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp index 6711c5c8..4a731fe1 100644 --- a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp @@ -16,12 +16,32 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDebuggerSubsystem) UFlowDebuggerSubsystem::UFlowDebuggerSubsystem() - : bPausedAtFlowBreakpoint(false) { UFlowSubsystem::OnInstancedTemplateAdded.BindUObject(this, &ThisClass::OnInstancedTemplateAdded); UFlowSubsystem::OnInstancedTemplateRemoved.BindUObject(this, &ThisClass::OnInstancedTemplateRemoved); } +void UFlowDebuggerSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + FFlowExecutionGate::SetGate(this); + + SetFlowDebuggerState(EFlowDebuggerState::InitialRunning, nullptr); +} + +void UFlowDebuggerSubsystem::Deinitialize() +{ + if (FFlowExecutionGate::GetGate() == this) + { + FFlowExecutionGate::SetGate(nullptr); + } + + SetFlowDebuggerState(EFlowDebuggerState::Invalid, nullptr); + + Super::Deinitialize(); +} + bool UFlowDebuggerSubsystem::ShouldCreateSubsystem(UObject* Outer) const { // Only create an instance if there is no override implementation defined elsewhere @@ -37,26 +57,29 @@ 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 UFlowNode* Node, const FName& PinName) +void UFlowDebuggerSubsystem::OnPinTriggered(UFlowNode* FlowNode, const FName& PinName) { - if (bPausedAtFlowBreakpoint) + if (FindBreakpoint(FlowNode->NodeGuid, PinName)) { - return; + MarkAsHit(FlowNode, PinName); } - if (!TryMarkAsHit(Node, PinName)) - { - // Node breakpoints waits on any pin triggered, but check it only if there is no hit pin breakpoint - TryMarkAsHit(Node); - } + // Node breakpoints waits on any pin triggered + MarkAsHit(FlowNode); } void UFlowDebuggerSubsystem::AddBreakpoint(const FGuid& NodeGuid) @@ -158,15 +181,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); @@ -298,21 +329,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; } @@ -323,111 +377,94 @@ bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr FlowAsset) +void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() { - UFlowDebuggerSettings* Settings = GetMutableDefault(); - for (const TPair& Node : FlowAsset->GetNodes()) + 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; } -bool UFlowDebuggerSubsystem::TryMarkAsHit(const UFlowNode* Node) +void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode) { - if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(Node->NodeGuid)) + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(FlowNode->NodeGuid)) { if (NodeBreakpoint->IsEnabled()) { + // Ensure only one breakpoint location is "hit" at a time. + ClearLastHitBreakpoint(); + NodeBreakpoint->MarkAsHit(true); - PauseSession(Node); - return true; + + LastHitNodeGuid = FlowNode->NodeGuid; + LastHitPinName = NAME_None; + + OnDebuggerBreakpointHit.Broadcast(FlowNode); + + PauseSession(*FlowNode->GetFlowAsset()); } } - - return false; } -bool UFlowDebuggerSubsystem::TryMarkAsHit(const UFlowNode* Node, 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()) { + // Ensure only one breakpoint location is "hit" at a time. + ClearLastHitBreakpoint(); + PinBreakpoint->MarkAsHit(true); - PauseSession(Node); - return true; + + LastHitNodeGuid = FlowNode->NodeGuid; + LastHitPinName = PinName; + + OnDebuggerBreakpointHit.Broadcast(FlowNode); + + PauseSession(*FlowNode->GetFlowAsset()); } } - - return false; } -void UFlowDebuggerSubsystem::PauseSession(const UFlowNode* Node) +void UFlowDebuggerSubsystem::PauseSession(UFlowAsset& FlowAssetInstance) { - SetPause(true); + SetFlowDebuggerState(EFlowDebuggerState::Paused, &FlowAssetInstance); } -void UFlowDebuggerSubsystem::ResumeSession() +void UFlowDebuggerSubsystem::ResumeSession(UFlowAsset& FlowAssetInstance) { - SetPause(false); + SetFlowDebuggerState(EFlowDebuggerState::Resumed, &FlowAssetInstance); } -void UFlowDebuggerSubsystem::SetPause(const bool bPause) +void UFlowDebuggerSubsystem::StopSession() { - // 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)) - { - if (const UGameInstance* GameInstance = World->GetGameInstance()) - { - if (APlayerController* PlayerController = GameInstance->GetFirstLocalPlayerController()) - { - if (AGameModeBase* const GameMode = GetWorld()->GetAuthGameMode()) - { - 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(); - } - } - } - } - } - } + SetFlowDebuggerState(EFlowDebuggerState::Invalid, nullptr); } void UFlowDebuggerSubsystem::ClearHitBreakpoints() { - bPausedAtFlowBreakpoint = false; - UFlowDebuggerSettings* Settings = GetMutableDefault(); + for (TPair& NodeBreakpoint : Settings->NodeBreakpoints) { NodeBreakpoint.Value.Breakpoint.MarkAsHit(false); @@ -437,6 +474,8 @@ void UFlowDebuggerSubsystem::ClearHitBreakpoints() PinBreakpoint.Value.MarkAsHit(false); } } + + ClearLastHitBreakpoint(); } bool UFlowDebuggerSubsystem::IsBreakpointHit(const FGuid& NodeGuid) @@ -464,3 +503,77 @@ void UFlowDebuggerSubsystem::SaveSettings() UFlowDebuggerSettings* Settings = GetMutableDefault(); Settings->SaveConfig(); } + +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 705b49d2..ef454af6 100644 --- a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h @@ -5,6 +5,9 @@ #include "Subsystems/EngineSubsystem.h" #include "Debugger/FlowDebuggerTypes.h" +#include "Interfaces/FlowExecutionGate.h" +#include "Types/FlowEnumUtils.h" + #include "FlowDebuggerSubsystem.generated.h" class UEdGraphNode; @@ -12,30 +15,68 @@ 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*/); + /** - * 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 bool ShouldCreateSubsystem(UObject* Outer) const override; + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; -protected: - bool bPausedAtFlowBreakpoint; + 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 UFlowNode* Node, const FName& PinName); + virtual void OnPinTriggered(UFlowNode* FlowNode, const FName& PinName); public: + // IFlowExecutionGate + virtual bool IsFlowExecutionHalted() const override { return EFlowDebuggerState_Classifiers::IsPausedGameState(FlowDebuggerState); } + // -- + virtual void AddBreakpoint(const FGuid& NodeGuid); virtual void AddBreakpoint(const FGuid& NodeGuid, const FName& PinName); @@ -62,24 +103,51 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem 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 bool TryMarkAsHit(const UFlowNode* Node); - virtual bool TryMarkAsHit(const UFlowNode* Node, const FName& PinName); + virtual void MarkAsHit(const UFlowNode* FlowNode); + virtual void MarkAsHit(const UFlowNode* FlowNode, const FName& PinName); - virtual void PauseSession(const UFlowNode* Node); - virtual void ResumeSession(); - void SetPause(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). + * 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(); +private: + void SetFlowDebuggerState(EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); + void ManageGameModePaused(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); + +protected: + virtual void OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) {} + public: virtual bool IsBreakpointHit(const FGuid& NodeGuid); virtual bool IsBreakpointHit(const FGuid& NodeGuid, const FName& PinName); -private: + // Delegates for debugger events (broadcast when pausing, resuming, or hitting breakpoints) + FFlowAssetDebuggerEvent OnDebuggerPaused; + FFlowAssetDebuggerEvent OnDebuggerResumed; + FFlowAssetDebuggerBreakpointHitEvent OnDebuggerBreakpointHit; + FFlowAssetDebuggerEvent OnDebuggerFlowAssetTemplateRemoved; + +protected: + EFlowDebuggerState FlowDebuggerState = EFlowDebuggerState::Invalid; + + // 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 7c0d96fc..860aeabe 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" diff --git a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp index 63b68d04..eed0440a 100644 --- a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp +++ b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp @@ -3,12 +3,19 @@ #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 "FlowSubsystem.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" @@ -22,6 +29,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) @@ -35,7 +44,7 @@ void UFlowDebugEditorSubsystem::OnInstancedTemplateAdded(UFlowAsset* AssetTempla } } -void UFlowDebugEditorSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) const +void UFlowDebugEditorSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) { AssetTemplate->OnRuntimeMessageAdded().RemoveAll(this); @@ -54,19 +63,32 @@ 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. + ClearLastHitBreakpoint(); + + if (HaltedOnFlowAssetInstance.IsValid()) + { + ResumeSession(*HaltedOnFlowAssetInstance.Get()); + } } void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) { + // Ensure we don't carry over a halted state between PIE sessions. ClearHitBreakpoints(); + StopSession(); + for (const TPair, TSharedPtr>& Log : RuntimeLogs) { if (Log.Key.IsValid() && Log.Value->NumMessages(EMessageSeverity::Warning) > 0) @@ -93,35 +115,143 @@ void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) } } -void UFlowDebugEditorSubsystem::PauseSession(const UFlowNode* Node) +void UFlowDebugEditorSubsystem::PauseSession(UFlowAsset& FlowAssetInstance) { - if (GEditor->ShouldEndPlayMap()) + HaltedOnFlowAssetInstance = &FlowAssetInstance; + + Super::PauseSession(FlowAssetInstance); +} + +void UFlowDebugEditorSubsystem::ResumeSession(UFlowAsset& FlowAssetInstance) +{ + HaltedOnFlowAssetInstance = &FlowAssetInstance; + + Super::ResumeSession(FlowAssetInstance); +} + +void UFlowDebugEditorSubsystem::StopSession() +{ + // Drop any pending deferred triggers — we are stopping the session entirely + if (HaltedOnFlowAssetInstance.IsValid()) { - return; + UFlowSubsystem* FlowSubsystem = HaltedOnFlowAssetInstance->GetFlowSubsystem(); + + if (IsValid(FlowSubsystem)) + { + FlowSubsystem->ClearAllDeferredTriggerScopes(); + } } - if (GUnrealEd->SetPIEWorldsPaused(true)) + HaltedOnFlowAssetInstance.Reset(); + + Super::StopSession(); +} + +void UFlowDebugEditorSubsystem::OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) +{ + check(PrevState != NextState); + + using namespace EFlowDebuggerState_Classifiers; + + const bool bIsPausedGameStatePrev = IsPausedGameState(PrevState); + const bool bIsPausedGameStateNext = IsPausedGameState(NextState); + + // Handle Pause/Unpause of the game & pie systems + if (bIsPausedGameStatePrev != bIsPausedGameStateNext) { - bPausedAtFlowBreakpoint = true; + const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bIsPausedGameStateNext); - const UFlowAsset* HitInstance = Node->GetFlowAsset(); - if (ensure(HitInstance)) + if (bIsPausedGameStateNext && !bWasPaused) + { + GUnrealEd->PlaySessionPaused(); + } + else if (!bIsPausedGameStateNext && bWasPaused) { - UFlowAsset* AssetTemplate = HitInstance->GetTemplateAsset(); - AssetTemplate->SetInspectedInstance(HitInstance); + GUnrealEd->PlaySessionResumed(); + } + } - UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); - if (AssetEditorSubsystem->OpenEditorForAsset(AssetTemplate)) - { - if (const TSharedPtr FlowAssetEditor = FFlowGraphUtils::GetFlowAssetEditor(AssetTemplate)) - { - FlowAssetEditor->JumpToNode(Node->GetGraphNode()); - } - } + // 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; + } +} + +void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowNode* FlowNode) const +{ + UFlowAsset* TemplateAsset = const_cast(FlowNode->GetFlowAsset()->GetTemplateAsset()); + if (!IsValid(TemplateAsset)) + { + return; + } + + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor ? GEditor->GetEditorSubsystem() : nullptr; + if (!AssetEditorSubsystem) + { + return; + } + + if (!AssetEditorSubsystem->OpenEditorForAsset(TemplateAsset)) + { + return; + } + + TemplateAsset->SetInspectedInstance(FlowNode->GetFlowAsset()); + + 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 == FlowNode->NodeGuid) + { + NodeToFocus = FlowGraphNode; + break; } + } + + if (!NodeToFocus) + { + return; + } + + const TSharedPtr GraphEditor = FFlowGraphUtils::GetFlowGraphEditor(FlowGraph); + if (GraphEditor.IsValid()) + { + constexpr bool bRequestRename = false; + constexpr bool bSelectNode = true; - GUnrealEd->PlaySessionPaused(); + GraphEditor->JumpToNode(NodeToFocus, bRequestRename, bSelectNode); } } -#undef LOCTEXT_NAMESPACE +#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 4d14f5b5..0e9a97c8 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" @@ -54,6 +57,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(); @@ -1148,11 +1214,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); } } @@ -1178,11 +1247,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; @@ -1205,11 +1277,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); } } @@ -1233,16 +1308,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 @@ -1262,11 +1328,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); } } @@ -1289,17 +1358,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 @@ -1319,11 +1379,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); } } @@ -1348,17 +1411,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 @@ -1378,11 +1431,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); } } @@ -1403,11 +1459,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 9864a54e..6626c817 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 { @@ -1991,4 +2047,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 881f6c26..630cec7d 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 b05d1551..6833c057 100644 --- a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h +++ b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h @@ -24,8 +24,10 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem protected: TMap, TSharedPtr> RuntimeLogs; + TWeakObjectPtr HaltedOnFlowAssetInstance; + 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 +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* Node) 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; }; diff --git a/Source/FlowEditor/Public/FlowEditorModule.h b/Source/FlowEditor/Public/FlowEditorModule.h index 9e32e660..b75187d9 100644 --- a/Source/FlowEditor/Public/FlowEditorModule.h +++ b/Source/FlowEditor/Public/FlowEditorModule.h @@ -30,6 +30,7 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface, public IHasMen TArray> RegisteredAssetActions; TSet CustomClassLayouts; TSet CustomStructLayouts; + bool bIsRegisteredForAssetChanges = false; public: diff --git a/Source/FlowEditor/Public/Graph/FlowGraphEditor.h b/Source/FlowEditor/Public/Graph/FlowGraphEditor.h index 15c6ee9b..8c292ece 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; /** * @@ -109,6 +111,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;