diff --git a/Source/Flow/Private/Asset/FlowAssetParams.cpp b/Source/Flow/Private/Asset/FlowAssetParams.cpp index 62ae577f7..e9983aef8 100644 --- a/Source/Flow/Private/Asset/FlowAssetParams.cpp +++ b/Source/Flow/Private/Asset/FlowAssetParams.cpp @@ -367,12 +367,12 @@ void UFlowAssetParams::RebuildPropertiesMap() } #endif -bool UFlowAssetParams::CanSupplyDataPinValues_Implementation() const +bool UFlowAssetParams::CanSupplyDataPinValues() const { return !PropertyMap.IsEmpty(); } -FFlowDataPinResult UFlowAssetParams::TrySupplyDataPin_Implementation(FName PinName) const +FFlowDataPinResult UFlowAssetParams::TrySupplyDataPin(FName PinName) const { if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) { diff --git a/Source/Flow/Private/Asset/FlowAssetParamsUtils.cpp b/Source/Flow/Private/Asset/FlowAssetParamsUtils.cpp index 6cf293d5e..9a7142912 100644 --- a/Source/Flow/Private/Asset/FlowAssetParamsUtils.cpp +++ b/Source/Flow/Private/Asset/FlowAssetParamsUtils.cpp @@ -6,6 +6,20 @@ #include "Misc/DateTime.h" #include "HAL/FileManager.h" +#if WITH_EDITOR +#include "Asset/FlowAssetParams.h" +#include "FlowLogChannels.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "ContentBrowserModule.h" +#include "FileHelpers.h" +#include "IContentBrowserSingleton.h" +#include "Misc/MessageDialog.h" +#include "Modules/ModuleManager.h" +#include "SourceControlHelpers.h" +#endif + #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowAssetParamsUtils) #if WITH_EDITOR @@ -116,4 +130,130 @@ bool FFlowAssetParamsUtils::ArePropertiesEqual( return A.DataPinValue == B.DataPinValue; } -#endif +UFlowAssetParams* FFlowAssetParamsUtils::CreateChildParamsAsset(UFlowAssetParams& ParentParams, const bool bShowDialogs, FText* OutOptionalFailureReason) +{ + if (!IsValid(&ParentParams)) + { + FailCreateChild( + NSLOCTEXT("FlowAssetParamsUtils", "InvalidParent", "Invalid Parent Flow Asset Params."), + bShowDialogs, + OutOptionalFailureReason); + + return nullptr; + } + + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + + const FString PackagePath = FPackageName::GetLongPackagePath(ParentParams.GetPackage()->GetPathName()); + const FString BaseAssetName = ParentParams.GetName(); + + // Generate a unique name for the new asset params + FString UniquePackageName; + FString UniqueAssetName; + AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + BaseAssetName, TEXT(""), UniquePackageName, UniqueAssetName); + + if (UniqueAssetName.IsEmpty()) + { + FailCreateChild( + FText::Format( + NSLOCTEXT("FlowAssetParamsUtils", "UniqueNameFail", "Failed to generate unique asset name for child params of {0}."), + FText::FromString(BaseAssetName)), + bShowDialogs, + OutOptionalFailureReason); + + return nullptr; + } + + // Create the new asset params + UFlowAssetParams* NewParams = Cast( + AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, ParentParams.GetClass(), nullptr)); + + if (!IsValid(NewParams)) + { + FailCreateChild( + FText::Format( + NSLOCTEXT("FlowAssetParamsUtils", "CreateAssetFail", "Failed to create child Flow Asset Params: {0}."), + FText::FromString(UniqueAssetName)), + bShowDialogs, + OutOptionalFailureReason); + + return nullptr; + } + + // Best-effort source control integration (before save) + if (USourceControlHelpers::IsAvailable()) + { + const FString FileName = USourceControlHelpers::PackageFilename(NewParams->GetPathName()); + if (!USourceControlHelpers::CheckOutOrAddFile(FileName)) + { + UE_LOG(LogFlow, Warning, TEXT("Failed to check out/add %s; saved in-memory only"), *NewParams->GetPathName()); + } + } + + // Configure from parent (copies OwnerFlowAsset + Properties, sets ParentParams, rebuilds PropertyMap, marks dirty) + NewParams->ConfigureFlowAssetParams(ParentParams.OwnerFlowAsset, &ParentParams, ParentParams.Properties); + + // Reconcile (cycle detection, flattened inheritance, etc.) + const EFlowReconcilePropertiesResult ReconcileResult = NewParams->ReconcilePropertiesWithParentParams(); + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + FailCreateChild( + FText::Format( + NSLOCTEXT("FlowAssetParamsUtils", "ReconcileFail", + "Created asset but reconciliation failed.\n\nAsset: {0}\nError: {1}\n\nThe asset may be invalid and should be reviewed."), + FText::FromString(NewParams->GetPathName()), + UEnum::GetDisplayValueAsText(ReconcileResult)), + bShowDialogs, + OutOptionalFailureReason); + + // Keep going: asset exists and may still be useful for debugging/fixing + } + + // Save the package (force save even if not prompted) + { + UPackage* Package = NewParams->GetPackage(); + TArray PackagesToSave = { Package }; + + const bool bForceSave = true; + if (!UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, bForceSave)) + { + FailCreateChild( + FText::Format( + NSLOCTEXT("FlowAssetParamsUtils", "SaveFail", "Failed to save child Flow Asset Params: {0}."), + FText::FromString(NewParams->GetPathName())), + bShowDialogs, + OutOptionalFailureReason); + + // Still return the in-memory asset + } + } + + // Register + sync to Content Browser + { + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistryModule.Get().AssetCreated(NewParams); + + const FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + const TArray AssetsToSync = { NewParams }; + ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); + } + + return NewParams; +} + +void FFlowAssetParamsUtils::FailCreateChild(const FText& Reason, const bool bShowDialogs, FText* OutOptionalFailureReason) +{ + if (OutOptionalFailureReason) + { + *OutOptionalFailureReason = Reason; + } + + UE_LOG(LogFlow, Error, TEXT("%s"), *Reason.ToString()); + + if (bShowDialogs) + { + FMessageDialog::Open(EAppMsgType::Ok, Reason); + } +} + +#endif \ No newline at end of file diff --git a/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp b/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp new file mode 100644 index 000000000..c5ac2b4db --- /dev/null +++ b/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp @@ -0,0 +1,32 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowDeferredTransitionScope.h" +#include "FlowAsset.h" +#include "Interfaces/FlowExecutionGate.h" + +void FFlowDeferredTransitionScope::EnqueueDeferredTrigger(const FFlowDeferredTriggerInput& Entry) +{ + check(bIsOpen); + + DeferredTriggers.Add(Entry); +} + +bool FFlowDeferredTransitionScope::TryFlushDeferredTriggers(UFlowAsset& OwningFlowAsset) +{ + // Ensure the scope is closed before beginning flushing + CloseScope(); + + // Remove and trigger each deferred trigger input + while (!DeferredTriggers.IsEmpty() && !FFlowExecutionGate::IsHalted()) + { + const FFlowDeferredTriggerInput Entry = DeferredTriggers[0]; + DeferredTriggers.RemoveAt(0, 1, EAllowShrinking::No); + + OwningFlowAsset.TriggerInput(Entry.NodeGuid, Entry.PinName, Entry.FromPin); + } + + check(DeferredTriggers.IsEmpty() || FFlowExecutionGate::IsHalted()); + + // Return true if everything flushed without being interrupted by an ExecutionGate + return DeferredTriggers.IsEmpty(); +} \ No newline at end of file diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index 63014aea2..34f5294a8 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -646,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(); @@ -971,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)) @@ -1013,11 +1020,6 @@ void UFlowAsset::PreStartFlow() void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier) { - if (FFlowExecutionGate::IsHalted()) - { - return; - } - PreStartFlow(); if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode()) @@ -1037,6 +1039,8 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool b { FinishPolicy = InFinishPolicy; + CancelAndWarnForUnflushedDeferredTriggers(); + // end execution of this asset and all of its nodes for (UFlowNode* Node : ActiveNodes) { @@ -1058,6 +1062,62 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool b } } +void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers() +{ + // Aggressively drop any pending deferred triggers — graph is done + // In normal execution these should have been flushed via PopDeferredTransitionScope() in TriggerInputDirect + // In the debugger they should have been flushed by ResumePIE + // Remaining scopes here usually mean: + // - early/abnormal termination (e.g. FinishFlow called from unexpected place) + // - exception/early return before Pop + // - forced deinitialization during active execution (e.g. PIE stop, subsystem cleanup) + if (!DeferredTransitionScopes.IsEmpty()) + { + int32 TotalDroppedTriggers = 0; + + for (const TSharedPtr& ScopePtr : DeferredTransitionScopes) + { + if (!ScopePtr.IsValid()) + { + continue; + } + + const TArray& Triggers = ScopePtr->GetDeferredTriggers(); + + if (TotalDroppedTriggers == 0 && !Triggers.IsEmpty()) + { + UE_LOG(LogFlow, Warning, TEXT("FlowAsset '%s' is finishing with %d lingering deferred transition scope(s) — dropping them. " + "This is usually unexpected and may indicate a bug or abnormal termination."), + *GetName(), DeferredTransitionScopes.Num()); + } + + TotalDroppedTriggers += Triggers.Num(); + + for (const FFlowDeferredTriggerInput& Trigger : Triggers) + { + const UFlowNode* ToNode = GetNode(Trigger.NodeGuid); + const UFlowNode* FromNode = Trigger.FromPin.NodeGuid.IsValid() ? GetNode(Trigger.FromPin.NodeGuid) : nullptr; + + UE_LOG(LogFlow, Error, + TEXT(" → Dropped deferred trigger:\n") + TEXT(" To Node: %s (%s)\n") + TEXT(" To Pin: %s\n") + TEXT(" From Node: %s (%s)\n") + TEXT(" From Pin: %s"), + *ToNode->GetName(), + *Trigger.NodeGuid.ToString(), + *Trigger.PinName.ToString(), + *FromNode->GetName(), + *Trigger.FromPin.NodeGuid.ToString(), + *Trigger.FromPin.PinName.ToString() + ); + } + } + + ClearAllDeferredTriggerScopes(); + } +} + bool UFlowAsset::HasStartedFlow() const { return RecordedNodes.Num() > 0; @@ -1081,11 +1141,6 @@ TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGr void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const { - if (FFlowExecutionGate::IsHalted()) - { - return; - } - // NOTE (gtaylor) Custom Input nodes cannot currently add data pins (like Start or DefineProperties nodes can) // but we may want to allow them to source parameters, so I am providing the subgraph node as the // IFlowDataPinValueSupplierInterface when triggering the node (even though it's not used at this time). @@ -1099,11 +1154,6 @@ void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNod void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier) { - if (FFlowExecutionGate::IsHalted()) - { - return; - } - for (UFlowNode_CustomInput* CustomInputNode : CustomInputNodes) { if (CustomInputNode->EventName == EventName) @@ -1143,11 +1193,33 @@ void UFlowAsset::TriggerCustomOutput(const FName& EventName) void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) { - if (FFlowExecutionGate::EnqueueDeferredTriggerInput(this, NodeGuid, PinName, FromPin)) + if (FFlowExecutionGate::IsHalted()) { - return; + // Halt always takes precedence for debugger correctness + EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); } + else if (ShouldUseStandardDeferTriggers()) + { + // Defer only if we have an open the top scope + if (!DeferredTransitionScopes.IsEmpty() && DeferredTransitionScopes.Top()->IsOpen()) + { + EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); + } + else + { + const TSharedPtr CurentScope = PushDeferredTransitionScope(); + TriggerInputDirect(NodeGuid, PinName, FromPin); + PopDeferredTransitionScope(CurentScope); + } + } + else + { + TriggerInputDirect(NodeGuid, PinName, FromPin); + } +} +void UFlowAsset::TriggerInputDirect(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) +{ if (UFlowNode* Node = Nodes.FindRef(NodeGuid)) { if (!ActiveNodes.Contains(Node)) @@ -1160,6 +1232,80 @@ void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const } } +bool UFlowAsset::ShouldUseStandardDeferTriggers() const +{ + return GetDefault()->bDeferTriggeredOutputsWhileTriggering; +} + +TSharedPtr UFlowAsset::PushDeferredTransitionScope() +{ + // Close the former top scope (if any) + if (!DeferredTransitionScopes.IsEmpty()) + { + const TSharedPtr& FormerTop = DeferredTransitionScopes.Top(); + FormerTop->CloseScope(); + } + + // Push a fresh open scope + return DeferredTransitionScopes.Add_GetRef(MakeShared()); +} + +bool UFlowAsset::TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& ScopeToFlush) +{ + if (ScopeToFlush->TryFlushDeferredTriggers(*this)) + { + // Remove the exact instance we were holding (handles nested push/pop cases) + DeferredTransitionScopes.RemoveSingle(ScopeToFlush); + return true; + } + else + { + // Flush was interrupted — should only happen due to execution gate halt + check(FFlowExecutionGate::IsHalted()); + return false; + } +} + +void UFlowAsset::EnqueueDeferredTrigger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) +{ + if (DeferredTransitionScopes.IsEmpty() || !DeferredTransitionScopes.Top()->IsOpen()) + { + // This should only occur when halted at an execution gate + check(FFlowExecutionGate::IsHalted()); + PushDeferredTransitionScope(); + } + + // Always enqueue to the current innermost (top) scope + DeferredTransitionScopes.Top()->EnqueueDeferredTrigger(FFlowDeferredTriggerInput{ NodeGuid, PinName, FromPin }); +} + +bool UFlowAsset::TryFlushAllDeferredTriggerScopes() +{ + while (const TSharedPtr TopScope = GetTopDeferredTransitionScope()) + { + if (!TryFlushAndRemoveDeferredTransitionScope(TopScope)) + { + break; + } + + // Keep flushing until stack is empty or we hit an ExecutionGate halt + } + + check(DeferredTransitionScopes.IsEmpty() || FFlowExecutionGate::IsHalted()); + + return DeferredTransitionScopes.IsEmpty(); +} + +void UFlowAsset::ClearAllDeferredTriggerScopes() +{ + DeferredTransitionScopes.Reset(); +} + +TSharedPtr UFlowAsset::GetTopDeferredTransitionScope() const +{ + return !DeferredTransitionScopes.IsEmpty() ? DeferredTransitionScopes.Top() : nullptr; +} + void UFlowAsset::FinishNode(UFlowNode* Node) { if (ActiveNodes.Contains(Node)) diff --git a/Source/Flow/Private/FlowSettings.cpp b/Source/Flow/Private/FlowSettings.cpp index e098071fd..e6f835f4e 100644 --- a/Source/Flow/Private/FlowSettings.cpp +++ b/Source/Flow/Private/FlowSettings.cpp @@ -11,6 +11,7 @@ UFlowSettings::UFlowSettings(const FObjectInitializer& ObjectInitializer) , bWarnAboutMissingIdentityTags(true) , bLogOnSignalDisabled(true) , bLogOnSignalPassthrough(true) + , bDeferTriggeredOutputsWhileTriggering(true) , bUseAdaptiveNodeTitles(false) , DefaultExpectedOwnerClass(UFlowComponent::StaticClass()) { diff --git a/Source/Flow/Private/FlowSubsystem.cpp b/Source/Flow/Private/FlowSubsystem.cpp index 70a88eafe..0bcf694b4 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" @@ -267,6 +268,63 @@ void UFlowSubsystem::RemoveInstancedTemplate(UFlowAsset* Template) InstancedTemplates.Remove(Template); } +bool UFlowSubsystem::TryFlushAllDeferredTriggerScopes() const +{ + // 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 (const 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 (const UFlowAsset* Template : InstancedTemplates) + { + if (!IsValid(Template)) + { + continue; + } + + for (UFlowAsset* Instance : Template->GetActiveInstances()) + { + if (IsValid(Instance)) + { + Instance->ClearAllDeferredTriggerScopes(); + } + } + } +} + TMap UFlowSubsystem::GetRootInstances() const { TMap Result; diff --git a/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp index 44fd864f2..e09b7d02a 100644 --- a/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp +++ b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp @@ -5,20 +5,6 @@ #include "FlowAsset.h" #include "Nodes/FlowPin.h" -namespace FlowExecutionGate_Private -{ - struct FDeferredTriggerInput - { - TWeakObjectPtr FlowAssetInstance; - FGuid NodeGuid; - FName PinName; - FConnectedPin FromPin; - }; - - static TArray DeferredTriggerInputs; - static bool bIsFlushing = false; -} - IFlowExecutionGate* FFlowExecutionGate::Gate = nullptr; void FFlowExecutionGate::SetGate(IFlowExecutionGate* InGate) @@ -34,101 +20,4 @@ IFlowExecutionGate* FFlowExecutionGate::GetGate() bool FFlowExecutionGate::IsHalted() { return (Gate != nullptr) && Gate->IsFlowExecutionHalted(); -} - -bool FFlowExecutionGate::EnqueueDeferredTriggerInput(UFlowAsset* FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) -{ - using namespace FlowExecutionGate_Private; - - // If we're halted, always enqueue (even during flushing). The whole point is to stop propagation. - if (IsHalted()) - { - if (!IsValid(FlowAssetInstance)) - { - return true; // treat as handled while halted - } - - FDeferredTriggerInput& Entry = DeferredTriggerInputs.AddDefaulted_GetRef(); - Entry.FlowAssetInstance = FlowAssetInstance; - Entry.NodeGuid = NodeGuid; - Entry.PinName = PinName; - Entry.FromPin = FromPin; - - return true; - } - - // Not halted: - // During flush we must not enqueue "normal" triggers (we want them to execute now), - // otherwise we can get infinite deferral. - if (bIsFlushing) - { - return false; - } - - return false; -} - -void FFlowExecutionGate::FlushDeferredTriggerInputs() -{ - using namespace FlowExecutionGate_Private; - - if (bIsFlushing) - { - return; - } - - // Do not flush while halted; callers should clear the halt first. - if (IsHalted()) - { - return; - } - - if (DeferredTriggerInputs.IsEmpty()) - { - return; - } - - bIsFlushing = true; - - // Move into a local array so new deferred triggers can be added while we flush. - TArray Local = MoveTemp(DeferredTriggerInputs); - DeferredTriggerInputs.Reset(); - - for (int32 Index = 0; Index < Local.Num(); ++Index) - { - // If a breakpoint was hit during this flush, stop immediately and re-queue remaining work. - if (IsHalted()) - { - const int32 Remaining = Local.Num() - Index; - if (Remaining > 0) - { - TArray RemainingItems; - RemainingItems.Reserve(Remaining); - - for (int32 j = Index; j < Local.Num(); ++j) - { - RemainingItems.Add(Local[j]); - } - - // RemainingItems should run before any items that may already be queued. - if (!DeferredTriggerInputs.IsEmpty()) - { - RemainingItems.Append(MoveTemp(DeferredTriggerInputs)); - } - - DeferredTriggerInputs = MoveTemp(RemainingItems); - } - - bIsFlushing = false; - return; - } - - const FDeferredTriggerInput& Entry = Local[Index]; - if (UFlowAsset* Asset = Entry.FlowAssetInstance.Get()) - { - Asset->TriggerDeferredInputFromDebugger(Entry.NodeGuid, Entry.PinName, Entry.FromPin); - } - } - - bIsFlushing = false; } \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp b/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp index 794625eed..3cb7fbbab 100644 --- a/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp +++ b/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp @@ -33,7 +33,7 @@ void UFlowNode_Log::ExecuteInput(const FName& PinName) const EFlowDataPinResolveResult MessageResult = TryResolveDataPinValue(GET_MEMBER_NAME_CHECKED(ThisClass, Message), ResolvedMessage); // #FlowDataPinLegacy - retire this backward compatibility when we remove legacy data pin support? - FLOW_ASSERT_ENUM_MAX(EFlowDataPinResolveResult, 8); + FLOW_ASSERT_ENUM_MAX(EFlowDataPinResolveResult, 9); if (MessageResult == EFlowDataPinResolveResult::FailedUnknownPin) { // Handle lookup of a FlowNode_Log that predated DataPins diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index aa319b35a..652f23bdd 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -417,7 +417,7 @@ void UFlowNode::SetAutoOutputDataPins(const TArray& AutoOutputPins) #endif // WITH_EDITOR -FFlowDataPinResult UFlowNode::TrySupplyDataPin_Implementation(FName PinName) const +FFlowDataPinResult UFlowNode::TrySupplyDataPin(FName PinName) const { const FFlowPin* FlowPin = FindOutputPinByName(PinName); if (!FlowPin) @@ -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,22 +505,66 @@ 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; } @@ -530,15 +585,10 @@ bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, T // Potentially add this current node as a default value supplier // (this will be pushed down the priority queue as higher priority suppliers are found) - if (ThisAsPinValueSupplier && IFlowDataPinValueSupplierInterface::Execute_CanSupplyDataPinValues(this)) - { - FFlowPinValueSupplierData NewPinValueSupplier; - NewPinValueSupplier.PinValueSupplier = ThisAsPinValueSupplier; - NewPinValueSupplier.SupplierPinName = PinName; - - // Put this node as the backup supplier - InOutPinValueSupplierDatas.Add(NewPinValueSupplier); - } + FFlowPinValueSupplierData NewPinValueSupplier; + NewPinValueSupplier.PinValueSupplier = ThisAsPinValueSupplier; + NewPinValueSupplier.SupplierPinName = PinName; + TryAddSupplierDataToArray(NewPinValueSupplier, InOutPinValueSupplierDatas); // If the pin is connected, try to add the connected node as the priority supplier FFlowPinValueSupplierData ConnectedPinValueSupplier; @@ -550,23 +600,11 @@ bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, T { const UFlowNode* SupplierFlowNode = FlowAsset->GetNode(ConnectedNodeGuid); - // If the connected node can supply data pin values, insert it into the top of the priority queue - const IFlowDataPinValueSupplierInterface* SupplierFlowNodeAsInterface = Cast(SupplierFlowNode); - if (SupplierFlowNodeAsInterface && IFlowDataPinValueSupplierInterface::Execute_CanSupplyDataPinValues(SupplierFlowNode)) + if (IsValid(SupplierFlowNode)) { - ConnectedPinValueSupplier.PinValueSupplier = SupplierFlowNodeAsInterface; - - InOutPinValueSupplierDatas.Add(ConnectedPinValueSupplier); - } + ConnectedPinValueSupplier.PinValueSupplier = Cast(SupplierFlowNode); - // Exception case for nodes with external suppliers, recurse here to crawl further - // to the external supplier's connected pin as our most preferred source (see block comment above). - if (const IFlowNodeWithExternalDataPinSupplierInterface* HasExternalPinSupplierInterface = Cast(SupplierFlowNode)) - { - if (const UFlowNode* ExternalDataPinSupplierFlowNode = Cast(HasExternalPinSupplierInterface->GetExternalDataPinSupplier())) - { - return ExternalDataPinSupplierFlowNode->TryGetFlowDataPinSupplierDatasForPinName(ConnectedPinValueSupplier.SupplierPinName, InOutPinValueSupplierDatas); - } + TryAddSupplierDataToArray(ConnectedPinValueSupplier, InOutPinValueSupplierDatas); } } } @@ -574,6 +612,26 @@ bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, T return !InOutPinValueSupplierDatas.IsEmpty(); } +void UFlowNode::TryAddSupplierDataToArray(FFlowPinValueSupplierData& InOutSupplierData, TFlowPinValueSupplierDataArray& InOutPinValueSupplierDatas) const +{ + // If the connected node can supply data pin values, insert it into the top of the priority queue + const UFlowNode* SupplierFlowNode = CastChecked(InOutSupplierData.PinValueSupplier); + if (InOutSupplierData.PinValueSupplier && SupplierFlowNode->CanSupplyDataPinValues()) + { + InOutPinValueSupplierDatas.Add(InOutSupplierData); + } + + // Exception case for nodes with external suppliers, recurse here to crawl further + // to the external supplier's connected pin as our most preferred source (see block comment above). + if (const IFlowNodeWithExternalDataPinSupplierInterface* HasExternalPinSupplierInterface = Cast(SupplierFlowNode)) + { + if (const UFlowNode* ExternalDataPinSupplierFlowNode = Cast(HasExternalPinSupplierInterface->GetExternalDataPinSupplier())) + { + ExternalDataPinSupplierFlowNode->TryGetFlowDataPinSupplierDatasForPinName(InOutSupplierData.SupplierPinName, InOutPinValueSupplierDatas); + } + } +} + #if WITH_EDITOR void UFlowNode::AutoGenerateDataPins(FFlowAutoDataPinsWorkingData& InOutWorkingData) const { @@ -582,11 +640,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 @@ -712,7 +772,7 @@ FFlowPin* UFlowNode::FindOutputPinByName(const FName& PinName) return nullptr; } -bool UFlowNode::IsInputConnected(const FFlowPin& FlowPin) const +bool UFlowNode::IsInputConnected(const FFlowPin& FlowPin, FGuid* FoundGuid, FName* OutConnectedPinName) const { if (!InputPins.Contains(FlowPin.PinName)) { @@ -723,15 +783,15 @@ bool UFlowNode::IsInputConnected(const FFlowPin& FlowPin) const { // We don't cache the input exec pins for fast lookup in Connections, so use the slow path for them: - return FindConnectedNodeForPinSlow(FlowPin.PinName); + return FindConnectedNodeForPinSlow(FlowPin.PinName, FoundGuid, OutConnectedPinName); } else { - return FindConnectedNodeForPinFast(FlowPin.PinName); + return FindConnectedNodeForPinFast(FlowPin.PinName, FoundGuid, OutConnectedPinName); } } -bool UFlowNode::IsOutputConnected(const FFlowPin& FlowPin) const +bool UFlowNode::IsOutputConnected(const FFlowPin& FlowPin, FGuid* FirstFoundGuid, FName* OutFirstConnectedPinName) const { if (!OutputPins.Contains(FlowPin.PinName)) { @@ -740,13 +800,13 @@ bool UFlowNode::IsOutputConnected(const FFlowPin& FlowPin) const if (FlowPin.IsExecPin()) { - return FindConnectedNodeForPinFast(FlowPin.PinName); + return FindConnectedNodeForPinFast(FlowPin.PinName, FirstFoundGuid, OutFirstConnectedPinName); } else { // We don't cache the input data pins for fast lookup in Connections, so use the slow path for them: - return FindConnectedNodeForPinSlow(FlowPin.PinName); + return FindConnectedNodeForPinSlow(FlowPin.PinName, FirstFoundGuid, OutFirstConnectedPinName); } } diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp index 56360b9a1..8f60f3e4e 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBase.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -1020,7 +1020,7 @@ FFlowDataPinResult UFlowNodeBase::TryResolveDataPin(FName PinName) const { const FFlowPinValueSupplierData& SupplierData = PinValueSupplierDatas[Index]; - DataPinResult = IFlowDataPinValueSupplierInterface::Execute_TrySupplyDataPin(CastChecked(SupplierData.PinValueSupplier), SupplierData.SupplierPinName); + DataPinResult = SupplierData.PinValueSupplier->TrySupplyDataPin(SupplierData.SupplierPinName); if (FlowPinType::IsSuccess(DataPinResult.Result)) { diff --git a/Source/Flow/Private/Nodes/FlowPin.cpp b/Source/Flow/Private/Nodes/FlowPin.cpp index 8c2a34cd5..41581cc3c 100644 --- a/Source/Flow/Private/Nodes/FlowPin.cpp +++ b/Source/Flow/Private/Nodes/FlowPin.cpp @@ -149,6 +149,13 @@ FEdGraphPinType FFlowPin::BuildEdGraphPinType() const return EdGraphPinType; } + +void FFlowPin::ConfigureFromEdGraphPin(const FEdGraphPinType& EdGraphPinType) +{ + PinTypeName.Name = EdGraphPinType.PinCategory; + PinSubCategoryObject = EdGraphPinType.PinSubCategoryObject; + ContainerType = EdGraphPinType.ContainerType; +} #endif const FFlowPinType* FFlowPin::ResolveFlowPinType() const @@ -199,42 +206,9 @@ FFlowPinTypeName FFlowPin::GetPinTypeNameForLegacyPinType(EFlowPinType PinType) return FFlowPinTypeName(); } } +// -- #if WITH_EDITOR -void FFlowPin::PostEditChangedPinTypeOrSubCategorySource() -{ - // PinTypes with PinSubCategoryObjects will need to update this function - - // Must be called from PostEditChangeProperty() by an owning UObject - - if (PinTypeName == FFlowPinType_Class::GetPinTypeNameStatic()) - { - PinSubCategoryObject = SubCategoryClassFilter; - } - else if (PinTypeName == FFlowPinType_Object::GetPinTypeNameStatic()) - { - PinSubCategoryObject = SubCategoryObjectFilter; - } - else if (PinTypeName == FFlowPinType_Enum::GetPinTypeNameStatic()) - { - if (!SubCategoryEnumName.IsEmpty()) - { - SubCategoryEnumClass = UClass::TryFindTypeSlow(SubCategoryEnumName, EFindFirstObjectOptions::ExactClass); - if (SubCategoryEnumClass != nullptr && !FFlowPin::ValidateEnum(*SubCategoryEnumClass)) - { - SubCategoryEnumClass = nullptr; - } - } - - PinSubCategoryObject = SubCategoryEnumClass; - } - else - { - TrySetStructSubCategoryObjectFromPinType(); - } -} - -// -- FText FFlowPin::BuildHeaderText() const { diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.cpp new file mode 100644 index 000000000..d1011182c --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.cpp @@ -0,0 +1,26 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_BlueprintDataPinSupplierBase) + +UFlowNode_BlueprintDataPinSupplierBase::UFlowNode_BlueprintDataPinSupplierBase(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::Default; + Category = TEXT("Graph"); +#endif + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; +} + +FFlowDataPinResult UFlowNode_BlueprintDataPinSupplierBase::TrySupplyDataPin(FName PinName) const +{ + return BP_TrySupplyDataPin(PinName); +} + +FFlowDataPinResult UFlowNode_BlueprintDataPinSupplierBase::BP_TrySupplyDataPin_Implementation(FName PinName) const +{ + return Super::TrySupplyDataPin(PinName); +} diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_DefineProperties.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_DefineProperties.cpp index 6d453eaf0..10335e5f3 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_FormatText.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp index 13c055433..ceeff569d 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp @@ -20,7 +20,7 @@ UFlowNode_FormatText::UFlowNode_FormatText(const FObjectInitializer& ObjectIniti OutputPins.Add(FFlowPin(OUTPIN_TextOutput, FFlowPinType_Text::GetPinTypeNameStatic())); } -FFlowDataPinResult UFlowNode_FormatText::TrySupplyDataPin_Implementation(FName PinName) const +FFlowDataPinResult UFlowNode_FormatText::TrySupplyDataPin(FName PinName) const { if (PinName == OUTPIN_TextOutput) { @@ -37,7 +37,7 @@ FFlowDataPinResult UFlowNode_FormatText::TrySupplyDataPin_Implementation(FName P } } - return Super::TrySupplyDataPin_Implementation(PinName); + return Super::TrySupplyDataPin(PinName); } EFlowDataPinResolveResult UFlowNode_FormatText::TryResolveFormatText(const FName& PinName, FText& OutFormattedText) const diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_Start.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_Start.cpp index b8f399b78..81683dfc9 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_Start.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_Start.cpp @@ -44,11 +44,11 @@ bool UFlowNode_Start::TryAppendExternalInputPins(TArray& InOutPins) co #endif // WITH_EDITOR -FFlowDataPinResult UFlowNode_Start::TrySupplyDataPin_Implementation(FName PinName) const +FFlowDataPinResult UFlowNode_Start::TrySupplyDataPin(FName PinName) const { if (FlowDataPinValueSupplierInterface) { - FFlowDataPinResult SuppliedResult = IFlowDataPinValueSupplierInterface::Execute_TrySupplyDataPin(FlowDataPinValueSupplierInterface.GetObject(), PinName); + FFlowDataPinResult SuppliedResult = FlowDataPinValueSupplierInterface->TrySupplyDataPin(PinName); if (FlowPinType::IsSuccess(SuppliedResult.Result)) { @@ -56,6 +56,6 @@ FFlowDataPinResult UFlowNode_Start::TrySupplyDataPin_Implementation(FName PinNam } } - return Super::TrySupplyDataPin_Implementation(PinName); + return Super::TrySupplyDataPin(PinName); } diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp index 9e74acc08..6465b09e4 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp @@ -14,6 +14,7 @@ FFlowPin UFlowNode_SubGraph::StartPin(TEXT("Start")); FFlowPin UFlowNode_SubGraph::FinishPin(TEXT("Finish")); +const FName UFlowNode_SubGraph::AssetParams_MemberName = GET_MEMBER_NAME_CHECKED(ThisClass, AssetParams); UFlowNode_SubGraph::UFlowNode_SubGraph(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -212,7 +213,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)); + } } } } @@ -249,10 +258,42 @@ void UFlowNode_SubGraph::PostEditChangeProperty(FPropertyChangedEvent& PropertyC } } -bool UFlowNode_SubGraph::CanSupplyDataPinValues_Implementation() const +FFlowDataPinResult UFlowNode_SubGraph::TrySupplyDataPin(FName PinName) const { - // SubGraph node cannot supply data-pin values directly (they are created via AutoGenerateDataPins instead) - return false; + if (PinName == AssetParams_MemberName) + { + // Prevent infinite recursion by sourcing the AssetParams pin directly. + // Otherwise, it would attempt to resolve it below and infinitely crash our stack. + return Super::TrySupplyDataPin(PinName); + } + + if (!IsInputConnected(PinName)) + { + const bool bHasAssetParams = IsInputConnected(AssetParams_MemberName) || !AssetParams.IsNull(); + if (bHasAssetParams) + { + // If not connected, we can source the value from the asset data params (if available) + TObjectPtr Value = nullptr; + const EFlowDataPinResolveResult ResultEnum = Super::TryResolveDataPinValue(AssetParams_MemberName, Value); + if (FlowPinType::IsSuccess(ResultEnum) && IsValid(Value)) + { + if (const IFlowDataPinValueSupplierInterface* SupplierInterface = Cast(Value)) + { + return SupplierInterface->TrySupplyDataPin(PinName); + } + else + { + LogError(FString::Printf(TEXT("Could not cast object %s to IFlowDataPinValueSupplierInterface! This is unexpected."), *Value->GetName())); + + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedWithError); + } + } + } + } + + // Prefer the standard lookup if the pin is connected + // (or if there is no FlowAssetParams to ask) + return Super::TrySupplyDataPin(PinName); } void UFlowNode_SubGraph::SubscribeToAssetChanges() diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp index 2c24b9900..0d321565f 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp @@ -1,6 +1,7 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "Nodes/Route/FlowNode_Reroute.h" +#include "FlowAsset.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Reroute) @@ -18,3 +19,49 @@ void UFlowNode_Reroute::ExecuteInput(const FName& PinName) { TriggerFirstOutput(true); } + +#if WITH_EDITOR +void UFlowNode_Reroute::ConfigureInputPin(const UFlowNode& ConnectedNode, const FEdGraphPinType& EdGraphPinType) +{ + FFlowPin* InputPin = FindInputPinByName(UFlowNode::DefaultInputPin.PinName); + if (ensure(InputPin)) + { + InputPin->ConfigureFromEdGraphPin(EdGraphPinType); + } +} + +void UFlowNode_Reroute::ConfigureOutputPin(const UFlowNode& ConnectedNode, const FEdGraphPinType& EdGraphPinType) +{ + FFlowPin* OutputPin = FindOutputPinByName(UFlowNode::DefaultOutputPin.PinName); + if (ensure(OutputPin)) + { + OutputPin->ConfigureFromEdGraphPin(EdGraphPinType); + } +} +#endif + +FFlowDataPinResult UFlowNode_Reroute::TrySupplyDataPin(FName PinName) const +{ + const FFlowPin* InputPin = FindInputPinByName(UFlowNode::DefaultInputPin.PinName); + if (!InputPin) + { + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); + } + + FGuid FoundGuid; + FName ConnectedPinName; + if (!IsInputConnected(*InputPin, &FoundGuid, &ConnectedPinName)) + { + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedNotConnected); + } + + const UFlowNode* ConnectedFlowNodeSupplier = GetFlowAsset()->GetNode(FoundGuid); + if (!IsValid(ConnectedFlowNodeSupplier)) + { + checkf(IsValid(ConnectedFlowNodeSupplier), TEXT("This node should be valid if IsInputConnected returned true")); + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedNotConnected); + } + + // Hand-off to the connected flow node to supply the value + return ConnectedFlowNodeSupplier->TrySupplyDataPin(ConnectedPinName); +} diff --git a/Source/Flow/Private/Types/FlowAutoDataPinsWorkingData.cpp b/Source/Flow/Private/Types/FlowAutoDataPinsWorkingData.cpp index 78107aa17..fd3fba57c 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/FlowPinType.cpp b/Source/Flow/Private/Types/FlowPinType.cpp index c73ffd183..39bc03252 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 b959fe6d9..6dd857b04 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 1fe43bd6d..abe446cf9 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/FlowAssetParams.h b/Source/Flow/Public/Asset/FlowAssetParams.h index 43b9f23eb..f55e5c93b 100644 --- a/Source/Flow/Public/Asset/FlowAssetParams.h +++ b/Source/Flow/Public/Asset/FlowAssetParams.h @@ -54,8 +54,8 @@ class FLOW_API UFlowAssetParams // -- // IFlowDataPinValueSupplierInterface - virtual bool CanSupplyDataPinValues_Implementation() const override; - virtual FFlowDataPinResult TrySupplyDataPin_Implementation(FName PinName) const override; + virtual bool CanSupplyDataPinValues() const override; + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; // -- // IFlowAssetProviderInterface @@ -73,6 +73,9 @@ class FLOW_API UFlowAssetParams const TSoftObjectPtr& InOwnerFlowAsset, TArray& MutablePropertiesFromStartNode); + // Updates properties from ParentParams, handling inheritance and name enforcement. + EFlowReconcilePropertiesResult ReconcilePropertiesWithParentParams(); + void ConfigureFlowAssetParams(TSoftObjectPtr OwnerAsset, TSoftObjectPtr InParentParams, const TArray& InProperties); // IFlowDataPinValueOwnerInterface @@ -99,9 +102,6 @@ class FLOW_API UFlowAssetParams protected: - // Updates properties from ParentParams, handling inheritance and name enforcement. - EFlowReconcilePropertiesResult ReconcilePropertiesWithParentParams(); - EFlowReconcilePropertiesResult CheckForParentCycle() const; void ModifyAndRebuildPropertiesMap(); diff --git a/Source/Flow/Public/Asset/FlowAssetParamsUtils.h b/Source/Flow/Public/Asset/FlowAssetParamsUtils.h index 9c1f966a1..13dc1bd3b 100644 --- a/Source/Flow/Public/Asset/FlowAssetParamsUtils.h +++ b/Source/Flow/Public/Asset/FlowAssetParamsUtils.h @@ -8,6 +8,7 @@ #include "FlowAssetParamsUtils.generated.h" class UObject; +class UFlowAssetParams; struct FFlowNamedDataPinProperty; /** @@ -40,5 +41,26 @@ struct FLOW_API FFlowAssetParamsUtils static bool ArePropertiesEqual( const FFlowNamedDataPinProperty& A, const FFlowNamedDataPinProperty& B); + + /** + * Create Flow Asset Params asset from a parent params asset. + * - Creates the new asset in the same folder as the parent + * - Uses parent's asset name as the base for unique name generation (ParentName, ParentName_1, ...) + * - Copies OwnerFlowAsset + Properties and sets ParentParams to the provided parent + * - Runs ReconcilePropertiesWithParentParams (cycle detection, flattened inheritance, etc.) + * - Attempts source control checkout/add + * - Saves the new package + * - Registers and syncs to Content Browser + * + * @param ParentParams The parent params asset to inherit from. Must be valid. + * @param bShowDialogs If true, errors are surfaced via modal dialogs as well as logs. + * @param OutOptionalFailureReason If provided, filled with a human-readable error message on failure. + * @return The created child params asset or nullptr on failure. + */ + static UFlowAssetParams* CreateChildParamsAsset(UFlowAssetParams& ParentParams, bool bShowDialogs = true, FText* OutOptionalFailureReason = nullptr); + +protected: + static void FailCreateChild(const FText& Reason, const bool bShowDialogs, FText* OutOptionalFailureReason); + #endif -}; \ No newline at end of file +}; diff --git a/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h b/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h new file mode 100644 index 000000000..66a3783be --- /dev/null +++ b/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h @@ -0,0 +1,36 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Misc/Guid.h" + +#include "Nodes/FlowPin.h" + +class UFlowAsset; + +struct FFlowDeferredTriggerInput +{ + FGuid NodeGuid; + FName PinName; + FConnectedPin FromPin; +}; + +struct FLOW_API FFlowDeferredTransitionScope +{ +public: + void EnqueueDeferredTrigger(const FFlowDeferredTriggerInput& Entry); + bool TryFlushDeferredTriggers(UFlowAsset& OwningFlowAsset); + + void CloseScope() { bIsOpen = false; } + bool IsOpen() const { return bIsOpen; } + + const TArray& GetDeferredTriggers() const { return DeferredTriggers; } + +protected: + + // Deferred triggers for this scope + TArray DeferredTriggers; + + // Is currently accepting new deferred triggers + bool bIsOpen = true; +}; diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index 58a28b340..d4b9efa5e 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; @@ -30,7 +32,7 @@ DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, UFlowNode* /*FlowNode*/, const FNam #endif /** - * Single asset containing flow nodes. + * Asset containing Flow nodes organized as non-linear graph. */ UCLASS(BlueprintType, hideCategories = Object) class FLOW_API UFlowAsset : public UObject @@ -46,12 +48,13 @@ 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; - // Set it to False, if this asset is instantiated as Root Flow for owner that doesn't live in the world - // This allows to SaveGame support works properly, if owner of Root Flow would be Game Instance or its subsystem + /* Set it to False, if this asset is instantiated as Root Flow for owner that doesn't live in the world. + * This allows to SaveGame support works properly, if owner of Root Flow would be Game Instance or its subsystem. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Flow Asset") bool bWorldBound; @@ -90,7 +93,7 @@ class FLOW_API UFlowAsset : public UObject virtual EDataValidationResult ValidateAsset(FFlowMessageLog& MessageLog); - // Returns whether the node class is allowed in this flow asset + /* Returns whether the node class is allowed in this flow asset. */ bool IsNodeOrAddOnClassAllowed(const UClass* FlowNodeClass, FText* OutOptionalFailureReason = nullptr) const; virtual TSubclassOf GetDefaultFlowAssetForSubgraphs() const { return GetClass(); } @@ -104,7 +107,7 @@ class FLOW_API UFlowAsset : public UObject bool IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) const; private: - // Recursively validates the given addon and its children. + /* Recursively validates the given addon and its children. */ void ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& MessageLog); #endif @@ -126,17 +129,13 @@ class FLOW_API UFlowAsset : public UObject #if WITH_EDITORONLY_DATA protected: - /** - * Custom Inputs define custom entry points in graph, it's similar to blueprint Custom Events - * Sub Graph node using this Flow Asset will generate context Input Pin for every valid Event name on this list - */ + /* Custom Inputs define custom entry points in graph, it's similar to blueprint Custom Events. + * Sub Graph node using this Flow Asset will generate context Input Pin for every valid Event name on this list. */ UPROPERTY(EditAnywhere, Category = "Sub Graph") TArray CustomInputs; - /** - * Custom Outputs define custom graph outputs, this allows to send signals to the parent graph while executing this graph - * Sub Graph node using this Flow Asset will generate context Output Pin for every valid Event name on this list - */ + /* Custom Outputs define custom graph outputs, this allows to send signals to the parent graph while executing this graph. + * Sub Graph node using this Flow Asset will generate context Output Pin for every valid Event name on this list. */ UPROPERTY(EditAnywhere, Category = "Sub Graph") TArray CustomOutputs; #endif // WITH_EDITORONLY_DATA @@ -150,12 +149,12 @@ class FLOW_API UFlowAsset : public UObject void RegisterNode(const FGuid& NewGuid, UFlowNode* NewNode); void UnregisterNode(const FGuid& NodeGuid); - // Processes nodes and updates pin connections from the graph to the UFlowNode (processes all nodes in the graph if passed nullptr) + /* Processes nodes and updates pin connections from the graph to the UFlowNode (processes all nodes in the graph if passed nullptr). */ void HarvestNodeConnections(UFlowNode* TargetNode = nullptr); static bool TryGetDefaultForInputPinName(const FStructProperty& StructProperty, const void* Container, FString& OutString); - // Updates the auto-generated pins and bindings for a given FlowNode, returns true if any changes were made. + /* Updates the auto-generated pins and bindings for a given FlowNode, returns true if any changes were made. */ static bool TryUpdateManagedFlowPinsForNode(UFlowNode& FlowNode); #endif @@ -181,10 +180,10 @@ class FLOW_API UFlowAsset : public UObject UFUNCTION(BlueprintPure, Category = "FlowAsset") virtual UFlowNode* GetDefaultEntryNode() const; - // Gathers all of the nodes that are connected to the Start & Custom Inputs of the flow graph + /* Gathers all the nodes that are connected to the Start & Custom Inputs of the flow graph. */ TArray GatherNodesConnectedToAllInputs() const; - // Return all other Pins connected to the passed Pin. + /* Return all other Pins connected to the passed Pin. */ TArray GatherPinsConnectedToPin(const FConnectedPin& Pin) const; UFUNCTION(BlueprintPure, Category = "FlowAsset", meta = (DeterminesOutputType = "FlowNodeClass")) @@ -245,15 +244,15 @@ class FLOW_API UFlowAsset : public UObject // Instances of the template asset private: - // Original object holds references to instances + /* Original object holds references to instances. */ UPROPERTY(Transient) TArray> ActiveInstances; #if WITH_EDITORONLY_DATA TWeakObjectPtr InspectedInstance; - // Message log for storing runtime errors/notes/warnings that will only last until the next game run - // Log lives in the asset template, so it can be inspected after ending the PIE + /* Message log for storing runtime errors/notes/warnings that will only last until the next game run. + * Log lives in the asset template, so it can be inspected after ending the PIE. */ TSharedPtr RuntimeLog; #endif @@ -291,29 +290,29 @@ class FLOW_API UFlowAsset : public UObject UPROPERTY() TObjectPtr TemplateAsset; - // Object that spawned Root Flow instance, i.e. World Settings or Player Controller - // This pointer is passed to child instances: Flow Asset instances created by the SubGraph nodes + /* Object that spawned Root Flow instance, i.e. World Settings or Player Controller. + * This pointer is passed to child instances: Flow Asset instances created by the SubGraph nodes. */ TWeakObjectPtr Owner; - // SubGraph node that created this Flow Asset instance + /* SubGraph node that created this Flow Asset instance. */ TWeakObjectPtr NodeOwningThisAssetInstance; - // Flow Asset instances created by SubGraph nodes placed in the current graph + /* Flow Asset instances created by SubGraph nodes placed in the current graph. */ TMap, TWeakObjectPtr> ActiveSubGraphs; - // Optional entry points to the graph, similar to blueprint Custom Events - // Contains nodes only if it is initialized instance (see InitializeInstance, IsInstanceInitialized), empty otherwise + /* Optional entry points to the graph, similar to blueprint Custom Events. + * Contains nodes only if it is initialized instance (see InitializeInstance, IsInstanceInitialized), empty otherwise. */ UPROPERTY() TSet> CustomInputNodes; UPROPERTY() TSet> PreloadedNodes; - // Nodes that have any work left, not marked as Finished yet + /* Nodes that have any work left, not marked as Finished yet. */ UPROPERTY() TArray> ActiveNodes; - // All nodes active in the past, done their work + /* All nodes active in the past, done their work. */ UPROPERTY() TArray> RecordedNodes; @@ -329,8 +328,8 @@ class FLOW_API UFlowAsset : public UObject UFlowAsset* GetTemplateAsset() const { return TemplateAsset; } - // Object that spawned Root Flow instance, i.e. World Settings or Player Controller - // This pointer is passed to child instances: Flow Asset instances created by the SubGraph nodes + /* Object that spawned Root Flow instance, i.e. World Settings or Player Controller. + * This pointer is passed to child instances: Flow Asset instances created by the SubGraph nodes. */ UFUNCTION(BlueprintPure, Category = "Flow") UObject* GetOwner() const { return Owner.Get(); } @@ -340,11 +339,11 @@ class FLOW_API UFlowAsset : public UObject return Owner.IsValid() ? Cast(Owner) : nullptr; } - // Returns the Owner as an Actor, or if Owner is a Component, return its Owner as an Actor + /* Returns the Owner as an Actor, or if Owner is a Component, return its Owner as an Actor. */ UFUNCTION(BlueprintPure, Category = "Flow") AActor* TryFindActorOwner() const; - // Opportunity to preload content of project-specific nodes + /* Opportunity to preload content of project-specific nodes. */ virtual void PreloadNodes() {} virtual void PreStartFlow(); @@ -355,18 +354,14 @@ class FLOW_API UFlowAsset : public UObject bool HasStartedFlow() const; void TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr); - // Get Flow Asset instance created by the given SubGraph node + /* 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); - // TODO: Extend FromPin through to Node level Trigger functions + /* todo: Extend FromPin through to Node level Trigger functions. */ virtual void TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); virtual void FinishNode(UFlowNode* Node); @@ -384,25 +379,59 @@ class FLOW_API UFlowAsset : public UObject UFlowNode_SubGraph* GetNodeOwningThisAssetInstance() const; UFlowAsset* GetParentInstance() const; - // Are there any active nodes? + /* Are there any active nodes? */ UFUNCTION(BlueprintPure, Category = "Flow") bool IsActive() const { return ActiveNodes.Num() > 0; } - // Returns nodes that have any work left, not marked as Finished yet + /* Returns nodes that have any work left, not marked as Finished yet. */ UFUNCTION(BlueprintPure, Category = "Flow") const TArray& GetActiveNodes() const { return ActiveNodes; } - // Returns nodes active in the past, done their work + /* Returns nodes active in the past, done their work. */ 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; + + /* 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 protected: - // Expects to be owned (at runtime) by an object with this class (or one of its subclasses) - // NOTE - If the class is an AActor, and the flow asset is owned by a component, - // it will consider the component's owner for the AActor + /* Expects to be owned (at runtime) by an object with this class (or one of its subclasses). + * If the class is an AActor, and the Flow Asset is owned by a component, it will consider the component's owner for the AActor. */ UPROPERTY(EditAnywhere, Category = "Flow") TSubclassOf ExpectedOwnerClass; @@ -433,20 +462,20 @@ class FLOW_API UFlowAsset : public UObject bool IsBoundToWorld() const; ////////////////////////////////////////////////////////////////////////// -// FlowAssetParams support (Start node params for a flow graph) +// FlowAssetParams support (Start node params for a Flow graph) - // Default parameters asset for this Flow Asset (optional) + /* Default parameters asset for this Flow Asset (optional). */ UPROPERTY(EditAnywhere, Category = FlowAssetParams, meta = (ShowCreateNew, HideChildParams)) FFlowAssetParamsPtr BaseAssetParams; #if WITH_EDITOR - // Called before saving the asset. + /* Called before saving the asset. */ virtual void PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) override; - // Generates a new params asset from the Start node. + /* Generates a new params asset from the Start node. */ UFlowAssetParams* GenerateParamsFromStartNode(); - // Generates the FlowAssetParams name for the 'base' (root) asset, used when creating the params asset + /* Generates the FlowAssetParams name for the 'base' (root) asset, used when creating the params asset. */ virtual FString GenerateParamsAssetName() const; protected: diff --git a/Source/Flow/Public/FlowSettings.h b/Source/Flow/Public/FlowSettings.h index f73bf5202..08203ca0d 100644 --- a/Source/Flow/Public/FlowSettings.h +++ b/Source/Flow/Public/FlowSettings.h @@ -16,40 +16,44 @@ UCLASS(Config = Game, defaultconfig, meta = (DisplayName = "Flow")) class FLOW_API UFlowSettings : public UDeveloperSettings { GENERATED_UCLASS_BODY() - static UFlowSettings* Get() { return CastChecked(UFlowSettings::StaticClass()->GetDefaultObject()); } #if WITH_EDITOR virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; #endif - // Set if to False, if you don't want to create client-side Flow Graphs - // And you don't access to the Flow Component registry on clients + /* Set if to False, if you don't want to create client-side Flow Graphs. + * And you don't access to the Flow Component registry on clients. */ UPROPERTY(Config, EditAnywhere, Category = "Networking") bool bCreateFlowSubsystemOnClients; UPROPERTY(Config, EditAnywhere, Category = "SaveSystem") bool bWarnAboutMissingIdentityTags; - // If enabled, runtime logs will be added when a flow node signal mode is set to Disabled + /* If enabled, runtime logs will be added when a flow node signal mode is set to Disabled. */ UPROPERTY(Config, EditAnywhere, Category = "Flow") bool bLogOnSignalDisabled; - // If enabled, runtime logs will be added when a flow node signal mode is set to Pass-through + /* If enabled, runtime logs will be added when a flow node signal mode is set to Pass-through. */ UPROPERTY(Config, EditAnywhere, Category = "Flow") bool bLogOnSignalPassthrough; - // Adjust the Titles for FlowNodes to be more expressive than default - // by incorporating data that would otherwise go in the Description + /* If True, defer the Triggered Outputs for a Flow Asset while it is currently processing a TriggeredInput. + * If False, use legacy behavior for backward compatability. */ + 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") bool bUseAdaptiveNodeTitles; - + #if WITH_EDITOR DECLARE_DELEGATE(FFlowSettingsEvent); FFlowSettingsEvent OnAdaptiveNodeTitlesChanged; #endif - - // Default class to use as a FlowAsset's "ExpectedOwnerClass" + + /* Default class to use as a FlowAsset's "ExpectedOwnerClass" */ UPROPERTY(EditAnywhere, Config, Category = "Nodes") FSoftClassPath DefaultExpectedOwnerClass; diff --git a/Source/Flow/Public/FlowSubsystem.h b/Source/Flow/Public/FlowSubsystem.h index 492742214..3684f89ac 100644 --- a/Source/Flow/Public/FlowSubsystem.h +++ b/Source/Flow/Public/FlowSubsystem.h @@ -100,6 +100,14 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem protected: virtual void AddInstancedTemplate(UFlowAsset* Template); virtual void RemoveInstancedTemplate(UFlowAsset* Template); + +public: + /* Try to flush (and clear) all Deferred Trigger scopes + * Can fail to flush all if a FFlowExecutionGate causes a new halt */ + bool TryFlushAllDeferredTriggerScopes() const; + + /* Clear (do not trigger) any remaining deferred transitions (for shutdown cases) */ + void ClearAllDeferredTriggerScopes(); public: /* Returns all assets instanced by object from another system like World Settings */ diff --git a/Source/Flow/Public/Interfaces/FlowDataPinValueSupplierInterface.h b/Source/Flow/Public/Interfaces/FlowDataPinValueSupplierInterface.h index 7a6266f23..a98e19cef 100644 --- a/Source/Flow/Public/Interfaces/FlowDataPinValueSupplierInterface.h +++ b/Source/Flow/Public/Interfaces/FlowDataPinValueSupplierInterface.h @@ -10,7 +10,7 @@ // Interface to define a Flow Data Pin value supplier. This is generally a UFlowNode subclass, // but we may support external suppliers that are not flow nodes in the future // (eg, for supplying configuration values for the root graph) -UINTERFACE(MinimalAPI, Blueprintable, DisplayName = "Flow Data Pin Value Supplier Interface") +UINTERFACE(MinimalAPI, NotBlueprintable, DisplayName = "Flow Data Pin Value Supplier Interface") class UFlowDataPinValueSupplierInterface : public UInterface { GENERATED_BODY() @@ -23,11 +23,7 @@ class FLOW_API IFlowDataPinValueSupplierInterface public: // Can this node actually supply Data Pin values? // Implementers of this interface will need to use their own logic to answer this question. - UFUNCTION(BlueprintNativeEvent, Category = DataPins, DisplayName = "Can Supply DataPin Values") - bool CanSupplyDataPinValues() const; - virtual bool CanSupplyDataPinValues_Implementation() const { return true; } + virtual bool CanSupplyDataPinValues() const { return true; } - UFUNCTION(BlueprintNativeEvent, Category = DataPins, DisplayName = "Try Supply DataPin") - FFlowDataPinResult TrySupplyDataPin(FName PinName) const; - virtual FFlowDataPinResult TrySupplyDataPin_Implementation(FName PinName) const { return FFlowDataPinResult(); } + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const { return FFlowDataPinResult(); } }; diff --git a/Source/Flow/Public/Interfaces/FlowExecutionGate.h b/Source/Flow/Public/Interfaces/FlowExecutionGate.h index 8e1abdf66..8859dc266 100644 --- a/Source/Flow/Public/Interfaces/FlowExecutionGate.h +++ b/Source/Flow/Public/Interfaces/FlowExecutionGate.h @@ -31,15 +31,6 @@ class FLOW_API FFlowExecutionGate /** True if a gate exists and it currently wants Flow execution halted. */ static bool IsHalted(); - /** If halted, queues the trigger for later. Returns true if queued (caller should early-out). */ - static bool EnqueueDeferredTriggerInput(UFlowAsset* FlowAssetInstance, const FGuid& NodeGuid, const FName& PinName, const struct FConnectedPin& FromPin); - - /** - * Flushes queued trigger inputs (FIFO). - * Safe to call even if nothing is queued. - */ - static void FlushDeferredTriggerInputs(); - private: static IFlowExecutionGate* Gate; }; \ No newline at end of file diff --git a/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h index 0fdf3fa77..3c7825607 100644 --- a/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h +++ b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h @@ -57,4 +57,7 @@ class FLOW_API UFlowNode_Log : public UFlowNode_DefineProperties virtual void UpdateNodeConfigText_Implementation() override; #endif + +public: + EFlowLogVerbosity GetVerbosity() const { return Verbosity; } }; diff --git a/Source/Flow/Public/Nodes/FlowNode.h b/Source/Flow/Public/Nodes/FlowNode.h index 6e2d6fbb1..a9b1543a8 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; @@ -190,8 +207,8 @@ class FLOW_API UFlowNode : public UFlowNodeBase UFUNCTION(BlueprintPure, Category= "FlowNode") bool IsOutputConnected(const FName& PinName, bool bErrorIfPinNotFound = true) const; - bool IsInputConnected(const FFlowPin& FlowPin) const; - bool IsOutputConnected(const FFlowPin& FlowPin) const; + bool IsInputConnected(const FFlowPin& FlowPin, FGuid* FoundGuid = nullptr, FName* OutConnectedPinName = nullptr) const; + bool IsOutputConnected(const FFlowPin& FlowPin, FGuid* FirstFoundGuid = nullptr, FName* OutFirstConnectedPinName = nullptr) const; FFlowPin* FindInputPinByName(const FName& PinName); FFlowPin* FindOutputPinByName(const FName& PinName); @@ -216,6 +233,8 @@ class FLOW_API UFlowNode : public UFlowNodeBase // Data Pins public: + using TFlowPinValueSupplierDataArray = FlowArray::TInlineArray; + #if WITH_EDITORONLY_DATA UPROPERTY(VisibleDefaultsOnly, AdvancedDisplay, Category = "FlowNode", meta = (GetByRef)) TArray AutoInputDataPins; @@ -224,6 +243,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); @@ -236,7 +261,7 @@ class FLOW_API UFlowNode : public UFlowNodeBase // IFlowDataPinValueSupplierInterface public: - virtual FFlowDataPinResult TrySupplyDataPin_Implementation(FName PinName) const override; + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; // Advanced helper for TrySupplyDataPin, which can be overridden in subclasses to provide alternate sourcing for properties. // If returns true, either OutFoundProperty or OutFoundInstancedStruct is expected to carry the property value. @@ -248,6 +273,9 @@ class FLOW_API UFlowNode : public UFlowNodeBase TInstancedStruct& OutFoundInstancedStruct) const; protected: + // Helper for TryGetFlowDataPinSupplierDatasForPinName() + void TryAddSupplierDataToArray(FFlowPinValueSupplierData& InOutSupplierData, TFlowPinValueSupplierDataArray& InOutPinValueSupplierDatas) const; + // Static implementation of the default TryFindPropertyByPinName (which subclasses can incorporate into overrides) static bool TryFindPropertyByPinName_Static( const UObject& PropertyOwnerObject, @@ -267,7 +295,6 @@ class FLOW_API UFlowNode : public UFlowNodeBase const FFlowPin& FlowPin, FFlowDataPinResult& OutSuppliedResult) const; - using TFlowPinValueSupplierDataArray = FlowArray::TInlineArray; bool TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, TFlowPinValueSupplierDataArray& InOutPinValueSupplierDatas) const; // IFlowDataPinGeneratorInterface diff --git a/Source/Flow/Public/Nodes/FlowPin.h b/Source/Flow/Public/Nodes/FlowPin.h index 9aa88d484..752ae6a5d 100644 --- a/Source/Flow/Public/Nodes/FlowPin.h +++ b/Source/Flow/Public/Nodes/FlowPin.h @@ -24,17 +24,17 @@ struct FLOW_API FFlowPin GENERATED_BODY() // A logical name, used during execution of pin - UPROPERTY(EditDefaultsOnly, Category = DataPins) + UPROPERTY(EditDefaultsOnly, Category = FlowPin) FName PinName; // An optional Display Name, you can use it to override PinName without the need to update graph connections - UPROPERTY(EditDefaultsOnly, Category = DataPins) + UPROPERTY(EditDefaultsOnly, Category = FlowPin) FText PinFriendlyName; - UPROPERTY(EditDefaultsOnly, Category = DataPins) + UPROPERTY(EditDefaultsOnly, Category = FlowPin) FString PinToolTip; - // PinType (implies PinCategory) + // Deprecated PinType, use PinTypeName instead (all standard names are defined in FFlowPinTypeNamesStandard) UPROPERTY(Meta = (DeprecatedProperty, DeprecationMessage = "Use PinTypeName instead")) EFlowPinType PinType = EFlowPinType::Invalid; @@ -43,46 +43,25 @@ struct FLOW_API FFlowPin EPinContainerType ContainerType = EPinContainerType::None; protected: - UPROPERTY(EditDefaultsOnly, Category = DataPins) + UPROPERTY() FFlowPinTypeName PinTypeName = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameExec); // Sub-category object // (used to identify the struct or class type for some PinCategories) - UPROPERTY(VisibleAnywhere, Category = DataPins) + UPROPERTY() TWeakObjectPtr PinSubCategoryObject; -#if WITH_EDITORONLY_DATA - // Filter for limiting the compatible classes for this data pin. - // This property is editor-only, but it is automatically copied into PinSubCategoryObject if the PinTypeName matches (for runtime use). - UPROPERTY(EditAnywhere, Category = DataPins, meta = (EditCondition = "PinTypeName == Class", EditConditionHides)) - TSubclassOf SubCategoryClassFilter = UClass::StaticClass(); - - // Filter for limiting the compatible object types for this data pin. - // This property is editor-only, but it is automatically copied into PinSubCategoryObject if the PinTypeName matches (for runtime use). - UPROPERTY(EditAnywhere, Category = DataPins, meta = (EditCondition = "PinTypeName == Object", EditConditionHides)) - TSubclassOf SubCategoryObjectFilter = UObject::StaticClass(); - - // Configuration option for setting the EnumClass to a Blueprint Enum - // (C++ enums must bind by name using SubCategoryEnumName, due to a limitation with UE's UEnum discovery). - // This property is editor-only, but it is automatically copied into PinSubCategoryObject if the PinType matches (for runtime use). - UPROPERTY(EditAnywhere, Category = DataPins, meta = (EditCondition = "PinTypeName == Enum", EditConditionHides)) - TObjectPtr SubCategoryEnumClass = nullptr; - - // name of enum defined in c++ code, will take priority over asset from EnumType property - // (this is a work-around because EnumClass cannot find C++ Enums, - // so you need to type the name of the enum in here, manually) - // See also: FFlowPin::PostEditChangedEnumName() - UPROPERTY(EditAnywhere, Category = DataPins, meta = (EditCondition = "PinTypeName == Enum", EditConditionHides)) - FString SubCategoryEnumName; -#endif - public: - FFlowPin() : PinName(NAME_None) { } + 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) { @@ -205,13 +184,13 @@ struct FLOW_API FFlowPin public: #if WITH_EDITOR - // Must be called from PostEditChangeProperty() by an owning UObject - // whenever PinType, - void PostEditChangedPinTypeOrSubCategorySource(); FText BuildHeaderText() const; static bool ValidateEnum(const UEnum& EnumType); -#endif // WITH_EDITOR + + FEdGraphPinType BuildEdGraphPinType() const; + void ConfigureFromEdGraphPin(const FEdGraphPinType& EdGraphPinType); +#endif void SetPinTypeName(const FFlowPinTypeName& InTypeName); const FFlowPinTypeName& GetPinTypeName() const { return PinTypeName; } @@ -219,17 +198,12 @@ struct FLOW_API FFlowPin void SetPinSubCategoryObject(UObject* Object) { PinSubCategoryObject = Object; } static FFlowPinTypeName GetPinTypeNameForLegacyPinType(EFlowPinType PinType); -#if WITH_EDITOR - FEdGraphPinType BuildEdGraphPinType() const; -#endif - 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); + FORCEINLINE bool IsDataPin() const { return !IsExecPin(); } // -- // Metadata keys for properties that bind and auto-generate Data Pins: @@ -278,25 +252,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 { diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.h b/Source/Flow/Public/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.h new file mode 100644 index 000000000..162b235e8 --- /dev/null +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.h @@ -0,0 +1,33 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Nodes/FlowNode.h" + +#include "FlowNode_BlueprintDataPinSupplierBase.generated.h" + +/** + * FlowNode to give an event to blueprint for supplying data pin values on-demand + * (as there is no longer a blueprint override to TrySupplyDataPin) + */ +UCLASS(Abstract, Blueprintable, meta = (DisplayName = "Blueprint Data-Pin Supplier base")) +class FLOW_API UFlowNode_BlueprintDataPinSupplierBase : public UFlowNode +{ + GENERATED_UCLASS_BODY() + +public: + + // IFlowDataPinValueSupplierInterface + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; + // -- + + // Blueprint signature for TrySupplyDataPin override + UFUNCTION(BlueprintNativeEvent, Category = DataPins, DisplayName = "Try Supply DataPin") + FFlowDataPinResult BP_TrySupplyDataPin(FName PinName) const; + + // Blueprint access for the 'standard' implementation of TrySupplyDataPin + // (for cases where they want to override some pins, but maybe not all, they can have the BP + // override call this version to handle any cases it doesn't want to handle) + UFUNCTION(BlueprintPure, Category = DataPins, DisplayName = "Try Supply DataPin (standard implementation)") + FFlowDataPinResult BP_Super_TrySupplyDataPin(FName PinName) const { return Super::TrySupplyDataPin(PinName); } +}; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_FormatText.h b/Source/Flow/Public/Nodes/Graph/FlowNode_FormatText.h index bc699ddc6..1afe11d85 100644 --- a/Source/Flow/Public/Nodes/Graph/FlowNode_FormatText.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_FormatText.h @@ -33,7 +33,7 @@ class FLOW_API UFlowNode_FormatText : public UFlowNode_DefineProperties public: // IFlowDataPinValueSupplierInterface - virtual FFlowDataPinResult TrySupplyDataPin_Implementation(FName PinName) const override; + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; // -- static const FName OUTPIN_TextOutput; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_Start.h b/Source/Flow/Public/Nodes/Graph/FlowNode_Start.h index 8b31a392b..c62ccfd2b 100644 --- a/Source/Flow/Public/Nodes/Graph/FlowNode_Start.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_Start.h @@ -40,6 +40,6 @@ class FLOW_API UFlowNode_Start // -- // IFlowDataPinValueSupplierInterface - virtual FFlowDataPinResult TrySupplyDataPin_Implementation(FName PinName) const override; + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; // -- }; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h index 572a42aa3..d124247c9 100644 --- a/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h @@ -3,9 +3,10 @@ #pragma once #include "Nodes/FlowNode.h" - #include "FlowNode_SubGraph.generated.h" +class UFlowAssetParams; + /** * Creates instance of provided Flow Asset and starts its execution */ @@ -26,12 +27,12 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode UPROPERTY(EditAnywhere, Category = "Graph") TSoftObjectPtr Asset; - // TODO (gtaylor) Create FlowAssetParams option for the Subgraph & reconcile with connected input pins' values + /* Flow Asset Params to use as the data pin value supplier for the Asset */ + UPROPERTY(EditAnywhere, Category = "Graph", meta = (DefaultForInputFlowPin, FlowPinType = "Object")) + TSoftObjectPtr AssetParams; - /* - * Allow to create instance of the same Flow Asset as the asset containing this node - * Enabling it may cause an infinite loop, if graph would keep creating copies of itself - */ + /* Allow to create instance of the same Flow Asset as the asset containing this node. + * Enabling it may cause an infinite loop, if graph would keep creating copies of itself. */ UPROPERTY(EditAnywhere, Category = "Graph") bool bCanInstanceIdenticalAsset; @@ -53,15 +54,14 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode protected: virtual void OnLoad_Implementation() override; - #if WITH_EDITORONLY_DATA protected: - // All the classes allowed to be used as assets on this subgraph node + /* All the classes allowed to be used as assets on this subgraph node. */ UPROPERTY() TArray> AllowedAssignedAssetClasses; - // All the classes disallowed to be used as assets on this subgraph node + /* All the classes disallowed to be used as assets on this subgraph node. */ UPROPERTY() TArray> DeniedAssignedAssetClasses; #endif @@ -87,7 +87,7 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode // -- // IFlowDataPinValueSupplierInterface - virtual bool CanSupplyDataPinValues_Implementation() const override; + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; // -- // IFlowDataPinGeneratorInterface @@ -97,4 +97,6 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode private: void SubscribeToAssetChanges(); #endif + + static const FName AssetParams_MemberName; }; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h b/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h index 3556dbe91..6a8f8fb97 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h @@ -12,7 +12,20 @@ UCLASS(NotBlueprintable, meta = (DisplayName = "Reroute")) class FLOW_API UFlowNode_Reroute final : public UFlowNode { GENERATED_UCLASS_BODY() - + protected: + // IFlowCoreExecutableInterface virtual void ExecuteInput(const FName& PinName) override; + // -- + + // IFlowDataPinValueSupplierInterface + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; + // -- + +public: +#if WITH_EDITOR + // For configuration from connecting pins via UFlowGraphNode_Reroute + void ConfigureInputPin(const UFlowNode& ConnectedNode, const FEdGraphPinType& EdGraphPinType); + void ConfigureOutputPin(const UFlowNode& ConnectedNode, const FEdGraphPinType& EdGraphPinType); +#endif }; diff --git a/Source/Flow/Public/Types/FlowAutoDataPinsWorkingData.h b/Source/Flow/Public/Types/FlowAutoDataPinsWorkingData.h index 1a5f41bd7..8b99a93a0 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/FlowPinEnums.h b/Source/Flow/Public/Types/FlowPinEnums.h index 53bbecfb4..8998bb7da 100644 --- a/Source/Flow/Public/Types/FlowPinEnums.h +++ b/Source/Flow/Public/Types/FlowPinEnums.h @@ -88,6 +88,9 @@ enum class EFlowDataPinResolveResult : uint8 // Tried to extract with a null FlowNodeBase FailedNullFlowNodeBase, + // The pin is not connected to a node (used in reroutes) + FailedNotConnected, + // Failed with an error message (see the error log) FailedWithError, @@ -200,4 +203,4 @@ namespace EFlowSingleFromArray_Classifiers } } } -}; \ No newline at end of file +}; diff --git a/Source/Flow/Public/Types/FlowPinType.h b/Source/Flow/Public/Types/FlowPinType.h index a9201d41c..03f217702 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 a59cc3c24..0c32158a0 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/FlowPinTypeTemplates.h b/Source/Flow/Public/Types/FlowPinTypeTemplates.h index 6bad39388..f3b0660dd 100644 --- a/Source/Flow/Public/Types/FlowPinTypeTemplates.h +++ b/Source/Flow/Public/Types/FlowPinTypeTemplates.h @@ -834,7 +834,7 @@ namespace FlowPinType for (int32 i = 0; i < Num; ++i) { const FSoftObjectPath Path = InnerSoftProp->GetPropertyValue(ArrHelper.GetRawPtr(i)).ToSoftObjectPath(); - OutValues.Add(Cast(Path.ResolveObject())); + OutValues.Add(Cast(Path.TryLoad())); } return EFlowDataPinResolveResult::Success; } @@ -859,7 +859,7 @@ namespace FlowPinType else if (const TSoftProperty* SoftObjProp = CastField(Property)) { const FSoftObjectPath Path = SoftObjProp->GetPropertyValue_InContainer(Container).ToSoftObjectPath(); - OutValues = { Cast(Path.ResolveObject()) }; + OutValues = { Cast(Path.TryLoad()) }; return EFlowDataPinResolveResult::Success; } else if (const FWeakObjectProperty* WeakProp = CastField(Property)) diff --git a/Source/Flow/Public/Types/FlowPinTypesStandard.h b/Source/Flow/Public/Types/FlowPinTypesStandard.h index e8bca110d..077371f44 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 9037a4f1a..4a731fe1d 100644 --- a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp @@ -26,6 +26,8 @@ void UFlowDebuggerSubsystem::Initialize(FSubsystemCollectionBase& Collection) Super::Initialize(Collection); FFlowExecutionGate::SetGate(this); + + SetFlowDebuggerState(EFlowDebuggerState::InitialRunning, nullptr); } void UFlowDebuggerSubsystem::Deinitialize() @@ -35,6 +37,8 @@ void UFlowDebuggerSubsystem::Deinitialize() FFlowExecutionGate::SetGate(nullptr); } + SetFlowDebuggerState(EFlowDebuggerState::Invalid, nullptr); + Super::Deinitialize(); } @@ -373,20 +377,6 @@ bool UFlowDebuggerSubsystem::HasAnyBreakpointsMatching(const TWeakObjectPtrGetFlowAsset(); - HaltedOnNodeGuid = Node->NodeGuid; -} - -void UFlowDebuggerSubsystem::ClearHaltFlowExecution() -{ - bHaltFlowExecution = false; - HaltedOnFlowAssetInstance.Reset(); - HaltedOnNodeGuid.Invalidate(); -} - void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() { if (!LastHitNodeGuid.IsValid()) @@ -428,11 +418,9 @@ void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode) LastHitNodeGuid = FlowNode->NodeGuid; LastHitPinName = NAME_None; - RequestHaltFlowExecution(FlowNode); - OnDebuggerBreakpointHit.Broadcast(FlowNode); - PauseSession(); + PauseSession(*FlowNode->GetFlowAsset()); } } } @@ -451,98 +439,26 @@ void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode, const FName& P LastHitNodeGuid = FlowNode->NodeGuid; LastHitPinName = PinName; - RequestHaltFlowExecution(FlowNode); - OnDebuggerBreakpointHit.Broadcast(FlowNode); - PauseSession(); + PauseSession(*FlowNode->GetFlowAsset()); } } } -void UFlowDebuggerSubsystem::PauseSession() +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, won't work yet, shows intent for future development - // here be dragons: same as APlayerController::SetPause, but we allow debugger to pause on clients - - // 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; - - if (HaltedOnFlowAssetInstance.IsValid()) - { - if (const UWorld* World = HaltedOnFlowAssetInstance->GetWorld()) - { - GameMode = World->GetAuthGameMode(); - - if (IsValid(GameMode)) - { - bWasPaused = GameMode->IsPaused(); - } - - const UGameInstance* GameInstance = World->GetGameInstance(); - if (IsValid(GameInstance)) - { - PlayerController = GameInstance->GetFirstLocalPlayerController(); - } - } - } - - if (bWasPaused != bPause) - { - if (bPause) - { - // Pausing (from an unpaused state) - - if (IsValid(PlayerController)) - { - if (IsValid(GameMode)) - { - GameMode->SetPause(PlayerController); - } - - if (AWorldSettings* WorldSettings = PlayerController->GetWorldSettings()) - { - WorldSettings->ForceNetUpdate(); - } - } - - // Broadcast the Pause event - OnDebuggerPaused.Broadcast(*HaltedOnFlowAssetInstance.Get()); - } - 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(*HaltedOnFlowAssetInstance.Get()); - } - } + SetFlowDebuggerState(EFlowDebuggerState::Invalid, nullptr); } void UFlowDebuggerSubsystem::ClearHitBreakpoints() @@ -559,8 +475,7 @@ void UFlowDebuggerSubsystem::ClearHitBreakpoints() } } - LastHitNodeGuid.Invalidate(); - LastHitPinName = NAME_None; + ClearLastHitBreakpoint(); } bool UFlowDebuggerSubsystem::IsBreakpointHit(const FGuid& NodeGuid) @@ -588,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 2267ac318..ef454af6d 100644 --- a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h @@ -6,6 +6,7 @@ #include "Debugger/FlowDebuggerTypes.h" #include "Interfaces/FlowExecutionGate.h" +#include "Types/FlowEnumUtils.h" #include "FlowDebuggerSubsystem.generated.h" @@ -14,6 +15,37 @@ class UEdGraphNode; class UFlowAsset; class UFlowNode; +UENUM() +enum class EFlowDebuggerState +{ + // Initialized, running, but never halted + InitialRunning, + + // Running after being pausing + Resumed, + + // Currently paused at a breakpoint + Paused, + + Max UMETA(Hidden), + Invalid = -1 UMETA(Hidden), + Min = 0 UMETA(Hidden), + + // Subranges for classifier checks + PausedGameFirst = Paused UMETA(Hidden), + PausedGameLast = Paused UMETA(Hidden), + + FlushDeferredTriggersFirst = Resumed UMETA(Hidden), + FlushDeferredTriggersLast = Resumed UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowDebuggerState); + +namespace EFlowDebuggerState_Classifiers +{ + FORCEINLINE bool IsPausedGameState(EFlowDebuggerState State) { return FLOW_IS_ENUM_IN_SUBRANGE(State, EFlowDebuggerState::PausedGame); } + FORCEINLINE bool IsFlushDeferredTriggersState(EFlowDebuggerState State) { return FLOW_IS_ENUM_IN_SUBRANGE(State, EFlowDebuggerState::FlushDeferredTriggers); } +} + DECLARE_MULTICAST_DELEGATE_OneParam(FFlowAssetDebuggerEvent, const UFlowAsset& /*FlowAsset*/); DECLARE_MULTICAST_DELEGATE_OneParam(FFlowAssetDebuggerBreakpointHitEvent, const UFlowNode* /*FlowNode*/); @@ -42,7 +74,7 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public public: // IFlowExecutionGate - virtual bool IsFlowExecutionHalted() const override { return bHaltFlowExecution; } + virtual bool IsFlowExecutionHalted() const override { return EFlowDebuggerState_Classifiers::IsPausedGameState(FlowDebuggerState); } // -- virtual void AddBreakpoint(const FGuid& NodeGuid); @@ -79,9 +111,9 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public virtual void MarkAsHit(const UFlowNode* FlowNode); virtual void MarkAsHit(const UFlowNode* FlowNode, const FName& PinName); - virtual void PauseSession(); - 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). @@ -92,9 +124,12 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public /** Clears hit state for all breakpoints. Prefer ClearLastHitBreakpoint() for resume/step logic. */ virtual void ClearHitBreakpoints(); +private: + void SetFlowDebuggerState(EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); + void ManageGameModePaused(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); + protected: - void RequestHaltFlowExecution(const UFlowNode* Node); - void ClearHaltFlowExecution(); + virtual void OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) {} public: virtual bool IsBreakpointHit(const FGuid& NodeGuid); @@ -106,10 +141,8 @@ class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public FFlowAssetDebuggerBreakpointHitEvent OnDebuggerBreakpointHit; FFlowAssetDebuggerEvent OnDebuggerFlowAssetTemplateRemoved; -private: - bool bHaltFlowExecution = false; - TWeakObjectPtr HaltedOnFlowAssetInstance; - FGuid HaltedOnNodeGuid; +protected: + EFlowDebuggerState FlowDebuggerState = EFlowDebuggerState::Invalid; // Track the single breakpoint location that is currently "hit" (node or pin). FGuid LastHitNodeGuid; diff --git a/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp index 1c9748aaa..0f726f34d 100644 --- a/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp +++ b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp @@ -2,17 +2,10 @@ #include "Asset/AssetDefinition_FlowAssetParams.h" #include "Asset/FlowAssetParams.h" -#include "FlowAsset.h" +#include "Asset/FlowAssetParamsUtils.h" #include "FlowEditorLogChannels.h" #include "FlowEditorModule.h" -#include "Types/FlowDataPinValuesStandard.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" #include "ContentBrowserMenuContexts.h" -#include "ContentBrowserModule.h" -#include "FileHelpers.h" -#include "IContentBrowserSingleton.h" -#include "SourceControlHelpers.h" #include "ToolMenus.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(AssetDefinition_FlowAssetParams) @@ -36,7 +29,7 @@ TSoftClassPtr UAssetDefinition_FlowAssetParams::GetAssetClass() const TConstArrayView UAssetDefinition_FlowAssetParams::GetAssetCategories() const { - static const auto Categories = { FFlowAssetCategoryPaths::Flow }; + static const auto Categories = {FFlowAssetCategoryPaths::Flow}; return Categories; } @@ -70,83 +63,37 @@ namespace MenuExtension_FlowAssetParams return; } - FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); - const FString PackagePath = FPackageName::GetLongPackagePath(ParentParams->GetPackage()->GetPathName()); - const FString BaseAssetName = ParentParams->GetName(); - - FString UniquePackageName, UniqueAssetName; - AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + BaseAssetName, TEXT(""), UniquePackageName, UniqueAssetName); - if (UniqueAssetName.IsEmpty()) - { - UE_LOG(LogFlowEditor, Error, TEXT("Failed to generate unique asset name for child params of %s"), *BaseAssetName); - return; - } - - UFlowAssetParams* NewParams = Cast( - AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, ParentParams->GetClass(), nullptr)); - if (!IsValid(NewParams)) - { - UE_LOG(LogFlowEditor, Error, TEXT("Failed to create child Flow Asset Params: %s"), *UniqueAssetName); - return; - } - - if (USourceControlHelpers::IsAvailable()) - { - const FString FileName = USourceControlHelpers::PackageFilename(NewParams->GetPathName()); - if (!USourceControlHelpers::CheckOutOrAddFile(FileName)) - { - UE_LOG(LogFlowEditor, Warning, TEXT("Failed to check out/add %s; saved in-memory only"), *NewParams->GetPathName()); - } - } - - NewParams->ConfigureFlowAssetParams(ParentParams->OwnerFlowAsset, ParentParams, ParentParams->Properties); - - // Save the package (force save even if not prompted) - UPackage* Package = NewParams->GetPackage(); - TArray PackagesToSave = { Package }; - - // Saves without dialog/prompt - const bool bForceSave = true; - if (!UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, bForceSave)) - { - UE_LOG(LogFlowEditor, Error, TEXT("Failed to save child Flow Asset Params: %s"), *NewParams->GetPathName()); - return; - } - - FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - AssetRegistryModule.Get().AssetCreated(NewParams); - TArray AssetsToSync = { NewParams }; - ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); + constexpr bool bShowDialogs = true; + FFlowAssetParamsUtils::CreateChildParamsAsset(*ParentParams, bShowDialogs); } static void RegisterContextMenu() { UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateLambda([]() + { + FToolMenuOwnerScoped OwnerScoped(UE_MODULE_NAME); + UToolMenu* Menu = UE::ContentBrowser::ExtendToolMenu_AssetContextMenu(UFlowAssetParams::StaticClass()); + + FToolMenuSection& Section = Menu->FindOrAddSection("GetAssetActions"); + Section.AddDynamicEntry("Flow Asset Params Commands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { - FToolMenuOwnerScoped OwnerScoped(UE_MODULE_NAME); - UToolMenu* Menu = UE::ContentBrowser::ExtendToolMenu_AssetContextMenu(UFlowAssetParams::StaticClass()); - - FToolMenuSection& Section = Menu->FindOrAddSection("GetAssetActions"); - Section.AddDynamicEntry("Flow Asset Params Commands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) - { - const TAttribute Label = LOCTEXT("FlowAssetParams_CreateChildParams", "Create Child Params"); - const TAttribute ToolTip = LOCTEXT("FlowAssetParams_CreateChildParamsTooltip", "Creates a new Flow Asset Params inheriting from the selected params."); - const FSlateIcon Icon = FSlateIcon(); - - FToolUIAction UIAction; - UIAction.ExecuteAction = FToolMenuExecuteAction::CreateStatic(&ExecuteCreateChildParams); - UIAction.CanExecuteAction = FToolMenuCanExecuteAction::CreateLambda([](const FToolMenuContext& InContext) - { - const UContentBrowserAssetContextMenuContext* Context = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext); - return Context && Context->SelectedAssets.Num() == 1; - }); - InSection.AddMenuEntry("FlowAssetParams_CreateChildParams", Label, ToolTip, Icon, UIAction); - })); + const TAttribute Label = LOCTEXT("FlowAssetParams_CreateChildParams", "Create Child Params"); + const TAttribute ToolTip = LOCTEXT("FlowAssetParams_CreateChildParamsTooltip", "Creates a new Flow Asset Params inheriting from the selected params."); + const FSlateIcon Icon = FSlateIcon(); + + FToolUIAction UIAction; + UIAction.ExecuteAction = FToolMenuExecuteAction::CreateStatic(&ExecuteCreateChildParams); + UIAction.CanExecuteAction = FToolMenuCanExecuteAction::CreateLambda([](const FToolMenuContext& InContext) + { + const UContentBrowserAssetContextMenuContext* Context = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext); + return Context && Context->SelectedAssets.Num() == 1; + }); + InSection.AddMenuEntry("FlowAssetParams_CreateChildParams", Label, ToolTip, Icon, UIAction); })); + })); } static FDelayedAutoRegisterHelper DelayedAutoRegister(EDelayedRegisterRunPhase::EndOfEngineInit, &RegisterContextMenu); } -#undef LOCTEXT_NAMESPACE \ No newline at end of file +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp b/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp new file mode 100644 index 000000000..194e1d3f2 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp @@ -0,0 +1,176 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetParamsFactory.h" + +#include "Asset/FlowAssetParams.h" +#include "Asset/FlowAssetParamsUtils.h" + +#include "ContentBrowserModule.h" +#include "Framework/Application/SlateApplication.h" +#include "IContentBrowserSingleton.h" +#include "Misc/MessageDialog.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/SWindow.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "FlowAssetParamsFactory" + +UFlowAssetParamsFactory::UFlowAssetParamsFactory() +{ + SupportedClass = UFlowAssetParams::StaticClass(); + + bCreateNew = true; + bEditorImport = false; + bEditAfterNew = true; +} + +bool UFlowAssetParamsFactory::ConfigureProperties() +{ + SelectedParentParams.Reset(); + return ShowParentPickerDialog(); +} + +bool UFlowAssetParamsFactory::ShowParentPickerDialog() +{ + const FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + + // Holds current required parent selection (the params asset) + TSharedPtr CurrentSelection = MakeShared(); + + FAssetPickerConfig ParamsPickerConfig; + ParamsPickerConfig.Filter.ClassPaths.Add(UFlowAssetParams::StaticClass()->GetClassPathName()); + ParamsPickerConfig.InitialAssetViewType = EAssetViewType::List; + ParamsPickerConfig.SelectionMode = ESelectionMode::Single; + ParamsPickerConfig.bAllowNullSelection = false; + ParamsPickerConfig.bFocusSearchBoxWhenOpened = true; + + ParamsPickerConfig.OnAssetSelected = FOnAssetSelected::CreateLambda( + [CurrentSelection](const FAssetData& AssetData) + { + *CurrentSelection = AssetData; + }); + + const TSharedRef ParamsPicker = ContentBrowserModule.Get().CreateAssetPicker(ParamsPickerConfig); + + bool bUserAccepted = false; + + TSharedPtr PickerWindow = SNew(SWindow) + .Title(LOCTEXT("CreateChildParamsTitle", "Create Flow Asset Params")) + .SizingRule(ESizingRule::UserSized) + .ClientSize(FVector2D(850, 600)) + .SupportsMinimize(false) + .SupportsMaximize(false); + + PickerWindow->SetContent( + SNew(SBorder) + .Padding(8.f) + [ + SNew(SVerticalBox) + + // Parent picker + + SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(0.f, 0.f, 0.f, 8.f) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateChildParamsHelp", "Choose Parent Flow Asset Params:\n")) + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) + [ + ParamsPicker + ] + ] + + // Buttons + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.f, 0.f, 6.f, 0.f) + [ + SNew(SButton) + .Text(LOCTEXT("OK", "OK")) + .IsEnabled_Lambda([CurrentSelection]() + { + return CurrentSelection->IsValid(); + }) + .OnClicked_Lambda([&bUserAccepted, PickerWindow]() + { + bUserAccepted = true; + PickerWindow->RequestDestroyWindow(); + return FReply::Handled(); + }) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("Cancel", "Cancel")) + .OnClicked_Lambda([PickerWindow]() + { + PickerWindow->RequestDestroyWindow(); + return FReply::Handled(); + }) + ] + ] + ] + ); + + FSlateApplication::Get().AddModalWindow(PickerWindow.ToSharedRef(), nullptr); + + if (!bUserAccepted || !CurrentSelection->IsValid()) + { + return false; + } + + SelectedParentParams = TSoftObjectPtr(CurrentSelection->ToSoftObjectPath()); + if (SelectedParentParams.IsNull()) + { + FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("NoParentSelected", "You must select a parent Flow Asset Params asset.")); + + return false; + } + + return true; +} + +UObject* UFlowAssetParamsFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + if (SelectedParentParams.IsNull()) + { + FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("FactoryMissingParent", "No parent params were selected.")); + + return nullptr; + } + + UFlowAssetParams* Parent = SelectedParentParams.LoadSynchronous(); + if (!IsValid(Parent)) + { + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format(LOCTEXT("ParentLoadFail", "Failed to load selected parent params:\n{0}"), + FText::FromString(SelectedParentParams.ToString()))); + + return nullptr; + } + + FText FailureReason; + constexpr bool bShowDialogs = true; + UFlowAssetParams* NewParams = FFlowAssetParamsUtils::CreateChildParamsAsset(*Parent, bShowDialogs, &FailureReason); + + // FactoryCreateNew expects the created asset (or nullptr). The helper already shows dialogs/logs on failure. + return NewParams; +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp index f00f98c80..eed0440a5 100644 --- a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp +++ b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp @@ -9,6 +9,7 @@ #include "Graph/Nodes/FlowGraphNode.h" #include "Interfaces/FlowExecutionGate.h" #include "FlowAsset.h" +#include "FlowSubsystem.h" #include "Editor/UnrealEdEngine.h" #include "Engine/Engine.h" @@ -71,16 +72,14 @@ void UFlowDebugEditorSubsystem::OnBeginPIE(const bool bIsSimulating) void UFlowDebugEditorSubsystem::OnResumePIE(const bool bIsSimulating) { - // Clear only the last-hit breakpoint to return to enabled/disabled visuals without racing against - // a newly hit breakpoint during FlushDeferredTriggerInputs(). - ClearHaltFlowExecution(); - ClearLastHitBreakpoint(); - // Editor-level resume event (also used by Advance Single Frame). // This does not necessarily flow through AGameModeBase::ClearPause(), so we must unhalt Flow here. - ResumeSession(); + ClearLastHitBreakpoint(); - FFlowExecutionGate::FlushDeferredTriggerInputs(); + if (HaltedOnFlowAssetInstance.IsValid()) + { + ResumeSession(*HaltedOnFlowAssetInstance.Get()); + } } void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) @@ -88,8 +87,7 @@ void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) // Ensure we don't carry over a halted state between PIE sessions. ClearHitBreakpoints(); - ClearHaltFlowExecution(); - FFlowExecutionGate::FlushDeferredTriggerInputs(); + StopSession(); for (const TPair, TSharedPtr>& Log : RuntimeLogs) { @@ -117,27 +115,86 @@ void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) } } -void UFlowDebugEditorSubsystem::PauseSession() +void UFlowDebugEditorSubsystem::PauseSession(UFlowAsset& FlowAssetInstance) +{ + HaltedOnFlowAssetInstance = &FlowAssetInstance; + + Super::PauseSession(FlowAssetInstance); +} + +void UFlowDebugEditorSubsystem::ResumeSession(UFlowAsset& FlowAssetInstance) { - // do not call Super, non-PIE world has its only Pause/Resume logic + HaltedOnFlowAssetInstance = &FlowAssetInstance; - constexpr bool bShouldBePaused = true; - const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); - if (!bWasPaused) + Super::ResumeSession(FlowAssetInstance); +} + +void UFlowDebugEditorSubsystem::StopSession() +{ + // Drop any pending deferred triggers — we are stopping the session entirely + if (HaltedOnFlowAssetInstance.IsValid()) { - GUnrealEd->PlaySessionPaused(); + UFlowSubsystem* FlowSubsystem = HaltedOnFlowAssetInstance->GetFlowSubsystem(); + + if (IsValid(FlowSubsystem)) + { + FlowSubsystem->ClearAllDeferredTriggerScopes(); + } } + + HaltedOnFlowAssetInstance.Reset(); + + Super::StopSession(); } -void UFlowDebugEditorSubsystem::ResumeSession() +void UFlowDebugEditorSubsystem::OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) { - // do not call Super, non-PIE world has its only Pause/Resume logic + check(PrevState != NextState); + + using namespace EFlowDebuggerState_Classifiers; - constexpr bool bShouldBePaused = false; - const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bShouldBePaused); - if (bWasPaused) + const bool bIsPausedGameStatePrev = IsPausedGameState(PrevState); + const bool bIsPausedGameStateNext = IsPausedGameState(NextState); + + // Handle Pause/Unpause of the game & pie systems + if (bIsPausedGameStatePrev != bIsPausedGameStateNext) + { + const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bIsPausedGameStateNext); + + if (bIsPausedGameStateNext && !bWasPaused) + { + GUnrealEd->PlaySessionPaused(); + } + else if (!bIsPausedGameStateNext && bWasPaused) + { + GUnrealEd->PlaySessionResumed(); + } + } + + // Issue the broadcasts for specific state entry + FLOW_ASSERT_ENUM_MAX(EFlowDebuggerState, 3); + if (NextState == EFlowDebuggerState::Paused) { - GUnrealEd->PlaySessionResumed(); + OnDebuggerPaused.Broadcast(*FlowAssetInstance); + } + else if (NextState == EFlowDebuggerState::Resumed) + { + OnDebuggerResumed.Broadcast(*FlowAssetInstance); + } + + UFlowSubsystem* FlowSubsystem = + IsValid(FlowAssetInstance) ? + FlowAssetInstance->GetFlowSubsystem() : + nullptr; + + if (FlowSubsystem && IsFlushDeferredTriggersState(NextState)) + { + // Flush any deferred triggers now that halt is cleared. + FlowSubsystem->TryFlushAllDeferredTriggerScopes(); + + // NOTE (gtaylor) this flush needs to be the last thing we do in this function + // (thus the explicit return to emphasize it), as this flush can be interrupted by another breakpoint + return; } } diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowPinCustomization.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowPinCustomization.cpp index e5dfbfb30..e19623835 100644 --- a/Source/FlowEditor/Private/DetailCustomizations/FlowPinCustomization.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowPinCustomization.cpp @@ -26,8 +26,6 @@ void FFlowPinCustomization::OnChildPropertyValueChanged() { if (FFlowPin* FlowPin = GetFlowPin()) { - FlowPin->PostEditChangedPinTypeOrSubCategorySource(); - IFlowExtendedPropertyTypeCustomization::OnAnyChildPropertyChanged(); } } diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp index 68b6ce845..0995dca24 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp @@ -15,6 +15,7 @@ #include "FlowPinSubsystem.h" #include "FlowSettings.h" #include "AddOns/FlowNodeAddOn.h" +#include "Graph/Nodes/FlowGraphNode_Reroute.h" #include "Nodes/FlowNode.h" #include "Nodes/FlowNodeAddOnBlueprint.h" #include "Nodes/FlowNodeBlueprint.h" @@ -876,24 +877,25 @@ TSharedPtr UFlowGraphSchema::GetCreateCommentAction() cons PRAGMA_DISABLE_DEPRECATION_WARNINGS void UFlowGraphSchema::OnPinConnectionDoubleCicked(UEdGraphPin* PinA, UEdGraphPin* PinB, const FVector2D& GraphPosition) const { - if (!FFlowPin::IsExecPinCategory(PinA->PinType.PinCategory) || !FFlowPin::IsExecPinCategory(PinB->PinType.PinCategory)) - { - // Disallowing Reroute node creation for non-exec connections (until we have a good solution for it) - - return; - } - const FScopedTransaction Transaction(LOCTEXT("CreateFlowRerouteNodeOnWire", "Create Flow Reroute Node")); const FVector2D NodeSpacerSize(42.0f, 24.0f); const FVector2D KnotTopLeft = GraphPosition - (NodeSpacerSize * 0.5f); UEdGraph* ParentGraph = PinA->GetOwningNode()->GetGraph(); - UFlowGraphNode* NewReroute = FFlowGraphSchemaAction_NewNode::CreateNode(ParentGraph, nullptr, UFlowNode_Reroute::StaticClass(), KnotTopLeft, false); + UFlowGraphNode* NewEdNode = FFlowGraphSchemaAction_NewNode::CreateNode(ParentGraph, nullptr, UFlowNode_Reroute::StaticClass(), KnotTopLeft, false); + UFlowGraphNode_Reroute* NewRerouteEdNode = Cast(NewEdNode); - PinA->BreakLinkTo(PinB); - PinA->MakeLinkTo((PinA->Direction == EGPD_Output) ? NewReroute->InputPins[0] : NewReroute->OutputPins[0]); - PinB->MakeLinkTo((PinB->Direction == EGPD_Output) ? NewReroute->InputPins[0] : NewReroute->OutputPins[0]); + if (PinA->Direction == EGPD_Output) + { + check(PinB->Direction == EGPD_Input && PinA->Direction == EGPD_Output); + NewRerouteEdNode->ConfigureRerouteNodeFromPinConnections(*PinB, *PinA); + } + else + { + check(PinA->Direction == EGPD_Input && PinB->Direction == EGPD_Output); + NewRerouteEdNode->ConfigureRerouteNodeFromPinConnections(*PinA, *PinB); + } } PRAGMA_ENABLE_DEPRECATION_WARNINGS diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp index 6626c8173..92a0910ce 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp @@ -1140,20 +1140,7 @@ void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextO 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); - } - } + DataResult = FlowNodeBase->TryResolveDataPin(GraphPinObj->PinName); } FString ValueString; diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp index 63422adcd..eb5a83da5 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp @@ -29,3 +29,29 @@ bool UFlowGraphNode_Reroute::CanPlaceBreakpoints() const { return false; } + +#if WITH_EDITOR +void UFlowGraphNode_Reroute::ConfigureRerouteNodeFromPinConnections(UEdGraphPin& InPin, UEdGraphPin& OutPin) +{ + UFlowNode_Reroute* NewRerouteTemplate = Cast(NodeInstance); + + UFlowGraphNode* FlowGraphNodeIn = Cast(InPin.GetOwningNode()); + UFlowNode* NodeIn = Cast(FlowGraphNodeIn->GetFlowNodeBase()); + + UFlowGraphNode* FlowGraphNodeOut = Cast(OutPin.GetOwningNode()); + UFlowNode* NodeOut = Cast(FlowGraphNodeOut->GetFlowNodeBase()); + + // Update the FlowPin structs on the UFlowNode_Reroute + NewRerouteTemplate->ConfigureInputPin(*NodeIn, InPin.PinType); + NewRerouteTemplate->ConfigureOutputPin(*NodeOut, OutPin.PinType); + + InPin.BreakLinkTo(&OutPin); + + // Copy the PinType information from the connected pins + InputPins[0]->PinType = InPin.PinType; + OutputPins[0]->PinType = OutPin.PinType; + + InPin.MakeLinkTo(OutputPins[0]); + OutPin.MakeLinkTo(InputPins[0]); +} +#endif diff --git a/Source/FlowEditor/Public/Asset/FlowAssetParamsFactory.h b/Source/FlowEditor/Public/Asset/FlowAssetParamsFactory.h new file mode 100644 index 000000000..b9ccc5721 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowAssetParamsFactory.h @@ -0,0 +1,34 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Factories/Factory.h" +#include "UObject/SoftObjectPtr.h" + +#include "FlowAssetParamsFactory.generated.h" + +class UFlowAssetParams; + +/** + * Factory for creating Flow Asset Params via the Content Browser "Add New" menu. + * This creation path is strictly for creating CHILD params: the user must select a Parent FlowAssetParams. + */ +UCLASS(HideCategories = Object) +class FLOWEDITOR_API UFlowAssetParamsFactory : public UFactory +{ + GENERATED_BODY() + +public: + UFlowAssetParamsFactory(); + + // UFactory + virtual bool ConfigureProperties() override; + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + // -- + +private: + // Required selection + TSoftObjectPtr SelectedParentParams; + + bool ShowParentPickerDialog(); +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h index cc874e10a..6833c0570 100644 --- a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h +++ b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h @@ -24,6 +24,8 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem protected: TMap, TSharedPtr> RuntimeLogs; + TWeakObjectPtr HaltedOnFlowAssetInstance; + virtual void OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) override; virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) override; @@ -33,8 +35,10 @@ class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem virtual void OnResumePIE(const bool bIsSimulating); virtual void OnEndPIE(const bool bIsSimulating); - virtual void PauseSession() override; - virtual void ResumeSession() 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/Graph/Nodes/FlowGraphNode.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h index c01ccef93..c7a985ad3 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h @@ -31,7 +31,7 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode ////////////////////////////////////////////////////////////////////////// // Flow node -private: +protected: // The FlowNode or FlowNodeAddOn runtime instance that is being edited by this UFlowGraphNode UPROPERTY(Instanced) TObjectPtr NodeInstance; diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h index a3c6dbd88..53d1f9fa7 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h @@ -16,4 +16,6 @@ class FLOWEDITOR_API UFlowGraphNode_Reroute : public UFlowGraphNode // -- virtual bool CanPlaceBreakpoints() const override; + + void ConfigureRerouteNodeFromPinConnections(UEdGraphPin& InPin, UEdGraphPin &OutPin); };