diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index a6d32fb4..63014aea 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" @@ -1012,6 +1013,11 @@ void UFlowAsset::PreStartFlow() void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier) { + if (FFlowExecutionGate::IsHalted()) + { + return; + } + PreStartFlow(); if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode()) @@ -1075,6 +1081,11 @@ TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGr void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const { + if (FFlowExecutionGate::IsHalted()) + { + return; + } + // NOTE (gtaylor) Custom Input nodes cannot currently add data pins (like Start or DefineProperties nodes can) // but we may want to allow them to source parameters, so I am providing the subgraph node as the // IFlowDataPinValueSupplierInterface when triggering the node (even though it's not used at this time). @@ -1088,6 +1099,11 @@ void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNod void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier) { + if (FFlowExecutionGate::IsHalted()) + { + return; + } + for (UFlowNode_CustomInput* CustomInputNode : CustomInputNodes) { if (CustomInputNode->EventName == EventName) @@ -1127,6 +1143,11 @@ void UFlowAsset::TriggerCustomOutput(const FName& EventName) void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) { + if (FFlowExecutionGate::EnqueueDeferredTriggerInput(this, NodeGuid, PinName, FromPin)) + { + return; + } + if (UFlowNode* Node = Nodes.FindRef(NodeGuid)) { if (!ActiveNodes.Contains(Node)) diff --git a/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp new file mode 100644 index 00000000..44fd864f --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp @@ -0,0 +1,134 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowExecutionGate.h" + +#include "FlowAsset.h" +#include "Nodes/FlowPin.h" + +namespace FlowExecutionGate_Private +{ + struct FDeferredTriggerInput + { + TWeakObjectPtr FlowAssetInstance; + FGuid NodeGuid; + FName PinName; + FConnectedPin FromPin; + }; + + static TArray DeferredTriggerInputs; + static bool bIsFlushing = false; +} + +IFlowExecutionGate* FFlowExecutionGate::Gate = nullptr; + +void FFlowExecutionGate::SetGate(IFlowExecutionGate* InGate) +{ + Gate = InGate; +} + +IFlowExecutionGate* FFlowExecutionGate::GetGate() +{ + return Gate; +} + +bool FFlowExecutionGate::IsHalted() +{ + return (Gate != nullptr) && Gate->IsFlowExecutionHalted(); +} + +bool FFlowExecutionGate::EnqueueDeferredTriggerInput(UFlowAsset* FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) +{ + using namespace FlowExecutionGate_Private; + + // If we're halted, always enqueue (even during flushing). The whole point is to stop propagation. + if (IsHalted()) + { + if (!IsValid(FlowAssetInstance)) + { + return true; // treat as handled while halted + } + + FDeferredTriggerInput& Entry = DeferredTriggerInputs.AddDefaulted_GetRef(); + Entry.FlowAssetInstance = FlowAssetInstance; + Entry.NodeGuid = NodeGuid; + Entry.PinName = PinName; + Entry.FromPin = FromPin; + + return true; + } + + // Not halted: + // During flush we must not enqueue "normal" triggers (we want them to execute now), + // otherwise we can get infinite deferral. + if (bIsFlushing) + { + return false; + } + + return false; +} + +void FFlowExecutionGate::FlushDeferredTriggerInputs() +{ + using namespace FlowExecutionGate_Private; + + if (bIsFlushing) + { + return; + } + + // Do not flush while halted; callers should clear the halt first. + if (IsHalted()) + { + return; + } + + if (DeferredTriggerInputs.IsEmpty()) + { + return; + } + + bIsFlushing = true; + + // Move into a local array so new deferred triggers can be added while we flush. + TArray Local = MoveTemp(DeferredTriggerInputs); + DeferredTriggerInputs.Reset(); + + for (int32 Index = 0; Index < Local.Num(); ++Index) + { + // If a breakpoint was hit during this flush, stop immediately and re-queue remaining work. + if (IsHalted()) + { + const int32 Remaining = Local.Num() - Index; + if (Remaining > 0) + { + TArray RemainingItems; + RemainingItems.Reserve(Remaining); + + for (int32 j = Index; j < Local.Num(); ++j) + { + RemainingItems.Add(Local[j]); + } + + // RemainingItems should run before any items that may already be queued. + if (!DeferredTriggerInputs.IsEmpty()) + { + RemainingItems.Append(MoveTemp(DeferredTriggerInputs)); + } + + DeferredTriggerInputs = MoveTemp(RemainingItems); + } + + bIsFlushing = false; + return; + } + + const FDeferredTriggerInput& Entry = Local[Index]; + if (UFlowAsset* Asset = Entry.FlowAssetInstance.Get()) + { + Asset->TriggerDeferredInputFromDebugger(Entry.NodeGuid, Entry.PinName, Entry.FromPin); + } + } + + bIsFlushing = false; +} \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/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/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/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index ee470558..bd16210e 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -26,7 +26,7 @@ class UFlowAssetParams; #if !UE_BUILD_SHIPPING DECLARE_DELEGATE(FFlowGraphEvent); -DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, const UFlowNode* /*Node*/, const FName& /*PinName*/); +DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, UFlowNode* /*FlowNode*/, const FName& /*PinName*/); #endif /** @@ -358,6 +358,10 @@ class FLOW_API UFlowAsset : public UObject // Get Flow Asset instance created by the given SubGraph node TWeakObjectPtr GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const; + // Public trigger input signature for the FFlowExecutionGate mechanism in the Flow Debugger + FORCEINLINE void TriggerDeferredInputFromDebugger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) + { TriggerInput(NodeGuid, PinName, FromPin); } + protected: void TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* Node, const FName& EventName) const; void TriggerCustomOutput(const FName& EventName); diff --git a/Source/Flow/Public/Interfaces/FlowExecutionGate.h b/Source/Flow/Public/Interfaces/FlowExecutionGate.h new file mode 100644 index 00000000..8e1abdf6 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowExecutionGate.h @@ -0,0 +1,45 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "CoreMinimal.h" + +class UFlowAsset; + +/** + * Implemented by a debugger/runtime system (in another module) that can halt Flow execution. + * Flow runtime queries this through FFlowExecutionGate without depending on the debugger module. + */ +class FLOW_API IFlowExecutionGate +{ +public: + virtual ~IFlowExecutionGate() = default; + + /** Return true when Flow execution should be halted globally. */ + virtual bool IsFlowExecutionHalted() const = 0; +}; + +/** + * Global registry + minimal deferred-execution queue for Flow runtime. + */ +class FLOW_API FFlowExecutionGate +{ +public: + static void SetGate(IFlowExecutionGate* InGate); + static IFlowExecutionGate* GetGate(); + + /** True if a gate exists and it currently wants Flow execution halted. */ + static bool IsHalted(); + + /** If halted, queues the trigger for later. Returns true if queued (caller should early-out). */ + static bool EnqueueDeferredTriggerInput(UFlowAsset* FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName, const struct FConnectedPin& FromPin); + + /** + * Flushes queued trigger inputs (FIFO). + * Safe to call even if nothing is queued. + */ + static void FlushDeferredTriggerInputs(); + +private: + static IFlowExecutionGate* Gate; +}; \ No newline at end of file diff --git a/Source/Flow/Public/Nodes/FlowPin.h b/Source/Flow/Public/Nodes/FlowPin.h index 465a2cf1..9aa88d48 100644 --- a/Source/Flow/Public/Nodes/FlowPin.h +++ b/Source/Flow/Public/Nodes/FlowPin.h @@ -388,7 +388,6 @@ struct FLOW_API FPinRecord FString HumanReadableTime; EFlowPinActivationType ActivationType; - static FString NoActivations; static FString PinActivations; static FString ForcedActivation; static FString PassThroughActivation; diff --git a/Source/Flow/Public/Types/FlowDataPinValuesStandard.h b/Source/Flow/Public/Types/FlowDataPinValuesStandard.h index 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/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp index 6711c5c8..e014fe8e 100644 --- a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp @@ -16,12 +16,28 @@ #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); +} + +void UFlowDebuggerSubsystem::Deinitialize() +{ + if (FFlowExecutionGate::GetGate() == this) + { + FFlowExecutionGate::SetGate(nullptr); + } + + Super::Deinitialize(); +} + bool UFlowDebuggerSubsystem::ShouldCreateSubsystem(UObject* Outer) const { // Only create an instance if there is no override implementation defined elsewhere @@ -37,26 +53,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 +177,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 +325,44 @@ bool UFlowDebuggerSubsystem::IsBreakpointEnabled(const FGuid& NodeGuid, const FN return false; } -bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr FlowAsset) +bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr& FlowAsset) { - UFlowDebuggerSettings* Settings = GetMutableDefault(); - for (const TPair& Node : FlowAsset->GetNodes()) + return HasAnyBreakpointsMatching(FlowAsset, true); +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpointsDisabled(const TWeakObjectPtr& FlowAsset) +{ + return HasAnyBreakpointsMatching(FlowAsset, false); +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpointsMatching(const TWeakObjectPtr& FlowAsset, bool bDesiresEnabled) +{ + if (!FlowAsset.IsValid()) { - if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(Node.Key)) + return false; + } + + const UFlowDebuggerSettings* Settings = GetDefault(); + if (!Settings) + { + return false; + } + + for (const TPair& NodePair : FlowAsset->GetNodes()) + { + if (const FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(NodePair.Key)) { - if (NodeBreakpoint->Breakpoint.IsActive() && NodeBreakpoint->Breakpoint.IsEnabled()) + // Node-level breakpoint must be active to count (matches original behavior) + if (NodeBreakpoint->Breakpoint.IsActive() && + (NodeBreakpoint->Breakpoint.IsEnabled() == bDesiresEnabled)) { return true; } - for (auto& [Name, PinBreakpoint] : NodeBreakpoint->PinBreakpoints) + // Pin-level breakpoints + for (const auto& PinPair : NodeBreakpoint->PinBreakpoints) { - if (PinBreakpoint.IsEnabled()) + if (PinPair.Value.IsEnabled() == bDesiresEnabled) { return true; } @@ -323,111 +373,178 @@ bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr FlowAsset) +void UFlowDebuggerSubsystem::RequestHaltFlowExecution(const UFlowNode* Node) { - UFlowDebuggerSettings* Settings = GetMutableDefault(); - for (const TPair& Node : FlowAsset->GetNodes()) + bHaltFlowExecution = true; + HaltedOnFlowAssetInstance = Node->GetFlowAsset(); + HaltedOnNodeGuid = Node->NodeGuid; +} + +void UFlowDebuggerSubsystem::ClearHaltFlowExecution() +{ + bHaltFlowExecution = false; + HaltedOnFlowAssetInstance.Reset(); + HaltedOnNodeGuid.Invalidate(); +} + +void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() +{ + if (!LastHitNodeGuid.IsValid()) { - if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(Node.Key)) - { - if (NodeBreakpoint->Breakpoint.IsActive() && !NodeBreakpoint->Breakpoint.IsEnabled()) - { - return true; - } + return; + } - for (auto& [Name, PinBreakpoint] : NodeBreakpoint->PinBreakpoints) - { - if (!PinBreakpoint.IsEnabled()) - { - return true; - } - } + // Pin breakpoint "hit" state lives in the PinBreakpoints map, node breakpoint "hit" lives on NodeBreakpoint.Breakpoint. + if (!LastHitPinName.IsNone()) + { + if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(LastHitNodeGuid, LastHitPinName)) + { + PinBreakpoint->MarkAsHit(false); + } + } + else + { + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(LastHitNodeGuid)) + { + NodeBreakpoint->MarkAsHit(false); } } - return false; + LastHitNodeGuid.Invalidate(); + LastHitPinName = NAME_None; } -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; + + RequestHaltFlowExecution(FlowNode); + + OnDebuggerBreakpointHit.Broadcast(FlowNode); + + PauseSession(*FlowNode); } } - - 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; + + RequestHaltFlowExecution(FlowNode); + + OnDebuggerBreakpointHit.Broadcast(FlowNode); + + PauseSession(*FlowNode); } } - - return false; } -void UFlowDebuggerSubsystem::PauseSession(const UFlowNode* Node) +void UFlowDebuggerSubsystem::PauseSession(const UFlowNode& FlowNode) { - SetPause(true); + SetPause(FlowNode, true); } -void UFlowDebuggerSubsystem::ResumeSession() +void UFlowDebuggerSubsystem::ResumeSession(const UFlowNode& FlowNode) { - SetPause(false); + SetPause(FlowNode, false); } -void UFlowDebuggerSubsystem::SetPause(const bool bPause) +void UFlowDebuggerSubsystem::SetPause(const UFlowNode& FlowNode, const bool bPause) { - // experimental implementation, untested, shows intent for future development - // here be dragons: same as APlayerController::SetPause, but we allow debugger to pause on clients - if (const UWorld* World = GEngine->GetWorldFromContextObject(this, EGetWorldErrorMode::LogAndReturnNull)) + // Default bWasPaused to opposite of bPause + // (which we hope to get a better measure if we can get access to what we need) + bool bWasPaused = !bPause; + + AGameModeBase* GameMode = nullptr; + APlayerController* PlayerController = nullptr; + + const UFlowAsset* FlowAssetInstance = FlowNode.GetFlowAsset(); + const UWorld* World = FlowAssetInstance->GetWorld(); + if (IsValid(World)) + { + GameMode = World->GetAuthGameMode(); + + if (IsValid(GameMode)) + { + bWasPaused = GameMode->IsPaused(); + } + + const UGameInstance* GameInstance = World->GetGameInstance(); + if (IsValid(GameInstance)) + { + PlayerController = GameInstance->GetFirstLocalPlayerController(); + } + } + + if (bWasPaused != bPause) { - if (const UGameInstance* GameInstance = World->GetGameInstance()) + if (bPause) { - if (APlayerController* PlayerController = GameInstance->GetFirstLocalPlayerController()) + // Pausing (from an unpaused state) + + if (IsValid(PlayerController)) { - if (AGameModeBase* const GameMode = GetWorld()->GetAuthGameMode()) + if (IsValid(GameMode)) + { + GameMode->SetPause(PlayerController); + } + + if (AWorldSettings* WorldSettings = PlayerController->GetWorldSettings()) { - const bool bCurrentPauseState = PlayerController->IsPaused(); - if (bPause && !bCurrentPauseState) - { - GameMode->SetPause(PlayerController); - - if (AWorldSettings* WorldSettings = PlayerController->GetWorldSettings()) - { - WorldSettings->ForceNetUpdate(); - } - } - else if (!bPause && bCurrentPauseState) - { - if (GameMode->ClearPause()) - { - ClearHitBreakpoints(); - } - } + WorldSettings->ForceNetUpdate(); } } + + // Broadcast the Pause event + OnDebuggerPaused.Broadcast(*FlowAssetInstance); + } + else + { + // Resuming (from a paused state) + + ClearHaltFlowExecution(); + + // Replay any Flow propagation that was deferred while execution was halted. + FFlowExecutionGate::FlushDeferredTriggerInputs(); + + // Intentionally do NOT clear hit flags here. The editor-specific resume path will clear the last-hit + // breakpoint safely (without racing against immediate breakpoint hits during flush). + if (IsValid(GameMode)) + { + (void) GameMode->ClearPause(); + } + + // Broadcast the Resume event + OnDebuggerResumed.Broadcast(*FlowAssetInstance); } } } void UFlowDebuggerSubsystem::ClearHitBreakpoints() { - bPausedAtFlowBreakpoint = false; - UFlowDebuggerSettings* Settings = GetMutableDefault(); + for (TPair& NodeBreakpoint : Settings->NodeBreakpoints) { NodeBreakpoint.Value.Breakpoint.MarkAsHit(false); @@ -437,6 +554,9 @@ void UFlowDebuggerSubsystem::ClearHitBreakpoints() PinBreakpoint.Value.MarkAsHit(false); } } + + LastHitNodeGuid.Invalidate(); + LastHitPinName = NAME_None; } bool UFlowDebuggerSubsystem::IsBreakpointHit(const FGuid& NodeGuid) @@ -463,4 +583,4 @@ void UFlowDebuggerSubsystem::SaveSettings() { UFlowDebuggerSettings* Settings = GetMutableDefault(); Settings->SaveConfig(); -} +} \ No newline at end of file diff --git a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h index 705b49d2..bc4f4221 100644 --- a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h @@ -5,6 +5,8 @@ #include "Subsystems/EngineSubsystem.h" #include "Debugger/FlowDebuggerTypes.h" +#include "Interfaces/FlowExecutionGate.h" + #include "FlowDebuggerSubsystem.generated.h" class UEdGraphNode; @@ -12,30 +14,37 @@ class UEdGraphNode; class UFlowAsset; class UFlowNode; +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 bHaltFlowExecution; } + // -- + virtual void AddBreakpoint(const FGuid& NodeGuid); virtual void AddBreakpoint(const FGuid& NodeGuid, const FName& PinName); @@ -62,24 +71,50 @@ 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(const UFlowNode& FlowNode); + virtual void ResumeSession(const UFlowNode& FlowNode); + void SetPause(const UFlowNode& FlowNode, const bool bPause); + /** + * Clears the "currently hit" breakpoint only (node or pin). + * This avoids races where blanket-clearing all hit flags can erase a newly-hit breakpoint during resume/flush. + */ + void ClearLastHitBreakpoint(); + + /** Clears hit state for all breakpoints. Prefer ClearLastHitBreakpoint() for resume/step logic. */ virtual void ClearHitBreakpoints(); +protected: + void RequestHaltFlowExecution(const UFlowNode* Node); + void ClearHaltFlowExecution(); + public: virtual bool IsBreakpointHit(const FGuid& NodeGuid); virtual bool IsBreakpointHit(const FGuid& NodeGuid, const FName& PinName); + // Delegates for debugger events (broadcast when pausing, resuming, or hitting breakpoints) + FFlowAssetDebuggerEvent OnDebuggerPaused; + FFlowAssetDebuggerEvent OnDebuggerResumed; + FFlowAssetDebuggerBreakpointHitEvent OnDebuggerBreakpointHit; + FFlowAssetDebuggerEvent OnDebuggerFlowAssetTemplateRemoved; + private: + bool bHaltFlowExecution = false; + TWeakObjectPtr HaltedOnFlowAssetInstance; + FGuid HaltedOnNodeGuid; + + // Track the single breakpoint location that is currently "hit" (node or pin). + FGuid LastHitNodeGuid; + FName LastHitPinName; + /** Saves any modifications made to breakpoints */ virtual void SaveSettings(); -}; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp index 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..90cac202 100644 --- a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp +++ b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp @@ -3,12 +3,18 @@ #include "Asset/FlowDebugEditorSubsystem.h" #include "Asset/FlowAssetEditor.h" #include "Asset/FlowMessageLogListing.h" +#include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditor.h" #include "Graph/FlowGraphUtils.h" +#include "Graph/Nodes/FlowGraphNode.h" +#include "Interfaces/FlowExecutionGate.h" +#include "FlowAsset.h" #include "Editor/UnrealEdEngine.h" #include "Engine/Engine.h" #include "Engine/World.h" #include "Framework/Notifications/NotificationManager.h" +#include "Subsystems/AssetEditorSubsystem.h" #include "Templates/Function.h" #include "UnrealEdGlobals.h" #include "Widgets/Notifications/SNotificationList.h" @@ -22,6 +28,8 @@ UFlowDebugEditorSubsystem::UFlowDebugEditorSubsystem() FEditorDelegates::BeginPIE.AddUObject(this, &ThisClass::OnBeginPIE); FEditorDelegates::ResumePIE.AddUObject(this, &ThisClass::OnResumePIE); FEditorDelegates::EndPIE.AddUObject(this, &ThisClass::OnEndPIE); + + OnDebuggerBreakpointHit.AddUObject(this, &ThisClass::OnBreakpointHit); } void UFlowDebugEditorSubsystem::OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) @@ -35,7 +43,7 @@ void UFlowDebugEditorSubsystem::OnInstancedTemplateAdded(UFlowAsset* AssetTempla } } -void UFlowDebugEditorSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) const +void UFlowDebugEditorSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) { AssetTemplate->OnRuntimeMessageAdded().RemoveAll(this); @@ -54,19 +62,34 @@ void UFlowDebugEditorSubsystem::OnRuntimeMessageAdded(const UFlowAsset* AssetTem void UFlowDebugEditorSubsystem::OnBeginPIE(const bool bIsSimulating) { - // clear all logs from a previous session + // Clear all logs from a previous session RuntimeLogs.Empty(); + + // Clear any stale "hit" state from previous run + ClearHitBreakpoints(); } void UFlowDebugEditorSubsystem::OnResumePIE(const bool bIsSimulating) { - ClearHitBreakpoints(); + // Editor-level resume event (also used by Advance Single Frame). + // This does not necessarily flow through AGameModeBase::ClearPause(), so we must unhalt Flow here. + // + // Clear only the last-hit breakpoint to return to enabled/disabled visuals without racing against + // a newly hit breakpoint during FlushDeferredTriggerInputs(). + ClearHaltFlowExecution(); + ClearLastHitBreakpoint(); + + FFlowExecutionGate::FlushDeferredTriggerInputs(); } void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) { + // Ensure we don't carry over a halted state between PIE sessions. ClearHitBreakpoints(); + ClearHaltFlowExecution(); + FFlowExecutionGate::FlushDeferredTriggerInputs(); + for (const TPair, TSharedPtr>& Log : RuntimeLogs) { if (Log.Key.IsValid() && Log.Value->NumMessages(EMessageSeverity::Warning) > 0) @@ -93,35 +116,84 @@ void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) } } -void UFlowDebugEditorSubsystem::PauseSession(const UFlowNode* Node) +void UFlowDebugEditorSubsystem::PauseSession(const UFlowNode& FlowNode) +{ + Super::PauseSession(FlowNode); + + constexpr bool bShouldBePaused = true; + const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); + if (!bWasPaused) + { + GUnrealEd->PlaySessionPaused(); + } +} + +void UFlowDebugEditorSubsystem::ResumeSession(const UFlowNode& FlowNode) { - if (GEditor->ShouldEndPlayMap()) + Super::ResumeSession(FlowNode); + + constexpr bool bShouldBePaused = false; + const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); + if (bWasPaused) + { + GUnrealEd->PlaySessionResumed(); + } +} + +void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowNode* FlowNode) const +{ + UFlowAsset* TemplateAsset = const_cast(FlowNode->GetFlowAsset()->GetTemplateAsset()); + if (!IsValid(TemplateAsset)) { return; } - if (GUnrealEd->SetPIEWorldsPaused(true)) + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor ? GEditor->GetEditorSubsystem() : nullptr; + if (!AssetEditorSubsystem) { - bPausedAtFlowBreakpoint = true; + return; + } - const UFlowAsset* HitInstance = Node->GetFlowAsset(); - if (ensure(HitInstance)) - { - UFlowAsset* AssetTemplate = HitInstance->GetTemplateAsset(); - AssetTemplate->SetInspectedInstance(HitInstance); + if (!AssetEditorSubsystem->OpenEditorForAsset(TemplateAsset)) + { + return; + } - UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); - if (AssetEditorSubsystem->OpenEditorForAsset(AssetTemplate)) - { - if (const TSharedPtr FlowAssetEditor = FFlowGraphUtils::GetFlowAssetEditor(AssetTemplate)) - { - FlowAssetEditor->JumpToNode(Node->GetGraphNode()); - } - } + 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; } + } - GUnrealEd->PlaySessionPaused(); + if (!NodeToFocus) + { + return; + } + + const TSharedPtr GraphEditor = FFlowGraphUtils::GetFlowGraphEditor(FlowGraph); + if (GraphEditor.IsValid()) + { + constexpr bool bRequestRename = false; + constexpr bool bSelectNode = true; + + GraphEditor->JumpToNode(NodeToFocus, bRequestRename, bSelectNode); } } -#undef LOCTEXT_NAMESPACE +#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..4b2f839b 100644 --- a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h +++ b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h @@ -25,7 +25,7 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem TMap, TSharedPtr> RuntimeLogs; virtual void OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) override; - virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) const override; + virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) override; void OnRuntimeMessageAdded(const UFlowAsset* AssetTemplate, const TSharedRef& Message) const; @@ -33,5 +33,8 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem virtual void OnResumePIE(const bool bIsSimulating); virtual void OnEndPIE(const bool bIsSimulating); - virtual void PauseSession(const UFlowNode* Node) override; + virtual void PauseSession(const UFlowNode& FlowNode) override; + virtual void ResumeSession(const UFlowNode& FlowNode) override; + + 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;